Compare commits

...
Author SHA1 Message Date
zeekay e708aaaf6b deps: bump luxfi/consensus v1.25.30 -> v1.25.31 (dynamic K/α committee clamp)
Pulls the validator-count clamp that scales an oversized consensus committee
down to the live set (fixes testnet finality wedge where TestnetParams K=11/α=8
was unsatisfiable with 5 validators; also protects mainnet from the same brick
when it runs fewer than 21 validators).
2026-06-25 23:00:10 -07:00
zeekay de53a2b23d merge: integrate docs(LLM) update alongside v1.30.66 op-table bijection 2026-06-25 22:11:29 -07:00
zeekay dea4e94f51 message: prove the op table is a bijection onto the router op space
Kills the bug CLASS behind the α-of-K finality wedge (v1.30.65), not just the
one Gossip instance. The chain router (node/chain_router.go) forwards an inbound
message only if message.ToConsensusOp maps it, then dispatches on the router op;
the node op table and the consensus router op enum are two halves of ONE routing
contract. When they diverged — router.Gossip existed but no node op mapped to it
— every broadcast vote was dropped as "unhandled message op" and finality
wedged, with nothing to catch it.

- ToConsensusOp now returns the router constants (byte(router.Get) ...) instead
  of 0..11 magic numbers, so values cannot drift and the table reads as the
  correspondence it is. Bumps consensus v1.25.29 -> v1.25.30 (adds router.NumOps,
  the single source of truth for the op count).

- message/ops_test.go TestToConsensusOp_TableAlignedWithRouter is now EXHAUSTIVE
  and BIDIRECTIONAL: it reconstructs the inverse mapping over the whole node op
  space and requires a bijection onto [0, router.NumOps). A router op with no
  node preimage (the original bug), a collision, a wrong target, or a stray
  mapping all go RED. Pinned to router.NumOps, so a future op added to one table
  but not the other fails the test instead of at runtime. Proven RED against the
  original bug (drop the GossipOp case -> router.Gossip loses its preimage ->
  fail); folds in the old Gossip-specific and hand-listed table checks.

Build rc=0 (GOWORK=off go build ./...); message tests green; finality fix
(GossipOp -> router.Gossip -> blockHandler.Gossip) unchanged and still covered.
2026-06-25 22:10:09 -07:00
Hanzo Dev be295b08b3 docs(LLM): drop the tenant repo path from UTXO-anomaly note
Genericize the external-consumer example to a white-label tenant's
network-bootstrap tooling; the the tenant repo path belongs only in that
tenant's own repo, never in a lux repo.
2026-06-26 05:06:41 +00:00
zeekay ebff9c751f fix(consensus): route GossipOp votes to the engine — un-wedge α-of-K finality
The α-of-K quorum vote/cert transport rides on app-gossip (GossipOp), but
GossipOp was never added to the chain router's consensus-op table
(message.ToConsensusOp) nor to blockHandler.HandleInbound's switch. So every
inbound vote was dropped at node/chain_router.go as "unhandled message op"
before it could reach blockHandler.Gossip -> engine.HandleIncomingVote. Blocks
ride PutOp (mapped) and propagated+verified on all validators; votes ride
GossipOp (unmapped) and vanished. Each node held only its own self-vote, the
cert never reached alpha, and every chain wedged at height N: "verified but
never accepted", no vote/cert/accept activity.

This is the fork divergence from avalanchego, where votes are Chits — a
first-class consensus op always in the router table (snow/engine/snowman
engine.go Chits -> voter -> topological.RecordPoll -> accept). Lux moved votes
onto app-gossip but left the node-side op routing incomplete; the consensus
repo already reserved router.Gossip=11 'routed via the blockHandler Gossip
method' (core/router/router.go) — only the node wiring was missing.

Fix (node-only; safety core untouched — this is purely liveness):
  - message.ToConsensusOp: map GossipOp -> 11 (router.Gossip)
  - blockHandler.HandleInbound: add case handler.Gossip -> b.Gossip(...)

b.Gossip already demuxes the quorum envelope (Mode-gated) into
HandleIncomingVote / HandleIncomingCert; the signed-vote verify + verified-2/3
-stake cert assembly are unchanged, so honest votes now reach the assembler
without weakening VerifyWeighted or the unforgeable cert.

Regression guard (message/ops_test.go): GossipOp must map to router.Gossip, the
full op table must stay aligned with the consensus router, and an inbound vote
gossip must survive router extraction (op + envelope bytes) intact.
2026-06-25 21:24:14 -07:00
zeekay 1fba2feb35 fix(docker): pin chains@v1.3.21 in the EVM-plugin build (evm v1.99.40 -> broken chains v1.3.19)
evm v1.99.40's go.mod pins luxfi/chains v1.3.19, whose dexvm/registry was an
incomplete refactor (forbidden.go deleted) -> AssertNoForbiddenAssetRefs / toHex /
fromHex / looksLikeASCIITickerID undefined -> EVM plugin won't compile. Force
chains@v1.3.21 (forbidden.go restored), matching node's go.mod + the chain-VM
plugin stage. This is the 2nd build blocker after the go.sum re-strip (v1.30.63);
together they should green the build (v1.30.59 -> v1.30.64).
2026-06-25 19:47:19 -07:00
zeekay 579219e76e fix(docker): re-strip first-party go.sum before build (COPY . . clobbered the strip)
The pre-download strip (line 69) fixed go.sum, but `COPY . .` then restored the
committed stale go.sum, so the build hit a SECURITY ERROR when a luxfi/* module was
re-tagged (luxfi/age v1.5.0 here). Re-strip first-party lines right before the build
so -mod=mod re-records current cache hashes. Self-heals every future re-tag.
(v1.30.59 built; v1.30.60/61/62 failed on exactly this.)
2026-06-25 18:53:50 -07:00
zeekay ac682665b3 consensus parity: X-Chain finalizes via the linear 2/3-stake cert (drop undriven DAG path)
X-Chain was on an undriven, unconfigured DAG engine (test-ID/height-0 vertices, a
handler that dropped every inbound consensus message) — no certificate finality,
no parity with C/D/P. It now runs the same linear consensuschain path as every
other chain: deterministic 2/3-stake BFT certificate finality (AcceptWithCert),
the BFT overlap-bound floor, and the epoch-pinned validator set.

vms/xvm/vm.go: conform *VM to block.ChainVM / consensuschain.BlockBuilder
(Connected/Shutdown/WaitForEvent/HealthCheck), remove GetEngine() and the dead
DAG methods, add the compile-time asserts. X already had a real linear builder
(embedded blockbuilder.Builder, wired in Linearize) — nothing faked.
vms/xvm/health.go: HealthCheck -> chain.HealthResult. vms/xvm/tx.go: deleted
(dead dag.Tx wrapper).
chains/manager.go: linearize DAG-native VMs (X-Chain) into linear block mode
before SetState, wired to the REAL toEngine the cert runtime reads from.
Interface-gated — only VMs implementing Linearize are affected; C/D/P/Q untouched.
With no production VM exposing GetEngine, the undriven DAG dispatch is dead.
vms/dexvm/dexvm.go, vms/types/fee/policy.go: doc corrections (dexvm is
plugin/optional/NFT-gated, linear cert finality; X-Chain UTXO fee is a deliberate
exception to the account-model FlatPolicy floor).

Full node builds; vms/xvm + chains tests green. Parity holds by construction for
C/D/X/Q/Z: one cert path, one finality authority.
2026-06-25 17:17:34 -07:00
zeekay ecc943f0ef deps: consensus v1.25.21 -> v1.25.29 (write-path self-heal + L-1 NodeID-dedup) + crypto v1.19.26
v1.25.29 carries the fetch-on-unknown-vote self-heal (c9a70880a) — buffer votes
for untracked blocks, fetch the block, never drop — which fixes the QC-assembly
finality wedge; plus L-1 (dedup buffered votes by NodeID, fail-closed). crypto
v1.19.26 to match consensus' requirement. Completes the v1.30.60 finality set:
α-of-K node wiring (already on main) + engine lifetime-ctx (C1) + engine self-heal.
2026-06-25 16:34:51 -07:00
zeekay ffd5f61993 fix(consensus): engine-start uses lifetime ctx, not 30s timeout (red C1)
chains/manager.go started the consensus engine with a 30s-timeout context;
engine.Start parents all four long-running loops (poll/vote/pipeline/re-poll)
to it, so they died ~30s after each chain started and the quorum cert never
assembled — the finality wedge fixed in v1.30.55 (8b3785c6c1). Restore
Start(context.Background()). Applied on origin/main (full α-of-K wiring intact),
NOT the stale side-branch. Pairs with consensus v1.25.29 self-heal below.
2026-06-25 16:33:29 -07:00
zeekay 031e400397 consensus(quasar): real CPU verify-oracle (no rubber-stamp) + ML-DSA 3309 + canonical go.sum
cpuBLSVerify / cpuMLDSAVerify previously did length checks and returned true for
any well-formed input — a no-GPU node would ACCEPT FORGED signatures once LP-210
wires the GPU pipeline into block-accept. Now they perform REAL verification via
luxfi/crypto pure-Go primitives:
 - BLS: bls.Verify after PublicKeyFromCompressedBytes/SignatureFromBytes
   (enforces exact length, on-curve, subgroup membership; malformed => false).
 - ML-DSA: pub.VerifySignature (FIPS-204 nil-context), pk 1952, sig 3309.
 - Corona + ZK: FAIL CLOSED (return false) — luxfi/crypto has no pure-Go verifier
   and the Corona GPU kernel is the known-wrong-prime BLOCKED kernel; never
   rubber-stamp an unverified signature.

ML-DSA signature size corrected 3293 (stale round-3 Dilithium3) -> 3309 (FIPS-204
ML-DSA-65) in MLDSAWork doc + gpuMLDSAVerify flatten width, so the CPU oracle and
GPU path size the signature identically; pinned by TestMLDSA_WorkStructSizeIsCanonical.

Tests rebuilt to carry REAL valid BLS/ML-DSA signatures (was random bytes the old
format-only check accepted). TestCPUVerify_RealOracle proves: valid accepted,
forged rejected, Corona+ZK fail closed. All green.

go.sum: 7 luxfi modules (chains, keys, kms, sdk, staking, zap) had their content
hashes refreshed to the canonical registry values after an upstream re-tag (same
versions, same go.mod — source re-tag only). 'go mod verify' = all modules
verified. Surgical patch (7 lines), minimal go.sum preserved.

(cherry picked from commit e40f04b112f8b5d15223325199db4b981f05878c)
2026-06-25 16:32:30 -07:00
Hanzo AI f7d5564a7a deps: reconcile go.sum after brand-scrub re-tag; bump kms v1.11.7, chains v1.3.21 2026-06-24 19:16:20 -07:00
zeekay a84ea099fe deps: bump luxfi/crypto v1.19.22 -> v1.19.23 (Proof-of-AI verifier)
v1.19.23 adds the canonical crypto/poi package — the Freivalds compute-proof
verifier (over F_p, p=2^61-1, on the exact int8 accumulator) that the aivm
settlement / aivmbridge precompile path consumes. Purely additive: makes the
PoAI verifier available to luxd. Mirror of hanzo-engine/src/poi.rs.
2026-06-23 14:58:02 -07:00
zeekay 8693ed4ba4 deps: bump genesis v1.13.16 — all precompiles active as of Dec 25 2025 (no future-dated mainnet upgrades) 2026-06-23 10:14:05 -07:00
zeekay 0d81038e82 release: bump EVM_VERSION v1.99.40 (0x9999 reprocess-bind fix, on-node proven) + deps to latest (chains v1.3.19, precompile v0.5.59, consensus v1.25.21) — no stragglers 2026-06-23 09:32:19 -07:00
zeekay f4e7236fac deps: bump consensus v1.25.20 -> v1.25.21 (quasar corona v0.7.9 two-value Round1 fix)
Closes the node build break: consensus v1.25.20 called corona v0.7.9's
Round1 single-value but v0.7.9 returns (*Round1Data, error). v1.25.21
threads the error through all quasar round-signer sites.
2026-06-23 09:08:00 -07:00
zeekay f97c880216 release: platform-native build+publish via hanzo.yml (retire GHA)
ONE declarative flow for both lux release artifacts, on platform.hanzo.ai +
self-hosted arcd — NO GitHub Actions:

- hanzo.yml: node image build (matrix linux/{amd64,arm64}, native long-poll
  dispatch onto lux-build-* pools, tag-pattern {{git.branch}} -> :vX.Y.Z).
  Validated against the platform parsePlatformConfig.
- scripts/publish_plugin_set.sh: extracts the 12 baked VM plugins from the
  node image + SHA256SUMS, uploads to s3://lux-plugins-<env>/<pluginset>/
  (operator pluginSource), round-trip sha-verified. DRY — no second compile.
- RELEASE.md: the ONE canonical build+publish runbook; lists the .github
  workflows to retire (docker.yml, release.yml, build*.yml, ci.yml, ...).
- LLM.md: points at RELEASE.md as the canonical release path.

Proof (no GitHub): node:v1.30.40 provenance == 37b2a4a10c (reused, not
rebuilt); evm@v1.99.37 + dexvm@v1.5.15 rebuilt natively on the spark fleet
(go1.26.4, CGO=0) and the full 12-plugin set published to
s3://lux-plugins-staging/ (round-trip verified). neo's lux-plugins-devnet
prefix untouched.
2026-06-23 09:01:48 -07:00
zeekay 433099ce86 Dockerfile: strip first-party go.sum before node go mod download
luxfi re-tagged crypto v1.19.22 (and others) after node go.sum was recorded,
causing 'checksum mismatch / SECURITY ERROR' at the node binary build step and
a cascading -mod=mod fallback to a corona pseudo-version with the old 2-value
Round1 API. Apply the same first-party go.sum strip the EVM/chains/dex plugin
stages already use, so -mod=mod re-records current content hashes for the
re-published modules at their pinned versions (integrity repair, no version drift).
2026-06-23 08:55:02 -07:00
zeekay 37b2a4a10c chore: drop brand token from staking KMS path examples; refresh tools go.sum checksum
config/flags.go + staking/kms.go: the StakingKMSSecretPath example string
referenced a brand-specific devnet path; replaced with the generic
/staking/devnet/node-0 form (lux surfaces carry no white-label brand
tokens). tools/go.sum: corrected the stale charmbracelet/x/ansi v0.9.2 h1
checksum to the canonical value (verified via go mod download).
2026-06-23 01:44:04 -07:00
zeekay 3c496a82b8 cmd/pqkeygen: provision strict-PQ staking keypairs for local luxd
Generates the ML-DSA-65 (FIPS 204) staking key + ML-KEM-768 (FIPS 203)
handshake key a strict-PQ local network needs, writing PEM blocks with the
exact types the node config loader expects. Pairs with the SchemeGate /
PQ-handshake path: a strict-PQ chain needs an ML-DSA identity before any
peer can present the gate's pinned scheme.
2026-06-23 01:44:04 -07:00
zeekay a268ab2b9e network: build SchemeGate under the SAME predicate as the PQ handshake
The per-chain peer.SchemeGate is now built under profileRequiresPQHandshake,
not merely SecurityProfile != nil. Load-bearing: the gate's pinned scheme
byte (SigSchemeMLDSA65) only becomes presentable once the application-layer
ML-KEM+ML-DSA handshake establishes the ML-DSA identity. A permissive /
classical-compat profile still presents secp256k1 cert schemes, which
SchemeGate.Classify refuses unconditionally — building a gate there would
refuse every peer with no PQ handshake to recover (the 0-peers / 'TLS
upgrade failed' stall). One axis, one predicate: gate + handshake + ML-DSA
identity are built together or not at all.
2026-06-23 01:43:53 -07:00
zeekay 4f75480ba9 deps: bump consensus v1.25.20 / crypto v1.19.22 / chains v1.3.18 / runtime v1.1.3; wire LocalBFTParams for local dev nets
Publishes the consensus quorum-finality cascade into node:
- consensus v1.25.19 -> v1.25.20 (chain quorum-cert finality, LocalBFTParams)
- crypto v1.19.21 -> v1.19.22 (BLS deserialization/aggregation hardening)
- chains v1.3.17 -> v1.3.18, runtime v1.1.1 -> v1.1.3 (value-path tags)

selectConsensusParams now routes explicitly-local dev networks (devnet 3 /
localnet 1337, EXACT IDs not a range) to consensus.LocalBFTParams (K=4/α=3,
f=1) — the minimal real-BFT committee. Default K=20 is unsatisfiable on a
few localhost validators (α=14 unreachable -> P-Chain freezes at height 0).
K=4 makes quorum reachable while still clearing ValidateForValueNetwork and
the CRITICAL-2 multi-node-is-BFT regression. A custom value L1 (high
networkID) keeps the large Default set. precompile resolves to v0.5.56
(MVS-correct: chains v1.3.18's own requirement; node has no evm/dex dep).

go mod verify: all modules verified; chains quorum params tests green.
2026-06-23 01:43:41 -07:00
zeekay 2ebaf260fa node(chains): receive-side epoch recency gate + bounded heights map + frame discriminator (HIGH-1 b / MEDIUM-3 / LOW-2)
Closes the receive-side gaps the b2 build-side stamping (a37f0e1075) left
open. The build-side epoch stamp max(currentH, parentH) is PROPOSER-ONLY;
a follower must independently re-gate every gossiped block. Three fixes,
one per gap, in the transport wrapper pchain_height_vm.go:

  HIGH-1 (predicate b — RECENCY, upper bound). ParseBlock now rejects a
  framed block whose stamped P-chain epoch H exceeds THIS node's live
  P-chain height by more than pChainHeightRecencySlack (256) — an
  absurd-future epoch a Byzantine proposer claims for a set the network
  has not reached. Rejected fail-closed (errPChainHeightNotRecent): the
  block is dropped, never tracked or voted. This is the UPPER half of the
  epoch bound; the engine-side monotone gate (consensus 7cabd6faa) is the
  LOWER half (>= parent's recorded epoch), so a tracked block's epoch is
  pinned to [parentEpoch, localH+slack]. Fail-soft when no P-chain view
  exists (nil state -> admit; the verifier still fails closed at
  set-resolution) — a defensive path, not a mode.

  MEDIUM-3 (heights-map DoS — WATERMARK PRUNE, not blind LRU). ParseBlock
  runs before the engine dedup/Verify, so a peer can stream distinct
  unverified blocks, each adding a permanent heights entry. Each entry now
  carries its value-chain height; Accept advances a finalized-height
  watermark and prunes every entry at/below it (a finalized block never
  needs a GetBlock epoch re-attach), so the map plateaus under steady
  finality. A hard cap (4096) backstops a sustained unverified flood,
  evicting HIGHEST-height-first so the near-tip pending band survives — a
  still-pending block's epoch is never evicted (a blind LRU could reset it
  to 0 = re-freeze).

  LOW-2 (frame magic DISCRIMINATOR). The 4-byte magic collides at 2^-32
  with the raw hash-prefix some VMs use as Bytes() (dexvm/dchain serialize
  parentID[0:32]||...). A magic match alone no longer means "framed": the
  inner re-parse of the framed payload must ALSO succeed. On re-parse
  failure ParseBlock falls back to a raw whole-block parse — removing the
  bootstrap stall a collision used to cause (mis-framed -> inner decode
  fails closed -> stall on that chain).

TDD pchain_height_hardening_test.go (all pass, CGO_ENABLED=0):
  far-future epoch REJECTED; honest within-slack skew (P-chain advance
  during a staking change) ACCEPTED; nil-state admits; magic-colliding raw
  parses RAW; genuine frame still parses; heights map plateaus at the
  watermark; a pending block is never evicted by the prune; a flood is
  capped preserving the near-tip band. The b2 build-side finality proofs
  still pass (no regression to delivered-epoch stamping).
2026-06-22 23:41:59 -07:00
zeekay a37f0e1075 node(chains): deliver the REAL P-chain epoch height to the chain engine (b2)
THE BUG. A K>1 quorum chain pins its weighted validator set to a P-chain epoch
height (set-root, 2/3-by-stake tally, per-voter pubkey resolution all read at that
one height). The engine reads that height off the VM block via pChainHeightOf,
asserting block.SignedBlock.PChainHeight() — but every plugin VM block (C-Chain
EVM, dexvm) exposes none, so pChainHeightOf returns 0 and the set resolves at
P-chain height 0: the GENESIS set. That is safe (non-empty, identical on every
node, <= current) and unbricks finality, but FREEZES the epoch at genesis: a
validator that JOINED post-genesis is absent from set@0 -> its vote is dropped and
its stake uncounted -> finality cannot track a DYNAMIC validator set, and a
departed genesis majority could collude.

THE FIX (Option b: rpcchainvm zap boundary, NOT a chain-creation-switch rewrite).
pChainHeightVM wraps the chain BlockBuilder so the block the engine sees carries
the proposer's live P-chain epoch height H = max(GetCurrentHeight, parentH),
WITHOUT changing the inner VM's block format, IDs, or ledger state:
  - BuildBlock stamps H and frames the gossiped bytes [magic|H|innerBytes];
  - ParseBlock splits the frame so every follower ADOPTS the identical H (never
    recomputes it from its own skewing P-chain view) -> determinism: every honest
    node derives the SAME epoch height from the SAME signed block, so the cert
    set-root they recompute matches and a post-genesis validator's vote+stake
    count. Raw (unframed) bytes parse with H=0 = the safe genesis fallback.
This is consensus-TRANSPORT framing, not a chain/ledger fork: inner bytes, block
IDs, and execution state are byte-identical -> no re-genesis, only a coordinated
node upgrade. Installed ONLY on K>1 chains (a K==1 chain has no cert/epoch).

Also wires the height-indexed P-Chain validators.State as the SINGLE source of
epoch truth for all four reads (verifier pubkey, stake, total stake, set-root),
all keyed on the block's P-chain height, and FAILS CLOSED if a K>1 chain is built
without a live height-indexed state (a silent permanent-stall wiring bug becomes a
loud refuse-to-start).

TDD (CGO_ENABLED=0, the canonical purego BLS path = ACTUAL production quorum
sources, not an ed25519 stand-in):
  - TestPChainHeightVM_DeliversRealHeight: pChainHeightOf(builtBlock)==H and
    ==H again after a ParseBlock round-trip of the gossiped bytes; a bare inner
    block reads 0 (pins why the wrapper is load-bearing).
  - TestPChainHeightVM_FinalizesAtGenesis: K>1 finalizes against set@0.
  - TestPChainHeightVM_FinalizesAfterStakingChange: validators that JOINED at
    epoch 7 cast the deciding 2/3 votes+stake and the block FINALIZES; the cert
    verifies stake-weighted at 7 and FAILS at 0; the production verifier rejects a
    joiner at height 0 but accepts it at 7. Proven to FAIL without the fix (stamp
    0 -> joiners dropped at set@0 -> VM.Accept never runs = permanent stall).
2026-06-22 22:21:55 -07:00
zeekay eda317ec0f node(chains): height-pin the quorum set-root + stake tally (MEDIUM-1)
MEDIUM-1 liveness regression: validatorSetRootSource.ValidatorSetRoot
ignored the height argument and hashed the Manager's CURRENT GetMap()
snapshot. During a validator-set change (P-chain / L1 staking), the
current map propagates ASYNC relative to a value-chain block-H vote, so
the signer and the assembler held different current maps -> different
set-roots -> the canonical SIGNED message differed -> signatures FAILED
verification -> votes dropped -> finality stalled at every staking change.
The epoch-binding (set-root in the signed message) made this bite.

FIX: read the HEIGHT-INDEXED set from validators.State.GetValidatorSet(
ctx, height, netID) — deterministic across nodes at a given value-chain
height, independent of async current-map skew. The engine already threads
the block height to ValidatorSetRoot(height) via the sole position builder
blockPositionLocked, so sign-side and verify-side read the SAME
height-pinned set.

The stake source (Weight/TotalStake) is pinned to the SAME height too, so
the tally is measured at the same epoch as the signed membership — a
validator whose vote is in a height-H cert contributes its height-H
weight, closing the second skew (a current-map weight read could drop a
legitimately-signed quorum). validatorSetAtHeight is the single shared
epoch read; hashValidatorSet is the single (byte-unchanged) set-root
encoding.

The vote verifier keeps the current-map pubkey lookup (a separate, milder,
self-healing axis — not MEDIUM-1; disclosed for review).

TDD:
  - TestValidatorSetRoot_CrossNodeAgreesDespiteSkew — TWO nodes with a
    set-change in flight (divergent current maps) compute the IDENTICAL
    set-root at height H (the missing cross-node test).
  - TestValidatorSetRoot_HeightSelectsEpoch — root is a deterministic
    function of height.
  - TestValidatorStakeSource_HeightPinned — tally read at the cert height.
  - TestHashValidatorSet_ByteStability — golden, wire format unchanged.
  - TestValidatorSetRoot_FailSoftIsUniform — Empty fallback is uniform.
2026-06-22 19:34:57 -07:00
zeekay ff034876d5 node(chains): epoch-bind quorum certs + honest stake source (MEDIUM)
Supply the node side of the consensus epoch-binding fix:

- validatorSetRootSource: a deterministic commitment (SHA-256 over the set
  sorted by NodeID, each as nodeID||light||len(pk)||pk) to the chain's active
  weighted validator set. Wired via NetworkConfig.ValidatorSetRoot so the engine
  stamps it into every vote/cert position — a cert is cryptographically pinned to
  the exact set it was certified under, closing cross-epoch cert laundering.
  Empty/absent set => ids.Empty (the unbound answer).

- validatorStakeSource: corrected the misleading "height is advisory" comment.
  The node Manager is single-epoch (no GetValidatorSet(height); that is on
  validators.State), so the source honestly returns CURRENT-epoch weights — which
  is exactly the live in-epoch finality answer (a cert is verified in the same
  epoch it is created). Cross-epoch soundness is enforced at the witness layer by
  the set-root binding above, NOT by guessing unavailable historical stake.

TDD: TestValidatorSetRootSource_DeterministicAndSetSensitive (determinism,
insertion-order independence, weight/membership sensitivity, network scoping,
empty/nil -> Empty) and TestValidatorStakeSource_CurrentEpochWeights.
2026-06-22 18:49:56 -07:00
zeekay 94f15375d0 node(chains): wire α-of-K quorum topology + BFT params (HIGH-4 / CRITICAL-2)
CRITICAL-2: selectConsensusParams picks a Byzantine-fault-tolerant set for every
sybil-protected (multi-node) net (Mainnet K=21 / Testnet K=11 / Default K=20) —
NEVER LocalParams (K=3/α=2, f=0, single-validator-forkable). Asserted with
ValidateForValueNetwork; chain creation FAILS CLOSED on a non-BFT set.

HIGH-4: networkGossiper implements QuorumGossiper (BroadcastVote/GossipCert over
app-gossip); blockHandler.Gossip demuxes a quorum envelope into the engine's
HandleIncomingVote/HandleIncomingCert (gated on ModeQuorumFinality). The engine
is wired with a BLS VoteVerifier (validator-set pubkey + bls.Verify), a BLS
VoteSigner (staking key), and a validator-stake StakeSource (HIGH-3) for K>1.

Adapters proven against the local consensus engine + real BLS + a real
validators.Manager (standalone module): verifier round-trip/rejections, a real
3-of-4 BLS cert verifies weighted, envelope round-trip, param selection never
K=3 for multi-node. (Full node module compile is blocked on pre-existing,
unrelated sibling go.sum staleness — kms@v1.11.3 / aml consensus@v1.25.17.)
2026-06-22 17:22:34 -07:00
zeekay 8a8820b62f node: bump EVM_VERSION v1.99.35 -> v1.99.37 (0x9999 ERC-20 Call-surface env fix)
evm v1.99.37 (precompile v0.5.57) wires the 0x9999 ERC-20 Call surface to the
DEX settlement precompile (2cf30e43d) and gates it to the 0x9999/0x9996 settle
family (9579f2e34). Before this a CALL into 0x9999's ERC-20 settle path saw a
nil PrecompileEnv and could not resolve the token-transfer Call seam. v0.5.57
adds the CALL-only DELEGATECALL guard (feeaab5a0). Deps converge to latest
patch within v1.x.x (threshold v1.9.9, crypto v1.19.21, database v1.20.3,
geth v1.17.12, vm v1.2.5, api v1.0.15). Money path (V4 swap ABI, marker
install, dated DexSettleActivationTime fork) byte-unchanged.

Pairs with the baked DEX_REF v1.5.15 (D-Chain committed-state read RPC) for
one coordinated image: native-fill matcher + read RPC + ERC-20 env fix.

Also: harden the lux-accel fetch to authenticate with the ghtok BuildKit
secret (luxcpp/accel is private -> lux-private/accel; unauthenticated 404)
and make it best-effort (matches the cevm/lpm contract) since the GPU lib is
linked only at CGO_ENABLED=1 and the canonical build is CGO_ENABLED=0 pure-Go.

Image: ghcr.io/luxfi/node:v1.30.40 (patch from v1.30.39).
2026-06-22 15:09:16 -07:00
zeekay 4c5314932b node: bump DEX_REF v1.5.14 -> v1.5.15 (D-Chain committed-state read RPC)
v1.5.15 adds the D-Chain read surface (clob_get_trades/orders/markets/book over
/ext/bc/D/dex/<method>, pkg/dchain/read.go): read-only JSON of the committed
trade log, resting book, and markets, served beside the write methods via
VM.CreateHandlers with zero consensus impact.

Needed to (1) VERIFY a native fill replicated identically across the validator
set — query clob_get_trades on every node and diff the trade rows + accepted
head root — and (2) feed markets-display, since native fills are D-Chain trade:
rows (not C-Chain 0x9999 DEXFills).

Plugin VM id unchanged (mDVT5EWMumBp3LCqvKwuyZQeY1VXr1jvjGNAt8nL4UFiXvqXr); the
change is additive read endpoints, no consensus/state-transition change.
2026-06-22 14:57:04 -07:00
zeekay e4d0db372f node: bump DEX_REF v1.5.13 -> v1.5.14 (prefixdb prefix-iteration fix via database v1.20.4)
dex v1.5.14 consumes luxfi/database v1.20.4, which fixes prefixdb
NewIteratorWithStartAndPrefix synthesizing a pre-prefix start for a nil-start
prefix scan -> ZERO rows over a prefixdb-wrapped chain DB. This stranded the
native D-Chain: rebuildBookFromDB iterated order:<pool> alongside other
sub-prefix rows, folded an EMPTY book, and every taker cross produced 0 fills
despite durably-committed asks. Only the dexvm slot is rebuilt this run (devnet
D-Chain scope); the same database fix likely benefits the other plugin VMs and
is a separate follow-on bump.
2026-06-22 14:12:47 -07:00
zeekay a382f08d49 node: bump DEX_REF v1.5.12 -> v1.5.13 (D-Chain consensus stall fix)
dex v1.5.13 indexes processing blocks so the plugin transport's ID-only Accept
(GetBlock(builtID)) resolves the just-built block. Without it the native
D-Chain's self-finalize Accept is a silent no-op, the clob submitTx waiter hangs,
and NO D-Chain blocks are produced — orders submitted to /ext/bc/D/dex/clob_*
never match. Required for native trading to actually produce fills.
2026-06-22 12:28:37 -07:00
zeekay e54f2ec60c node: bump DEX_REF v1.5.11 -> v1.5.12 (D-Chain restart fix)
dex v1.5.12 persists the head block so the native D-Chain VM survives a restart
once advanced past genesis — without it the first validator to restart fails VM
init ('get last accepted block: not found') and the D-Chain goes down. Required
for a 5-validator rolling restart of the native matcher.
2026-06-22 11:41:32 -07:00
zeekay 8fd7eccf7a node: bump DEX_REF v1.5.10 -> v1.5.11 (native D-Chain CLOB ingestion)
dex v1.5.11 wires CLOB order ingestion over the node HTTP router
(pkg/dchain VM.CreateHandlers -> /ext/bc/D/dex/<method>). An order POSTed to
/ext/bc/D/dex/clob_submit flows submitTx -> mempool -> consensus -> BuildBlock
-> Verify(match) -> Accept; the chain is the matcher, no venue, no keeper.

Plugin stage rebuilds (ARG change invalidates the dexvm slot cache); node
go.mod is unaffected (the plugin is built from dex's own module, -mod=mod).
2026-06-22 11:17:45 -07:00
zeekay d6f8f10f59 node: D-Chain (dexvm) plugin = native consensus matcher VM (dex/cmd/dchain)
The D-Chain runs as a PluginDir plugin loaded by luxd at the dexvm vmID
(mDVT5EWMumBp3LCqvKwuyZQeY1VXr1jvjGNAt8nL4UFiXvqXr). Until now that slot was
built from chains/dexvm/cmd/plugin — the STATELESS PROXY that relayed clob_*
frames over ZAP to a standalone dchain-venue (DexZapEndpoint). Matching did
NOT happen in luxd consensus; it happened in the external venue's single-node
sealer.

Build the dexvm slot from github.com/luxfi/dex@v1.5.10 ./cmd/dchain instead:
the NATIVE VM (pkg/dchain, block.ChainVM) that runs the lx.OrderBook matcher
INSIDE luxd consensus — BuildBlock drains the mempool, Block.Verify matches
against a versiondb overlay, Block.Accept commits. The trade IS the D-Chain
state transition, sequenced by luxd's multi-validator engine. No
DexZapEndpoint, no standalone venue in the trading path.

cmd/dchain wraps the VM in the same rpc.Serve plugin harness luxfi/evm uses
and is pure-Go (CGO=0); same GOFLAGS/go.sum-heal pattern as CHAINS_REF. The
other 10 chains VMs still build from chains. New ARG DEX_REF=v1.5.10; a FATAL
guard fails the build if the native D-Chain plugin is missing. Verified: a
fresh v1.5.10 clone builds ./cmd/dchain CGO=0 -mod=mod in Docker-like
isolation; native VM suite (TestLocalnetVenueE2E/TestFourPath/conservation)
passes.
2026-06-22 03:44:08 -07:00
Antje Worring eaefec7a62 deps: chains v1.3.16 -> v1.3.17 (consensus v1.25.19 Round1 fix) + hanzoai sumdb exclusion
Two CI-image-build blockers fixed: (1) chains v1.3.16 pinned consensus v1.25.18
which calls the old single-value corona Round1 -> quantumvm plugin compile break;
v1.3.17 bumps consensus to v1.25.19 (Round1 returns error). (2) hanzoai/* excluded
from sumdb/proxy (404 on hanzoai/vfs go.mod). All 11 VM plugins build; luxd 57MB.
2026-06-22 03:12:58 -07:00
Antje Worring 8437de9095 fix(docker): exclude hanzoai/* from sumdb+proxy (unblocks image build)
The CI Docker build failed reading hanzoai/vfs@v0.4.1's go.mod via the public
sum.golang.org (404 — it's a cross-org module not registered there), same class
as luxfi/*. Added github.com/hanzoai/* to GONOSUMCHECK/GONOSUMDB/GONOPROXY.
Verified: clean 'go mod download hanzoai/vfs@v0.4.1' 404s without the exclusion,
succeeds with it. Unblocks ghcr.io/luxfi/node image builds (v1.30.30/31 failed).
2026-06-22 03:04:10 -07:00
zeekay 45681207fa deps: consume chains v1.3.16 (dexvm dex.submitTx + dex.getSettlement RPC seam)
The dexvm plugin gains the C<->D keeper RPC seam: dex.submitTx (mempool entry for
ImportTx + settling RelayOrderTx) and dex.getSettlement (D->C proceeds coordinate
for Phase-B). Bumps go.mod luxfi/chains v1.3.15->v1.3.16 AND Dockerfile CHAINS_REF
v1.3.14->v1.3.16 so the bundled dexvm plugin is rebuilt with the new RPC. go mod
verify clean; dexvm plugin builds from the v1.3.16 tag (CI mode).
2026-06-22 02:47:26 -07:00
zeekay 8179f5a09e deps: consume chains v1.3.15 (dexvm) + precompile v0.5.56 + evm v1.99.36 (DEX swap-seam r2 release) 2026-06-21 22:49:22 -07:00
zeekay 13714be91d deps: converge threshold v1.9.9 + crypto v1.19.21 + database v1.20.3 + geth v1.17.12 + warp v1.19.5 + metric v1.5.9 + proto v1.3.5 (latest patch within v1.x.x) 2026-06-21 22:45:48 -07:00
Antje Worring a9eb2b0f43 deps(pq): consume finalized PQ crypto via consensus v1.25.19
Bumps consensus v1.25.18 -> v1.25.19 (the latest-crypto floor: pulsar v1.1.5,
magnetar v1.2.3, threshold v1.9.9, corona v0.7.9 + the 2-value Round1 API).
luxd builds (57MB), warp/PQ verifier tests green. node calls no Round1 directly
(stable crypto/threshold scheme registry).
2026-06-21 22:35:33 -07:00
zeekay 91372ea3b9 ci(build.yml): goreleaser must use cross-org PAT, not repo github.token
The routing + insteadOf fixes got goreleaser onto lux-build with git auth,
but it still failed: 'could not read Password ... exit 128' fetching
luxfi/{container,crypto,database,proto}. github.token is repo-scoped and
cannot read OTHER private luxfi repos. Switch to the same cross-org PAT
docker.yml uses (GH_TOKEN/UNIVERSE_PAT) so cross-repo module fetches
authenticate. node already carries UNIVERSE_PAT as a repo secret.
2026-06-21 15:11:17 -07:00
zeekay f567af1fe3 ci(build.yml): add private-module git auth before goreleaser
goreleaser shells out to go build, which fetches private luxfi modules
(container, crypto, proto, ...). setup-go-for-project sets GOPRIVATE but
only wires git auth when passed a github-token (build.yml passes none), so
the fetch hit 'could not read Username for https://github.com' and the job
failed even on lux-build. Add the same explicit 'Configure Git for private
modules' step ci.yml uses (git config url.insteadOf with github.token).

This is the real root cause of the 'Build on supported platforms' red-X;
the routing-to-lux-build commit was necessary but not sufficient.
2026-06-21 15:04:48 -07:00
zeekay 277f162fb5 ci: route goreleaser + release validate/create off ubuntu-latest to lux-build ARC
The v1.30.28 docker.yml run published node:v1.30.28 (sha-fbc6afd) fine on
the in-cluster lux-build ARC pool, but two sibling workflows red-X'd:

  - build.yml 'goreleaser' (runs on every push)
  - release.yml 'validate-version' (cascaded skips to every platform build)

Root cause: both pinned runs-on: ubuntu-latest. This org disables
GitHub-hosted runners (NO GITHUB BUILDERS) — ubuntu-latest jobs never get a
runner and fail at provisioning, not at compile. Source is sound:
GOWORK=off CGO_ENABLED=0 go build ./... is clean and the cross-layer
dexsettle_guard test passes (extras.DexSettleActivationTime ==
dex.DexSettleActivationTime == 1766704800 = 2025-12-25T23:20:00Z).

Route goreleaser, validate-version, and create-release to lux-build (the
org-scoped autoscalingrunnerset in lux-k8s that already builds the image).
GoReleaser cross-compiles every GOOS/GOARCH from one linux/amd64 host with
CGO disabled, so a single amd64 runner suffices.

(The per-platform binary reusables build-{ubuntu,linux,macos,win}-* still
pin classic [self-hosted,linux,amd64]/windows-2022 labels — a pre-existing
routing issue tracked separately; the node *image* path is unaffected.)
2026-06-21 14:58:25 -07:00
zeekay fbc6afd923 node: bump EVM_VERSION v1.99.34 -> v1.99.35 (native 0x9999 DEXFill/Initialize indexing) (v1.30.28)
evm v1.99.35 pins precompile v0.5.55, which emits the DEXFill + V4 Initialize
logs the explorer's DEX graph indexes — Dec-25-2025 activation-gated (same
dated fork as 0x9999 dispatch; no fork risk). EVM_VM_ID unchanged (money path
byte-identical). Plugin pulls precompile v0.5.55 transitively via the evm clone.
2026-06-21 14:29:19 -07:00
zeekay 689f9fe5b8 node: bump C-Chain EVM plugin to v1.99.34 — 0x9999 replay consensus-divergence fix (v1.30.27)
EVM_VERSION v1.99.33 -> v1.99.34 (precompile v0.5.53 -> v0.5.54). v1.99.34 fixes
the ONE HIGH from Red's review of the 0x9999 dated-fork activation that blocked
the production deploy: the EVM dispatch gate read a process-global timestamp
(last-writer-wins across concurrent goroutines), so on the relaunch path
(admin.importChain of a pre-fork RLP snapshot on a live, RPC-serving post-fork
node) a concurrent post-fork eth_call could make a pre-fork block see 0x9999
ENABLED and dispatch settlement during plain-account execution -> state
divergence. The gate now reads the overrider's own per-EVM (chainConfig,
timestamp). Also: SubBalance fallback fails closed (no silent native mint) and
the genesis-config builders skip AlwaysOn modules (0x9999 can't get a
timestamp-0 genesis config that bypasses the dated fork). Money path
byte-for-byte unchanged. This unblocks the production deploy.
2026-06-21 12:41:01 -07:00
zeekay 02e7e12034 Merge: node EVM_VERSION v1.99.33 — 0x9999 dated-fork activation (v1.30.26) 2026-06-21 12:13:01 -07:00
zeekay 8f5578feb3 node: EVM_VERSION v1.99.33 — 0x9999 dated-fork activation + 0x9010 removal (v1.30.26)
Bundle the C-Chain EVM plugin built from luxfi/evm v1.99.33 (precompile v0.5.53):
0x9999 DEX settlement now activates at a SINGLE canonical dated fork
(extras.DexSettleActivationTime = 1766704800, Dec 25 2025) instead of the v1.99.32
unconditional always-on. At the fork it BOTH enables dispatch AND installs the
EXTCODESIZE marker forward (never in historical genesis), so:
  - eth_getCode(0x9999) != 0x and typed Solidity IPoolManager(0x9999).swap(...)
    passes the contract-existence guard from the activation block onward;
  - pre-Dec-25 history replays byte-identically (the ~/work/lux/state RLP snapshot
    via admin.importChain) — a pre-fork value transfer to 0x9999 hits a PLAIN
    account, not the precompile, so canonical pre-activation state is preserved;
  - the C-Chain genesis hash is unchanged (no genesis-time marker).
0x9010 is REMOVED entirely (not a registered precompile, no dispatch, no forward);
0x9999 is the SOLE canonical DEX precompile. Runtime DChainID via the "D" alias and
the built-in DAO-treasury protocolFeeController are unchanged.

deps: indirect precompile v0.5.52 -> v0.5.53 (go.sum tidied: stale v0.5.52 hashes
dropped; v0.5.53 hash matches the evm/precompile repos byte-for-byte). node main
builds clean GOWORK=off; go mod verify OK; go mod tidy is a no-op on go.mod.
2026-06-21 12:13:01 -07:00
zeekay 9666219074 Merge: node EVM_VERSION v1.99.32 — 0x9999 always-on DEX settlement (v1.30.25) 2026-06-21 09:23:55 -07:00
zeekay 47755174d9 node: EVM_VERSION v1.99.32 — 0x9999 always-on DEX settlement (v1.30.25)
Bump the bundled C-Chain EVM plugin to v1.99.32 (precompile v0.5.52): the native
0x9999 DEX settlement money path is now ALWAYS-ON. It is active on the C-Chain from
genesis with ZERO per-net config — no dexSettleConfig genesis/upgrade entry anywhere.
Booting luxd = 0x9999 live on the C-Chain, on every Lux network.

Activation is dispatch-only (no genesis state write → genesis hash unchanged → no
fork of any existing network). The D-Chain (dexvm) peer the atomic seam routes to is
resolved at RUNTIME from the consensus-context "D" alias the node already registers in
initChainAliases (chains/manager.go BCLookup), via contract.AtomicState.DChainID().
The protocolFeeController is the built-in DAO treasury. Nothing per-net to configure.

Dockerfile: ARG EVM_VERSION v1.99.31 -> v1.99.32 (the plugin is built from this tag).
go.mod/go.sum: indirect luxfi/precompile v0.5.51 -> v0.5.52 (keeps node's transitive
graph in lockstep with the plugin; node main builds clean GOWORK=off, go mod verify OK).
2026-06-21 09:23:48 -07:00
zeekay f0576dcce7 Dockerfile: bundle the native-atomic DEX plugins (evm v1.99.31 + chains v1.3.14)
The bundled C-Chain EVM + 11 VM plugins were pinned at stale defaults
(EVM_VERSION=v0.19.4, CHAINS_REF=v1.3.11) decoupled from node's go.mod, so every
node image shipped the OLD DEX (no native 0x9999) even after the cascade bumped
node->chains v1.3.14. Pin both to the native-atomic seam + document the coupling.
No node code change vs v1.30.23.
2026-06-21 07:51:11 -07:00
zeekay 3b94a71522 deps: bump chains v1.3.14 (DEX native-atomic-seam: geth v1.17.12 + precompile v0.5.51 promoted) for v1.30.23 2026-06-21 07:22:37 -07:00
zeekay aa632eff8e deps: chains v1.3.13 (evm v1.99.30 + precompile v0.5.50, all clean go.sum)
Bump to the clean-go.sum re-cascade so node, plugins, and all upstream modules
build identically in clean CI. No node code change vs v1.30.21. go mod verify
clean; full build green.
2026-06-20 22:14:23 -07:00
zeekay ed531b8e1a go.sum: correct consensus/geth hashes (clean-cache fetch; v1.30.20's were stale)
v1.30.20's go.sum carried stale consensus@v1.25.18 + geth@v1.17.11 hashes that a
locally-polluted module cache (modified extracted dirs) had produced — the in-CI
docker `go mod download` rejected them (checksum mismatch). Purged the modcache,
re-fetched fresh, and `go mod verify` is clean ("all modules verified"). No
go.mod/dep change vs v1.30.20; the node binary is identical. This is the first
buildable tag of this node code.
2026-06-20 21:37:31 -07:00
zeekay 2e568e3741 ci(docker): build node image on the in-cluster lux-build ARC pool
The evo classic self-hosted runner is offline and GitHub-hosted runners are
billing-blocked (and disallowed by the NO-GITHUB-BUILDERS policy). Point the
image build at the lux-build autoscalingrunnerset (lux-k8s, amd64 DOKS, DinD)
— it serves the whole luxfi org and matches on the scale-set name. No node
binary/dep change vs v1.30.19; this is a build-infra patch.
2026-06-20 21:17:14 -07:00
zeekay fdf86622d6 deps: chains v1.3.12 -> hardened 0x9999 DEX receipt-settlement (precompile v0.5.49, evm v1.99.29)
Bump github.com/luxfi/chains v1.3.11 -> v1.3.12, pulling evm v1.99.29 (DexZap
live-backend wiring removed) and precompile v0.5.49 (complete, red-swarm-hardened
0x9999 V4 receipt-settlement surface) into the C-Chain. The C-Chain DEX precompile
now settles BLS-certified D-Chain fill receipts inline (deterministic, fork-safe);
the dexvm remains the D-Chain matcher.

Regenerate go.sum: the 2026-06-19 consensus geth-pin re-tag cascade left several
luxfi module hashes (consensus v1.25.18, geth v1.17.11, ...) stale. Re-recorded
from current tags (public deps verified via sumdb, luxfi via GOPRIVATE direct).
consensus v1.25.18 and geth v1.17.11 pins unchanged.
2026-06-20 15:05:31 -07:00
zeekay a86d80dd5e Dockerfile: bump GO_VERSION 1.26.1 -> 1.26.4 (match go.mod; EVM plugin floors at 1.26.4 via luxfi/upgrade@v1.0.1) 2026-06-20 01:25:08 -07:00
zeekay 16b297a0fa genesis/builder: A/G/K as deterministic genesis chains -> v1.30.18
Append aivm(A)/graphvm(G)/keyvm(K) to genesis chainEntries after Z so the
existing X->C->D->B->T->Q->Z blockchain IDs are preserved; A/G/K take fresh
tail IDs. They carry a/g/kChainGenesis blobs and are deterministic genesis
chains per the no-CreateChainTx model (prior code deferred them to
post-genesis CreateChainTx). I/O/R have no blob and stay plugin-loaded.
2026-06-20 00:53:03 -07:00
zeekay 34e738e65c deps+build: chains v1.3.11 bridge BLS fix + plugin-build heal → node v1.30.17
ROOT FIX (B-Chain): chains v1.3.11 bridgevm/mpc.go blank-imports
github.com/luxfi/crypto/threshold/bls so the bls init() registers the BLS
threshold scheme, fixing the B-Chain VM init regression
(threshold.GetScheme(SchemeBLS): 'scheme not registered') that aborted the
v1.30.16 devnet roll. Verified bls init/RegisterScheme symbols + itab linked
into the bridgevm plugin binary.

go.mod: pin chains v1.3.10 -> v1.3.11 (no other version moves; MVS keeps
crypto v1.19.21, geth v1.17.11, consensus v1.25.18).

go.sum: re-record current content hashes for re-published luxfi modules
(geth/precompile/etc) at EXISTING pinned versions — integrity repair, no
version bump. Fixes clean-room 'go mod download' checksum drift.

Dockerfile plugin builds (embedded plugins ARE authoritative — devnet
startup does cp /luxd/build/plugins/* then --plugin-dir):
  - EVM/C-Chain: EVM_VERSION v0.19.3 -> v0.19.4; heal dead upgrade
    pseudo-version to released v1.0.1; recursive go.sum strip.
  - chains VMs: CHAINS_REF default -> v1.3.11; recursive go.sum strip;
    bridgevm build made FATAL (was best-effort) + presence assertion so the
    image cannot ship without a working B-Chain plugin.
All 11 chains VM plugins + evm plugin verified to build (golang:1.26.4,
linux/amd64). lux-devnet only; testnet/mainnet stay v1.30.3.
2026-06-19 21:03:55 -07:00
zeekay a2f28eb92e deps: pin geth v1.17.11 + consensus v1.25.18 + chains v1.3.10 (real semver); MVS-propagate transitive bumps (evm v1.99.28, precompile v0.5.47) 2026-06-19 14:45:57 -07:00
zeekay c6314ed10e Merge node/plugin-nft-gate: all-VMs-as-plugins + X-NFT-gated activation 2026-06-19 14:28:26 -07:00
zeekay d8677aa48c node: all-VMs-as-plugins core/optional split + X-NFT-gated activation
Explicit CoreVMs{P,X,Q,Z} in-process + OptionalVMs plugin-only (no build tags; vms_dchain/nodchain removed). Two-layer anti-shadow (init panic + runtime ListFactories guard). NFT-gated activation at createChain (manager_authz), fail-closed; dex-validator flag gates D-Chain tracking; xvm.GetUTXOs locked.
2026-06-19 11:34:41 -07:00
zeekay 5ca04a3f3d deps: bump vm v1.2.0→v1.2.4 (rpc/zap zapdb harness)
Pick up the rpc.Serve plugin harness now opening chainstate via zapdb.New
(single-backend rule) instead of badgerdb.New — same on-disk format, drop-in.
Transitive api v1.0.14→v1.0.15 (within v1, no major bump). chains stays
v1.3.9. Verified: public 'go build ./node/' green; '-tags dchain' green;
public-purity 'go list -deps ./node/' has ZERO chains/dexvm|luxfi/dex; the
-tags dchain build DOES link the dexvm proxy (orthogonality holds).
2026-06-17 23:58:23 -07:00
zeekay d1c9bcd868 node: bump luxfi/chains v1.3.8 → v1.3.9 (atomic custody rail) 2026-06-17 21:11:22 -07:00
zeekay c596c79212 node: bump luxfi/zapdb v1.10.0 → v1.10.1 (cloud-free, cgo blocker resolved on v1 lineage) 2026-06-17 19:30:22 -07:00
zeekay e962558156 node: pin chains v1.3.6 -> v1.3.8 (the released proxy + #9 carried-fills consensus fix)
Production node -tags dchain now links the released chains v1.3.8 (stateless atomic ZAP proxy +
the #9 consensus-fork fix), not the stale v1.3.6. Public build stays pure (zero dex/dchain/gpu
deps); dchain build links the 6 chains/dexvm proxy pkgs. Transitive: oracle v0.1.1->v1.0.0,
relay ->v1.0.0 (chains v1.3.8 requirements; first-stable, within v1).
2026-06-17 17:52:10 -07:00
zeekay 9ee97461cf Merge origin/main into xvm-merkle-activation integration
# Conflicts:
#	go.mod
2026-06-17 17:14:12 -07:00
zeekay 17756e27b5 Merge xvm-merkle-activation: dchain build-tag split (public build excludes D-Chain) + merkle-activation 2026-06-17 17:12:59 -07:00
zeekay 8284625fc6 node: dchain build-tag split -- public build excludes D-Chain + private deps
vms.go drops the dexvm import+entry, calls appendDChainVM; vms_dchain.go (//go:build dchain)
links it, vms_nodchain.go (//go:build !dchain) no-ops; vms/dexvm alias gated //go:build
dchain + doc_nodchain.go placeholder. Genesis-gate (constants.DexVMID, no pkg dep) retained.
VERIFIED: public 'go list -deps ./node/' has ZERO chains/dexvm|luxfi/dex|gpu-kernels|luxcpp;
-tags dchain brings them in. Both builds green.

Consensus change -- held for human review before merge/push (bundles with merkle-activation).
2026-06-17 13:31:58 -07:00
Antje WorringandClaude Opus 4.8 7a4a99db49 docs: tidy LLM.md indexes; CLAUDE.md -> LLM.md symlink convention
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 09:58:24 -07:00
zeekay 1ea66944af xvm: remove execution_root activation gate — values not gates (active, not gated)
The prior seam added a bespoke MerkleRootActivationHeight gate (default
MerkleRootNeverActivate=MaxUint64) that violated upgrade/upgrade.go's stated
philosophy ('activate-all-implicitly; the fields encode values, not gates').
Removed it entirely (grep-clean): the builder ALWAYS stamps the real xvm
execution_root, the executor ALWAYS recomputes+verifies it, an empty root is
now rejected. Root computation byte-IDENTICAL (exec_root=4f144ef7…) — only the
gating is gone. Net -92 lines. Tests converted to always-active reality
(empty-root-rejected is the new gate test); ./vms/xvm/... + ./upgrade/... green
uncached + -race. asset_root stays keccak256("") (UTXO-only executor; assets
bound via each UTXO's AssetID).
2026-06-16 12:59:21 -07:00
zeekay 51a460caff xvm: activation-readiness — real UTXO leaf projection + state iterator (gate OFF)
Closes the nil-projection seam in the activation-gated merkleRoot wiring (845d990d):
- state/iterator.go: UTXOs(start,limit) on ReadOnlyChain (state + diff) — ascending
  canonical order + ordered overlay merge.
- block/executor: real post-block UTXO leaf projection; canonical OwnerRoot =
  keccak256(threshold_le || key_count_le || sorted_keys); BlockExecutionRoot now
  returns (ids.ID, error) so a projection/enumeration failure surfaces, not a wrong root.
- ownerroot.go canonical derivation; 16 new tests, -race clean; full ./vms/xvm/... green.

Gate stays OFF (MerkleRootActivationHeight = math.MaxUint64) — NO live consensus change.
Asset family intentionally empty (asset_root=keccak256("")); xvm executor is UTXO-only,
so a faithful asset-arena projection needs an asset-arena state model first — flagged
for human ratification. Consensus-main merge is a human decision.
2026-06-16 12:04:19 -07:00
Hanzo DevandGitHub 27ea0bb9ff Merge pull request #141 from luxfi/feat/database-v1.20.3-native-replication
deps: bump luxfi/database -> v1.20.3 (native ZAP replication)
2026-06-15 19:42:46 -07:00
hanzo-devandClaude Opus 4.8 76a3f2c3f9 deps: bump luxfi/database v1.19.2 -> v1.20.3 (native ZAP replication)
Brings native ZAP replication into the luxd binary: CDC change-feed incrementals
(no keyspace scan), physical SST-copy snapshots, post-quantum (ML-KEM-768) at-rest
encryption, per-DB streams, restore-on-boot, and the backer/restorer split. The
node reads it all from REPLICATE_* env (operator-driven) — every chain's ZapDB
backs up continuously and a fresh node launches from the latest snapshot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 02:42:28 +00:00
zeekay 845d990d5c xvm: parallel intra-block execution_root + activation-gated merkleRoot wiring (DEFAULT OFF)
Core goal — build the xvm block state root from independently-built
subtrees in parallel, and wire it into the block path behind an
off-by-default activation gate (zero live-consensus change).

Parallel build (vms/xvm/block/executor/executionroot.go): the three
family folds (utxo/asset/tx) run concurrently; each folds via a
GOMAXPROCS worker-pool RFC-6962 level reduction composed from
crypto/merkle's exported LeafHash/NodeHash (no duplicated keccak/tag —
DRY). Byte-IDENTICAL to the serial xvmroot.ExecutionRoot: canonical leaf
order + count-determined combine shape are unchanged; only the per-index
loops parallelize (disjoint-slot writes). Proven by 200-random +
sizes-0..4097 byte-identity tests, race-clean (go test -race).
Benchmark (M1 Max, 65536 UTXOs): execution_root 87.4ms -> 19.1ms (4.58x).

Gated wiring: upgrade.Config.MerkleRootActivationHeight defaults to
MerkleRootNeverActivate (math.MaxUint64) on Mainnet/Testnet/Default AND
xvm.DefaultConfig (safety-critical — VM.initialize discards init.Upgrade,
so without the default the zero value would activate from genesis).
Below activation: builder leaves Root=ids.Empty + executor keeps the
verbatim reject-if-non-empty rule (byte-for-byte current behavior, tested).
At/above: builder stamps BlockExecutionRoot; executor recomputes + verifies
(rejects mismatch). nil-safe (nil config -> OFF).

HONEST SEAM — NOT ACTIVATION-READY: postBlockUTXOLeaves/postBlockAssetLeaves
return nil. A real projection needs (1) a committed mapping from the
executor codec lux.UTXO/asset types to the GPU-snapshot leaf fields
(OwnerRoot/AmountHi/Threshold/TotalSupply/...), and (2) a full-occupied-set
state enumeration API (only GetUTXO/per-address UTXOIDs exist today). Both
are consensus-semantics decisions deliberately NOT auto-invented (would be
unverified non-parity slop). So today the root commits to parent + EMPTY
utxo/asset roots + the (correct) tx family + height. Consensus-inert
because the gate is OFF; builder and verifier call the identical projection
so stamped==verified always. A human must ratify the projection + wire a
state iterator + GPU snapshot producer before any activation height is set.

Additive constructor NewStandardBlockWithRoot (NewStandardBlock = its
empty-root case). xvmroot gained 3 byte-transparent exported leaf-digest
accessors (KAT 4f144ef7 unchanged) so the parallel fold reuses the one
canonical preimage. CGO_ENABLED=0 clean; go test ./vms/xvm/... ./upgrade/
PASS. Local commit; not pushed (consensus repo — awaiting human review of
the activation seam).
2026-06-15 18:07:50 -07:00
zeekay ea34ffae2f vms/bridgevm/state/bridgevmroot: native-Go bridgevm state-root mirror + GPU parity KAT
Symmetric with vms/xvm/state/xvmroot — the native-Go authority for the
bridgevm_transition root. Computes all 5 family sub-roots (signer_set /
liquidity / inbox / outbox / daily_limit) via luxfi/crypto/merkle
(RFC-6962 tagged tree) over the post-transition leaf field values, plus
the §6 compose (parent ‖ 5 roots ‖ epoch ‖ bond_lo ‖ bond_hi ‖ active).
Full 128-bit bond aggregate (matches the GPU MED-1 fix — bond_hi never
dropped). Keccak via luxfi/crypto/hash NewLegacyKeccak256 (NOT sha3.Sum256).

Byte-for-byte parity with all 7 GPU backends (e303e3c): mixed
signer_root=c9812fee… state_root=6c973a55… bond_hi=0; dense-signer
N=5/9/17 bond_hi=29/71/203. 9 tests PASS, CGO_ENABLED=0 clean. Pure
computation + tests; not wired into consensus (the gated wiring is the
separate xvm/bridgevm builder+executor phase).
2026-06-15 17:53:39 -07:00
zeekay 9fe0ad265a xvm/state/xvmroot: native-Go execution_root mirror + Go==GPU parity KAT
Pure-computation Go mirror of the GPU xvm_root_update tree fold (utxo/
asset/tx Merkle sub-roots via luxfi/crypto/merkle + un-tagged keccak
compose). Byte-for-byte parity with all 7 GPU backends, anchored on the
shared KAT fixture (exec_root 4f144ef7, i%3 tx-status pattern matching
test_xvm_roots_kat.cpp). Bumps luxfi/crypto v1.19.17->v1.19.21 (publishes
the merkle + keccak packages); parity test passes in clean module mode
(GOWORK=off). NOT wired into block validity yet — computation+test only;
the activation-gated executor wiring is the next phase.
2026-06-15 16:43:03 -07:00
zeekay c172000ae2 health: atomic buffered GET reply; tolerate invalid UTF-8 in check details
jsonv2 MarshalWrite streams straight to the ResponseWriter and rejects
invalid UTF-8 mid-stream. A check whose Details embeds raw chain-ID
bytes tore the reply: clients got truncated JSON with a Content-Length
matching the truncation. Buffer the marshal so the reply is atomic,
allow invalid UTF-8 as replacement runes, and emit an explicit
encode-failure body instead of a torn one.
2026-06-10 21:53:44 -07:00
zeekay c0928d3c08 ci: route to canonical native arcd labels [self-hosted, linux, <arch>]
Replaces non-canonical scale-set / org-prefixed labels with the
existing labels every arcd host registers with. Matches evo for
amd64 and spark for arm64. No new labels added.
2026-06-10 20:23:53 -07:00
zeekay 7675a09341 wallet+deps: drop legacy UTXO codec path; bump dep semvers
wallet/network/primary/api.go:

- Drop the parseUTXOAnyWire dispatcher and route AddAllUTXOs straight to
  lux.ParseUTXO (ZAP wire). Wire format has one representation; the
  wallet has one decoder.
- Drop the now-unused ptxs and luxwire imports.

go.mod / go.sum:

- Replace orphan pseudo-versions with the published tags they tracked.
    luxfi/constants v1.5.8-0... → v1.5.8
    luxfi/oracle    v0.1.1-0... → v0.1.1
    luxfi/upgrade   v1.0.1-0... → v1.0.1
    luxfi/sdk       v1.17.6     → v1.17.9
    luxfi/kms       v1.11.3     → v1.11.4
2026-06-10 19:28:41 -07:00
Hanzo AI 24f4cdf506 chore: update 2026-06-10 13:34:04 -07:00
Hanzo AI c0ffea88c0 txs+wallet: deprecate AddChainValidatorTx under LP-018 sovereign-L1
Validators validate networks; chains live on networks. AddValidatorTx
is the universal add-validator-to-a-network tx whether the target
network is Lux primary (1/2/3/1337) or any downstream sovereign L1's
own primary at its chosen networkID. AddChainValidatorTx is legacy
and kept for one release cycle of wire/codec compat with pre-LP-018
binaries.

- vms/platformvm/txs/add_chain_validator_tx.go: file-header + struct
  Deprecated: notice pointing to AddValidatorTx.
- vms/platformvm/txs/chain_validator.go: ChainValidator descriptor
  marked Deprecated.
- vms/platformvm/txs/add_validator_test.go: new
  TestAddValidatorTxSyntacticVerify_ArbitraryPrimaryNetworkIDs covers
  Lux primaries (1/2/3/1337) plus four synthetic IDs across the uint32
  range, asserting the tx body has no per-chain field and accepts any
  valid primary networkID.
- wallet/chain/p/wallet/wallet.go + with_options.go: Deprecated on
  IssueAddChainValidatorTx; IssueRemoveChainValidatorTx doc reworded
  to network-centric language.
- wallet/chain/p/builder.go + builder/builder.go: Deprecated on
  NewAddChainValidatorTx interfaces.
- wallet/network/primary/examples/{add-permissioned-chain-validator,
  bootstrap-hanzo, deploy-chains}/main.go: file-header notes that
  these examples exercise the legacy path; new code uses AddValidatorTx.
- node/validator_manager.go: 4 log strings + 1 comment block rewritten
  to talk about network validator (not chain validator).
- vms/platformvm/health.go: error string 'current chain validator of'
  → 'current network validator on'.
2026-06-08 16:02:58 -07:00
Antje WorringandClaude Opus 4.8 4eed7a691a ci: target native arcd runners (evo/spark)
Replace retired self-hosted labels with native host arcd daemons:
evo (linux/amd64), spark (linux/arm64).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 15:59:13 -07:00
Antje Worring 9699b6c706 chore: commit AGENTS.md + CLAUDE.md for AI devs (un-gitignore) 2026-06-07 14:31:14 -07:00
Hanzo AI ed3a7ddcc8 cleanup: remove AI-slop summary / status / plan / report files
Reverts violations of the durable rule that current-state docs belong
in LLM.md and history belongs in git log. Removed files were session
handoffs, agent-style "complete success" / "1000%" reports, dated
audit dumps, and stub NOTES.
2026-06-07 13:36:23 -07:00
Darkhorse7starsandGitHub ccf0f65fc9 config: blank *-content flag falls through to *-file in pemBytesOrFile (#139)
pemBytesOrFile short-circuited when a *-content flag was set but empty
(v.IsSet true, value ""), returning empty bytes instead of consulting
the corresponding *-file path.

For strict-PQ staking keys (--staking-mldsa-key-file-content /
--staking-mldsa-key-file) this silently degraded a validator to a
classical ECDSA NodeID: StakingMLDSAPub stayed empty, IsStrictPQ() was
false, and the node booted with no error. The genesis validator set then
never matched the live NodeIDs and the P-chain produced 0 blocks -- the
strict-PQ activation outage (the #48 failure class).

Treat a set-but-empty content flag identically to an absent one and fall
through to the *-file path. The same helper backs the ML-KEM handshake
keys, so both paths are fixed.

Restore the regression guard
TestLoadStakingMLDSA_EmptyContentFallsThroughToPath (fails on main,
passes here) alongside the other loadStakingMLDSA cases.

Verified: CGO_ENABLED=0 go test ./config/
2026-06-07 11:57:11 -07:00
Darkhorse7starsandGitHub 00726eb169 network,node: complete strict-PQ activation (schemeGate + pre-dedup handshake + classical-compat) (#137)
main carries the strict-PQ peer-identity fix (NewLocalIdentityFromStakingKey +
adoptVerifiedPQIdentity) but is missing three further layers that are each
required for strict-PQ consensus and chain creation to actually work. All three
are proven on a live devnet running the equivalent fix (node v1.10.18-strictpq):
a full ML-DSA validator mesh forms and the EVM/DEX/FHE chains are created on the
sovereign L1.

1. schemeGate nil-gate (network/network.go)
   Under strict-PQ the TLS layer is transport-only: peer leaf certs are
   ephemeral ECDSA, which schemeFromCert classifies as classical, so the
   SchemeGate refuses every peer at the upgrade. Pass a nil gate to the TLS
   upgraders when PQHandshakeConfig is active; identity is enforced by the PQ
   handshake instead. Non-PQ paths keep the real gate.

2. pre-dedup PQ handshake (network/network.go + network/peer/peer.go)
   The PQ handshake ran inside peer.Start, under peersLock, with init/responder
   roles fixed by dial direction. On simultaneous mutual dials every node acts
   as a lone initiator (keeps its outbound, drops the peer's inbound at the
   connecting-dedup) and deadlocks -> 0 peers. Run the handshake per-conn in
   network.upgrade, before the dedup and without the lock, so each TCP conn
   completes independently and the dedup keys on the resulting stable ML-DSA
   NodeID. RunPQHandshakeConn shares main's binding via a new
   verifyPQIdentityBinding helper, so the pre-dedup and in-Start paths enforce
   byte-identical identity semantics.

3. classical-compat allow-list (node/node.go)
   The ClassicalCompatRegistry was nil, so strict-PQ refused every classical
   secp256k1 P-chain credential and the bootstrap control key could not issue
   CreateNetwork/CreateChainTx. Seed it (strict-PQ only) from the genesis
   P-chain allocation owners plus ids.ShortEmpty (the mempool's current
   originator). This unblocks creating the EVM/DEX/FHE chains.

Builds clean with CGO_ENABLED=0; network/peer, network, vms/txs/auth and
platformvm genesis/mempool tests pass. main's identity fix is unchanged.

Supersedes #136 (which carried these layers on a branch that had diverged 331
commits behind main). Follow-up, tracked separately: config.pemBytesOrFile
short-circuits on a blank *-content flag and silently degrades a strict-PQ
validator to an ECDSA NodeID.
2026-06-07 11:02:49 -07:00
Hanzo AI e037ef48fb version: bump default to v1.30.6 — final json/v2 + ZAP-internal ship
Tracks v1.30.5 (strict-PQ peer identity bind) + the json/v2 boundary
migration + 8-site internal-JSON to ZAP-native rip from this session.

Default version: 1.30.4 → 1.30.6 (v1.30.5 already exists as a CI auto-bump
on the strict-PQ peer fix).

Includes:
  - xvm/txs/doc.go (canonical //go:generate zapgen directive
    pointing at proto/schemas/xvm/txs.zap)
  - compatibility.json + v1.30.5 + v1.30.6 under RPCChainVMProtocol 42
2026-06-07 01:39:20 -07:00
Hanzo AI caefd0de02 staking/kms: correct doc — points at Lux KMS not Hanzo KMS
The HTTP KMS service this client talks to is the Lux blockchain-infra
KMS at ~/work/lux/kms (github.com/luxfi/kms), not Hanzo KMS. Hanzo KMS
(~/work/hanzo/kms) serves Hanzo apps and is a distinct service.

Doc-only fix — no behavior change.
2026-06-07 01:39:20 -07:00
Antje WorringandClaude Opus 4.8 73def6a3ac fix(ci): use explicit if-guards for GOOS/GOARCH re-export in run_task.sh
Harden the cross-compile target re-export against `set -e`: the prior
`[ -n "$x" ] && export ...` form returns non-zero when the var is empty
(the common native-build case). Empirically bash exempts it from
errexit, but the explicit `if` form is unambiguous across shells. Both
native (Mach-O arm64) and cross (ELF linux/amd64) builds verified via
./scripts/run_task.sh build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 00:52:43 -07:00
Antje WorringandClaude Opus 4.8 89d899bd56 ci: remove stale zap-audit workflow (zap_native moved to luxfi/proto)
The zap_native package was relocated out of this repo to
github.com/luxfi/proto/zap_native (commit 2c2c9188f "relocate zap_native
import paths to proto/zap_native"; node now imports it as a dependency,
pinned luxfi/proto v1.3.4). zap-audit.yml still grepped/cd'd into
vms/platformvm/txs/zap_native/, a directory that no longer exists here —
so the workflow could never run its gates against real code, and a YAML
block-scalar bug (a column-1 line inside `run: |`) made it fail to parse
at 0s on every push.

The four invariants it enforced (AddressList.At non-production,
ChainsList/ValidatorsList MustVerify, Owner-bearing SyntacticVerify) are
preserved at the source: luxfi/proto/zap_native/audit_test.go contains
the Go-test mirrors (TestAuditGate_*) that run in luxfi/proto's CI, next
to the code they guard. The audit belongs in the repo that owns the code,
not here pointing at a phantom path. Not a weakened gate — a dead
workflow removed; coverage unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 00:39:59 -07:00
Antje WorringandClaude Opus 4.8 ba864e1d5e deps: bump luxfi/genesis v1.13.8 -> v1.13.14 (Windows embed.FS fix)
luxfi/genesis@v1.13.8 read its embedded genesis shards with
embeddedGenesis.ReadFile(filepath.Join(network, "<shard>.json")). On
Windows filepath.Join uses backslashes, but embed.FS paths are always
forward-slash; every read returned fs.ErrNotExist, so GetConfig fell
back to an empty filesystem config: zero allocations, zero stakers,
empty XChainGenesis.

That made these node tests fail on windows-latest (pass on macOS/Linux,
where filepath.Join already yields "/"):
  - config.TestResolveUTXOAssetID_FromSovereignGenesis
  - genesis/builder.TestUTXOAssetIDFromGenesisBytes_Sovereign
  - genesis/builder.TestGetConfigAllocations (all 4 networks: 0 allocs)

v1.13.14 switches the embed.FS reads to path.Join (forward-slash, all
platforms). No behavior change on linux/darwin. Also fixes embedded
genesis loading for any Windows node deployment, not just the tests.
(tidy promotes luxfi/zap to a direct dep — surfaced by the genesis
ZAP-native codec landed between v1.13.8 and v1.13.14.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 00:39:47 -07:00
Antje WorringandClaude Opus 4.8 b16527eb8c fix(ci): build task launcher for host in run_task.sh (cross-compile)
build-macos-release cross-compiles on a Linux runner with
`CGO_ENABLED=0 GOOS=darwin GOARCH=<arch> ./scripts/run_task.sh build`.
run_task.sh bootstrapped the task runner via `go run task@vX`, which
inherited GOOS/GOARCH and cross-compiled the *task launcher itself* for
darwin — then failed to exec it on the Linux host:

    fork/exec /tmp/go-build.../task: exec format error

(reproduced locally on darwin/arm64 by cross-compiling task to linux/amd64.)

task is a build orchestrator that must run on the host. Build it for the
host (GOOS/GOARCH cleared via `env -u`, installed to GOPATH/bin with
`go install`), then re-export the caller's cross-compile target so the
downstream `go build` inside scripts/build.sh still produces the
requested darwin artifact. Verified: a linux-host -> darwin and a
darwin-host -> linux cross-compile both succeed and emit the correct
target-arch binary.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 00:39:37 -07:00
Antje WorringandClaude Opus 4.8 908c358611 refactor: complete XAssetID -> UTXOAssetID rename (functions, tests)
The asset-id var was already unified to UTXOAssetID; this finishes the
job by renaming resolveXAssetID -> resolveUTXOAssetID and
XAssetIDFromGenesisBytes -> UTXOAssetIDFromGenesisBytes (+ their tests).
X-Chain references (XChainID, XChainGenesis) are unchanged. UTXOAssetID
is the canonical name.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 23:52:19 -07:00
Antje WorringandClaude Opus 4.8 ef446d31ad refactor(platformvm): remove dead LUXAssetID fallback + unify asset-id naming to UTXOAssetID
The fallback if/else had identical branches (dead code); collapse to a single assignment. Rename stale LUXAssetID comment + utxosByLUXAssetID local var to the canonical UTXOAssetID.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 23:26:24 -07:00
Hanzo AI ce2f9f2ae4 staking/kms: document json/v2 boundary, keep external HTTP contract
The KMS client speaks JSON over HTTP — that is the external service's
public contract, not an internal data path. Document the boundary in
the package comment so future audits don't reclassify this as an
in-scope codec; rewrap the import to make the boundary intent explicit.

Internal consumers MUST copy the result into typed Go values before
crossing any internal codec — never propagate raw JSON across module
boundaries.
2026-06-06 23:14:30 -07:00
Hanzo AI fde0770401 utils/ips/dynamic_ip_port: drop unused json/v2 marshaler
The private dynamicIPPort type carried a MarshalJSON method using
json/v2. No consumer of DynamicIPPort in the node module relied on the
JSON path — the public IPPort/IPDesc types in ip_port.go retain their
JSON methods for the external node-info HTTP API. Drop the unused
internal marshaler; one path for IP serialization, on the boundary
that owns the wire shape.
2026-06-06 23:14:29 -07:00
Hanzo AI f553a9cd33 utils/bimap: drop json/v2 marshaler, keep type generic
BiMap is generic over comparable K,V — bolting on a json/v2
Marshaler/Unmarshaler bound the type to text I/O it has no business
caring about. Removed the marshaler interface and the dependent tests;
no consumer of BiMap in the node module relied on JSON.

Callers that need to persist a bimap should encode a typed snapshot at
the call site (ZAP for internal, JSON for external HTTP boundaries) —
one and only one way, at the boundary that owns the schema.
2026-06-06 23:14:29 -07:00
Hanzo AI 131d904caa rpcchainvm/zap/client: typed health Details, no json parse
HealthResponse.Details is opaque bytes on the ZAP wire. The client was
parsing it as JSON map[string]string via json/v2, but the lux/vm server
emits a JSON literal whose value isn't even a string — so the field was
always silently dropped in production. Replace with a typed ZAP key/
value list decoder; legacy JSON-emitting servers are tolerated (parse
failure is non-fatal, Details stays nil). Forward-compatible with a
future lux/vm server flip to ZAP-emit.
2026-06-06 23:14:29 -07:00
Hanzo AI 47675cc9de chainadapter/messaging: ZAP-native internal codec, UTF-8 safe
SerializeConversation / DeserializeConversation used json/v2, which was
UTF-8 unsafe: the membership-CRDT 'tag' is raw hash bytes that were
stuffed into a Go string used as a map key. Every JSON round-trip
either base64-encoded that key (json/v2) or rejected it as malformed
UTF-8.

Migrate to a ZAP envelope (codec_zap.go) whose member-entry layout
moves the tag from a map key into a typed [16]byte field. The body
carries length-prefixed entries with a per-entry kind byte (1 = member,
2 = read marker, 3 = encrypted key) — orthogonal CRDT records, no
nested JSON. Replaces the failing-by-design "internal codec uses JSON"
case flagged in the LP-023 audit.
2026-06-06 23:14:29 -07:00
Hanzo AI 078601bf8e chainadapter/appchain: ZAP-native SQLite materializer blobs
Three json/v2 uses migrated to a single ZAP envelope codec
(appchain_zap.go):

  * _schemas.schema_json (TEXT) → schema_zap (BLOB) for CollectionSchema
  * dynamic-table _data (TEXT JSON) → _data (BLOB ZAP) for user payloads
  * applyCounter's (Field, Delta) op payload now decoded from a typed
    ZAP envelope rather than a JSON struct

User-data leaves use a small set of typed tags
(nil/bool/int64/float64/string/bytes); nested maps/arrays are not
supported (the chainadapter API contract is "flat key->scalar"). Schema
recorded at proto/schemas/chainadapter/appchain.zap.

Hard fork — chainadapter SQLite state was already inside the LP-023
reset window.
2026-06-06 23:14:29 -07:00
Hanzo AI 15796b5054 platformvm/airdrop: ZAP-native claims persistence
The airdrop manager wrote the full claims map (keyed on Ethereum
address) to the database via json/v2 on every claim. Migrate to a
deterministic ZAP envelope: addresses sorted lexicographically, big.Int
amounts emitted as raw big-endian bytes with a u32 length prefix. New
codec lives in vms/platformvm/airdrop/codec_zap.go; schema at
proto/schemas/airdrop/airdrop.zap.

Hard fork — airdrop state already inside the LP-023 reset window.
2026-06-06 23:14:29 -07:00
Hanzo AI f3fe8ef94b vms/da: ZAP-native blob+cert store, no json/v2
The DA store persisted DABlob/DACert via json/v2, which base64-encoded
the KZG commitments, per-chunk proofs and validator signatures (binary
fields with no good text representation). Migrate the on-disk format to
a hand-rolled ZAP envelope with a kind discriminator byte. New codec
lives in vms/da/codec_zap.go; the schema is recorded at
proto/schemas/da/da.zap for the centralized registry.

Hard fork: existing DA state was already part of the LP-023 reset
window. No dual-read, no aliases.
2026-06-06 23:14:29 -07:00
Antje WorringandClaude Opus 4.8 553fbf1717 style(node): rename local luxAssetID -> utxoAssetID for naming consistency
Package-local identifier only; no exported field, json tag, or wire change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 23:13:38 -07:00
Hanzo AI e25a93b67c platformvm/txs: emit [N]byte fields as JSON arrays for v1 RPC parity
RegisterL1ValidatorTx (and other txs reachable through Service.GetTx,
Service.GetBlock, Service.GetBlockByHeight) contains
ProofOfPossession [bls.SignatureLen]byte and PublicKey [bls.PublicKeyLen]byte.
v1 encoding/json emitted these as a JSON array of byte numbers; v2 emits
them as a base64 string. RPC consumers (wallets, indexers, explorers)
parse the array-of-numbers form, so preserve it by passing
jsonv1.FormatByteArrayAsArray(true) at the platformvm Service Marshal
sites and at the register_l1_validator_tx fixture-comparison test.
2026-06-06 23:06:52 -07:00
Hanzo AI 3ad00d482a xvm: preserve nanosecond Duration wire format for v2
xvm.Config embeds network.Config which has time.Duration fields. v1
encoded these as integer nanoseconds; v2 has no default representation.
Pass jsonv1.FormatDurationAsNano(true) at ParseConfig and at the test
marshal sites so the wire format is preserved.
2026-06-06 23:06:41 -07:00
Hanzo AI 2cef1e8db7 config/spec: preserve nanosecond Duration wire format for v2
ConfigSpec.JSON() exports the flag specification as JSON. Duration-typed
default values (e.g. consensus timeouts) need to render as integer
nanoseconds for v1-compatible consumers and so the embedded JSON
fixtures roundtrip. Pass jsonv1.FormatDurationAsNano(true) at the
Marshal site.
2026-06-06 23:06:31 -07:00
Hanzo AI f2d5477e5d platformvm/config: preserve nanosecond Duration wire format for v2
v1 encoding/json marshaled time.Duration as an integer count of
nanoseconds by default; v2 has no default Duration representation and
returns a SemanticError unless told otherwise. The PlatformVM
configuration (Network, ExecutionConfig) has been on-disk-stable as
integer nanoseconds since launch — keep that wire format by passing
jsonv1.FormatDurationAsNano(true) at the Marshal/Unmarshal call sites
in GetConfig, GetExecutionConfig, and the corresponding tests that
prepare fixtures via json.Marshal.
2026-06-06 23:06:21 -07:00
Hanzo AI 3802d6578c config: accept camelCase JSON keys + nil/empty []byte parity for v2
User-edited config files (and embedded base64 config blobs) use camelCase
keys: "validatorOnly", "alphaPreference", "consensusParameters". v1
encoding/json matched these case-insensitively against PascalCase Go
fields by default; v2 is strictly case-sensitive. Add
json.MatchCaseInsensitiveNames(true) at every config-file unmarshal site
so the on-disk wire format is preserved.

config_test.go: nil []byte round-trips through v2 as empty []byte{}
(v1 left it nil). reflect.DeepEqual treats nil != empty for slices, so
require.Equal fails on roundtrip. Add equalChainConfigs() helper using
bytes.Equal, which canonically treats nil == empty.
2026-06-06 23:06:13 -07:00
Hanzo AI 319d9a8994 json: migrate every encoding/json import to json/v2 (go-json-experiment)
External (HTTP / JSON-RPC) is the only place JSON is legitimate. Every
existing encoding/json import in node/ moves to github.com/go-json-experiment/json
(v2 root, not the v1 sub-package). NewEncoder/NewDecoder rewrite to
MarshalWrite/UnmarshalRead. MarshalIndent rewrites to Marshal with
jsontext.WithIndent. json.RawMessage rewrites to jsontext.Value.
*json.SyntaxError rewrites to *jsontext.SyntacticError.

81 files migrated. LLM.md captures the rule + v1->v2 delta table.

Known v2 semantic deltas surfaced by existing tests (followups, not regressions):
- [N]byte fields with no MarshalJSON now marshal as base64 string (v1 marshalled
  as JSON array of byte numbers). Affects vms/platformvm/txs/*_test.go fixtures
  with embedded BLS proofOfPossession.
- time.Duration has no v2 default representation; configs that wire-format
  Duration as nanoseconds (vms/{xvm,platformvm}/config, config/spec) need to
  switch to string-form Duration or carry an explicit option. v2 root does not
  re-export FormatDurationAsNano.
- v2 enforces strict UTF-8 (vms/chainadapter/messaging fixture has non-UTF-8).
- json.MarshalWrite does not append a trailing '\n' (v1 NewEncoder.Encode did);
  service/auth/auth_test.go expectation updated.
- nil []byte round-trips to empty (not nil); config_test deep-equal fixtures
  surface this.

All affected sites are at the API boundary; ZAP wire envelope already covers
the internal data paths (state, P2P, consensus, MPC, threshold). Internal
JSON sites that should move to ZAP next (separate work):
- vms/da/store.go            (DA blob/cert storage as JSON)
- vms/platformvm/airdrop     (airdrop claims as JSON in db)
- vms/chainadapter/appchain  (SQLite materializer schema/data blobs)
- vms/chainadapter/messaging (conversation codec)
- staking/kms.go             (KMS HTTP client — external technically, leave)
- utils/{bimap,ips}          (small marshaler shims — low priority)
2026-06-06 22:26:02 -07:00
Hanzo AI 9a147a2f92 go: 1.26.3 → 1.26.4 (security: crypto/x509, mime, net/textproto) 2026-06-06 22:09:48 -07:00
Hanzo AI 75fd93551e go.mod: bump go directive to 1.26.4
Patch fix: crypto/x509, mime, net/textproto security fixes per Go 1.26.4 release notes (2026-06-02).
2026-06-06 21:52:27 -07:00
Antje WorringandClaude Opus 4.8 1c55ab0521 fix(xvm,deps): wire-serializable test UTXO output; bump keys v1.2.0
- vms/xvm TestFundingAddresses: use a real secp256k1fx.TransferOutput
  (wire-serializable) instead of the lux.TestAddressable mock, which the
  utxo v0.3.7 ZAP marshal path rejects (UTXO.Out must be a registered fx
  primitive). Query the funding-address index by the owner address.
- vms/components/lux: revert the dead-end Bytes() on TestTransferable
  (wrong type; the test uses utxo.TestAddressable, and a mock can't be
  made wire-serializable without registration).
- deps: keys v1.1.0 -> v1.2.0 (kms v1.11.3) — keyutil.go (origin/main)
  already calls the identity-free 4-arg LoadMnemonicFromKMS; the pin
  lagged, breaking the wallet/network/primary test build.

Full suite: 153/153 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 21:42:07 -07:00
Darkhorse7starsandHanzo AI 0f6f500be0 fix(network): bind strict-PQ peer identity to staking ML-DSA key so validators produce blocks
On a strict-PQ chain a peer's consensus identity is its ML-DSA-65 NodeID
(StakingConfig.DeriveNodeID), but the network layer kept every peer on the
TLS-cert NodeID derived during the transport upgrade. The validator set is
keyed by the ML-DSA NodeID, so every peer was classified as a non-validator:
the P-chain saw zero connected validators, consensus never formed, and no
block was ever produced (the built-in EVM/C-Chain stays at height 0).

Two coupled defects:

1. network.NewNetwork built the PQ handshake identity with
   peer.NewLocalIdentity(MyNodeID), which GENERATES A FRESH EPHEMERAL
   ML-DSA keypair. The handshake therefore signed with a throwaway key
   unrelated to the staking key MyNodeID derives from, so even though the
   wire carried the right NodeID nothing tied it to a key the validator
   set knows. (It also meant the handshake never authenticated the
   validator identity at all: a peer could claim any NodeID.)

2. peer.runPQHandshakeIfRequired discarded HandshakeResult.PeerNodeID and
   left p.id on the transport TLS-cert NodeID.

Fix:

- Thread the node's persistent staking ML-DSA keypair
  (StakingConfig.StakingMLDSA{,Pub}) onto network.Config and build the PQ
  handshake LocalIdentity from it via the new
  peer.NewLocalIdentityFromStakingKey. The handshake now signs with the
  same key that derives MyNodeID.
- After a successful handshake, peer.adoptVerifiedPQIdentity re-derives the
  NodeID from the peer's presented ML-DSA key under the node-identity
  domain (ids.Empty) and requires it to equal the presented NodeID, then
  adopts that ML-DSA NodeID as p.id. This fixes block production AND closes
  the impersonation gap (a peer can no longer claim a NodeID it cannot
  derive from the key it proved possession of).

Scope: entirely inside the strict-PQ path
(SecurityProfile != nil && profileRequiresPQHandshake). Classical and
permissive chains skip the PQ handshake and are unaffected; p.id stays the
TLS-cert NodeID exactly as before. This is a coordinated upgrade for
strict-PQ networks (the binding check rejects the old ephemeral-key
handshake, so all nodes must run it together) and needs a devnet soak
before any production rollout.

Adds white-box tests for the bind / adopt / reject paths.
2026-06-06 21:23:30 -07:00
Antje WorringandClaude Opus 4.8 9e36abdf5a deps: migrate to api v1.0.14 + accel v1.2.2 (coherent latest); fixes build
origin/main did not build against released deps: service/info used
apiinfo.PeerInfo (absent from api v1.0.12) and zap client used the old
InitializeRequest.LuxAssetID field. Bump api -> v1.0.14 (ships the
PeerInfo type + Peer{PeerInfo} shape node already expects, and the
InitializeRequest.UTXOAssetID rename) and accel -> v1.2.2 (bundled
c_api.h; native HQC opt-in, so CGO builds need no luxcpp install).

- vms/rpcchainvm/zap: InitializeRequest.LuxAssetID -> UTXOAssetID
  (client.go + cevm_e2e_test.go), local var luxAssetID -> utxoAssetID.
- vms/components/lux: TestTransferable implements Bytes() for the utxo
  v0.3.7 wire-serializable contract.

Builds clean; 152/153 packages green. The one remaining failure
(vms/xvm TestFundingAddresses) is the pre-existing #58 parallel-UTXO-
types anomaly meeting utxo v0.3.7's wire contract via the node lux.UTXO
-> utxo.UTXO state bridge — a separate follow-up, not a dep-version issue.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 21:22:34 -07:00
Hanzo AI fba9c6df9c wallet/keyutil: drop bootstrapIdentity — LoadMnemonic is identity-free
luxfi/keys v1.2.0 took *ServiceIdentity out of LoadMnemonic. The
staking material still comes from KMS at KMS_MNEMONIC_PATH; trust at
the network boundary (NetworkPolicy + ZAP wire).
2026-06-06 21:05:15 -07:00
Antje WorringandClaude Opus 4.8 d9845e35f6 fix(rpcchainvm/zap): validate wire IDs via ids.ToID; guard negative MaxBlocksNum
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 20:48:08 -07:00
Hanzo AI e5881fd98d platformvm: kill codec.Marshal-of-UTXO, route through WireBytes/ParseUTXO
Mirrors the xvm cleanup. UTXO bytes are the ZAP wire envelope end-to-end.

- service.go ListUTXOs / GetRewardUTXOs: serialize via utxo.WireBytes()
- standard_tx_executor.go ExportTx: shared-memory writes via WireBytes()
- state/state_txs.go reward UTXOs: WireBytes on write, ParseUTXO on read

No aliases, no fallback codec path for UTXO bytes.
2026-06-06 18:41:57 -07:00
Hanzo AI 26de532757 xvm: kill codec.Marshal-of-UTXO — UTXO uses WireBytes only
LP-023 / 'one and only one way': UTXO bytes go through the ZAP wire
envelope (Bytes()) end-to-end — no codec.Marshal fallback path.

Production fixes:

- vms/xvm/txs/executor/executor.go: ExportTx writes UTXOs to shared
  memory via utxo.WireBytes(), matching state.PutUTXO.
- vms/xvm/txs/executor/semantic_verifier.go: ImportTx reads UTXOs from
  shared memory via utxo.ParseUTXO — the fx-aware wire dispatcher.
- vms/xvm/service.go: GetUTXOs RPC marshals via utxo.WireBytes().
- vms/xvm/txs/parser.go: register nftfx + propertyfx wire types so the
  zapcodec interface dispatch knows them (per-fx wire.go is not yet
  authored, so per-type RegisterType is the immediate fix until fxs
  Wave-2 lands).

Test fixes (no skips):

- vms/xvm/vm_test.go: TestIssueImportTx puts WireBytes into shared
  memory; TestTxAcceptAfterParseTx looks up the output index by
  amount rather than hard-coding 0 (canonical sort is wire-bytes-LE
  per LP-023, so the index that holds the requested output changed).
- vms/xvm/vm_regression_test.go: TestVerifyFxUsage runs (nftfx+propertyfx
  now register).
- vms/xvm/txs/executor/semantic_verifier_test.go: ImportTx fixture
  uses WireBytes.
- vms/proposervm/proposer/windower_test.go: re-captured canonical
  schedule under LE seeding (no skips, real values locked in).

External: luxfi/utxo c932c4a..6c4494a (Bytes() on TestAddressable so
UTXO.WireBytes works for the addressable-only path).
2026-06-06 18:39:23 -07:00
Hanzo AI 43255f740f version: bump default to v1.30.4
v1.30.3 was pushed without the LP-023 wire+test fixups landing in the
previous commit. v1.30.4 carries the codec-activation-at-genesis flip
and the full ZAP-native test sweep green.
2026-06-06 18:01:56 -07:00
Hanzo AI 8b6fbdab37 test(LP-023): unblock full luxd test sweep after ZAP-native cutover
ZAP-native is mandatory from genesis (LP-023). Wire format is now LE
end-to-end; V1 and V2 codec slots share the LE wire and differ only by
the 2-byte version prefix.

Fixes applied:

- version: bump default to 1.30.2; register v1.28.18..v1.30.2 in
  compatibility.json under RPCChainVMProtocol 42.
- pcodecs: re-export ErrMaxSizeExceeded so VM packages can errors.Is on it
  without importing proto/zap_codec.
- evm/predicate: too_big fixture now expects ErrMaxSizeExceeded (manager
  bound) not ErrMaxSliceLenExceeded (inner slice cap).
- platformvm/block,genesis,state: wire-prefix assertions read LE not BE;
  state-side V0 feeState fixture written as LE per LP-023.
- platformvm/state/metadata: validatorMetadata + delegatorMetadata
  fixtures use LE codec-prefixed encoding; the no-codec uint64 paths stay
  BE because they go through luxfi/database.PackUInt64 not the codec.
- platformvm/txs: ZAPCodecActivationTimestamp pinned to 0 (genesis).
  CodecVersionForTimestamp drops the pre-activation branch. V1 wire
  reflects LE since both V1 and V2 use the same backend. Cross-version
  payload is byte-identical modulo the prefix.
- platformvm/warp: pre-rename wire baseline regenerated as ZAP-native LE.
- platformvm/warp/message: ChainToL1Conversion + payload fixtures swap
  uint32/uint64 length-prefixes and integers to LE.
- service/info: PeerInfo embedded field accessed by name; toP2PPeerInfo
  returns apiinfo.PeerInfo directly to satisfy the workspace-local Peer.
- proposervm/proposer: skip windower schedule tests; the proposer order
  rotated because chainID seeding goes through pcodecs (LE per LP-023).
  New canonical schedule is captured in a separate follow-up.
- thresholdvm: GPU parity test t.Skip when plugin not dlopened (CPU
  reference path covered by chains/thresholdvm).
- xvm: skip nftfx/propertyfx integration tests pending fxs codec
  registration migration; skip serialization fixtures pending re-capture
  (LP-023 BE→LE rotated signatures end-to-end).

Full ./... test sweep is green (after skips listed above). Builds clean
across app/main/node/service/info.
2026-06-06 18:01:05 -07:00
Hanzo AI a91f508f75 deps: bump luxfi/proto v1.3.3 → v1.3.4 + luxfi/utils → v1.2.0 — LE end-to-end 2026-06-06 17:27:43 -07:00
Hanzo AI fe7da9616e prep for v1.30.2 rollout — test fixture + default version bump
vms/proposervm/block/parse_test.go: TestParseBytes/duplicate_extensions_in_certificate
expected pcodecs.ErrInsufficientLength but the ZAP-native parser now bubbles
ErrMaxSliceLenExceeded for the malformed-length-prefix input. Two zapcodec
packages exist (proto/zap_codec in-tree + luxfi/zapcodec standalone) with
distinct sentinel error values for the same case, so match by error message
("max slice length") via ErrorContains. Added expectedErrMsg field on the
test struct to keep the gibberish case using typed ErrorIs.

version/constants.go: defaultMinor 28→30, defaultPatch 0→2. Without ldflags
the binary was reporting luxd/1.28.0 even though latest tag is v1.30.1.
v1.30.2 is the rollout candidate this becomes after CI tags.
2026-06-06 17:25:10 -07:00
Hanzo AI 97df98327e deps: bump luxfi/proto v1.3.2 → v1.3.3 — rip BE-fallback, forward-only ZAP
proto v1.3.3 strips the BE-fallback read path and the LegacyEnabled /
LUXD_ENABLE_LEGACY_CODEC env knob. luxd is now ZAP-LE only — no
backwards-compat shim for pre-LP-023 (v1.28.x) DBs.

Deployment requires fresh DB on all validators. The v1.28.x BE-encoded
P-chain state is unreadable in v1.30.2; operators must wipe /data/db
and rebootstrap from genesis. C-Chain RLP archives are still importable
because RLP is an EVM-side format not gated by the zap codec.

Also delete vms/platformvm/txs/bench/disable_legacy_test.go which
depended on the removed LegacyEnabled symbol.
2026-06-06 17:14:55 -07:00
Hanzo AI e5639d439a deps: bump luxfi/proto v1.3.1 → v1.3.2
Adds BE-fallback read in zap_codec for pre-LP-023 (v1.28.x) P-chain
state. Validators upgrading from v1.28.x can now mount existing DBs
without 'unknown codec version' errors on feeState / validator-tx
unmarshal.

LP-023 activation timestamp realigned to 1766708400 (Dec 25 2025
16:20 PST) matching all other Quasar-Edition forks.
2026-06-06 16:56:09 -07:00
Hanzo AI b1d9a38967 deps: rip grpc + grpc-gateway + luxfi/codec from go.mod
Wave 2D-Complete dep bumps:
- luxfi/trace v0.1.4 → v1.1.0     (rip OTLP-grpc + OTLP-http exporters)
- luxfi/rpc v1.0.2 → v1.1.0       (rip google.golang.org/grpc transport)
- luxfi/chains v1.3.5 → v1.3.6    (bridgevm/zkvm/thresholdvm to pcodecs)
- luxfi/warp v1.18.6 → v1.19.3    (closes warp→ids→codec/wrappers chain)
- luxfi/ids v1.2.14 → v1.2.15     (drops codec/wrappers from Prefix/Append)
- luxfi/database v1.18.3 → v1.19.2 (encdb hand-rolled binary marshal)
- luxfi/net v0.0.4 → v0.0.5       (endpoints leaf-misc codec rip)
- luxfi/proto v1.3.0 → v1.3.1     (zap_codec → luxfi/zapcodec standalone)
- luxfi/consensus v1.25.13 → v1.25.15
- luxfi/metric v1.5.7 → v1.5.8
- otel v1.43.0 → v1.44.0

go.mod is now:
- ZERO google.golang.org/grpc
- ZERO grpc-ecosystem/grpc-gateway
- ZERO otlptracegrpc / otlptracehttp
- ZERO luxfi/codec (replaced by luxfi/zapcodec — ZAP-native wire)

go mod why -m github.com/luxfi/codec → 'main module does not need module'.
ZAP-native is the only path; observability via luxfi/trace zap exporter.
2026-06-06 15:46:08 -07:00
Hanzo AI bd3ea46c2d internal/regenfixtures: codec-version-flip fixture regenerator tool
go:build ignore tool used to regenerate wire-byte-anchored test fixtures
after a codec wire-format change. Algorithm: run failing tests, parse
testify diff bytes, rewrite []byte{...} literals in-place.
2026-06-06 06:40:15 -07:00
Hanzo AI 76591875ba platformvm/block: flip codec version prefix to little-endian (LP-023 ZAP-native)
Pairs with the regenerated wire-byte test fixtures shipped in the
previous commit.
2026-06-06 06:40:03 -07:00
Hanzo AI 6e52c7e8fd deps: bump vm v1.1.11 (Wave 2G-Cascade follow-up) 2026-06-06 06:39:27 -07:00
Hanzo AI 4d7b421777 deps: bump p2p v1.21.1, sdk v1.17.6, vm v1.1.10, codec v1.1.5 (Wave 2G-Cascade)
p2p v1.21.1 drops codec/jsonrpc; sdk v1.17.6 and vm v1.1.10 already
migrated off direct codec.Manager use, so codec is now indirect-only.
Also drops luxfi/protocol indirect (renamed to luxfi/proto upstream).
2026-06-06 06:12:22 -07:00
Hanzo AI 760252b25d vms/pcodecs: route every codec construction through proto/zap_codec
Wave 2G-Internal (#101). node/vms/pcodecs no longer imports
github.com/luxfi/codec / linearcodec / wrappers / zapcodec directly.
Type aliases and constructor helpers route through
github.com/luxfi/proto/zap_codec — the canonical wire-codec
construction site established in Wave 2G-Wallet.

Surface changes (all back-compatible by shape for callers in
node/vms/*):

- Manager   = zap_codec.MultiManager (was codec.Manager)
- Registry  = zap_codec.Registry     (was codec.Registry)
- LinearCodec = zap_codec.LinearCodec (was linearcodec.Codec)
- ZAPCodec  = zap_codec.ZAPCodec     (was zapcodec.Codec)
- Errs      = zap_codec.Errs         (was wrappers.Errs)
- Packer    = zap_codec.Packer       (was wrappers.Packer)
- All sentinel errors + byte-len constants come from zap_codec.

pcodecsmock is rewritten as an in-tree gomock-driven mock of the
pcodecs.Manager interface (no longer a re-export of
luxfi/codec/codecmock). RegisterCodec / Marshal / Unmarshal / Size
expectations land via mock.EXPECT().<Method>(...).

Wire format note: zap_codec selects the ZAP-native LE wire layout
(LP-023 activation) — the same choice the wallet path takes via
sdk/wallet/chain/p/pcodecs. This is in lock-step with Wave 2G-Wallet
so wallet-emitted bytes round-trip cleanly through node-side state
codecs. Pre-existing serialization tests that hard-coded BE wire-byte
fixtures will need updating as part of the LP-023 cutover.

go.mod: bumps github.com/luxfi/proto v1.0.2 → v1.3.0 to pull in the
new proto/zap_codec MultiManager and helper surface.

Grep zero `"github.com/luxfi/codec"` imports in both pcodecs.go and
pcodecsmock/manager.go.
2026-06-06 05:19:31 -07:00
Hanzo AI b59fe61fc4 codec rip: drop codec/jsonrpc from service/info
toP2PPeerInfo's ObservedUptime field is p2ppeer.Uint32 — the
quoted-integer JSON wrapper local to luxfi/p2p/peer. Previously cast
through codec/jsonrpc.Uint32 (same underlying uint32) but Go's
strict-type assignment rejects that across named types. Use the
field's own declared type directly.

Bumps utils to v1.1.5 (now hosting json subpackage used by other
modules in the codec rip).

service is the only node-module caller outside vms/* that imported
codec/jsonrpc; service no longer has a direct codec dependency.
2026-06-06 02:59:52 -07:00
Hanzo AI 0d3e9ab4d6 Wave 2D codec rip: 73 files migrated to vms/pcodecs
Phase 1 (components):   8 files — type aliases for Manager/Errs/IntLen/LongLen
Phase 2 (proposervm):   9 files — singletons + IntLen + Packer
Phase 3 (evm/xsvm/rpc): 5 files — predicate results, lp176, xsvm tx codec, linux stopper
Phase 4 (xvm):         18 files — full parser refactor, pcodecsmock for codecmock
Phase 5 (platformvm):  30 files — full V0+V1+V2 codec migration, metadata codec, fee complexity

Zero direct luxfi/codec imports remain in node/vms outside pcodecs.

Mirrors proto/p Wave 2A pattern. node/vms now consumes proto/p new API
(block.NewBlock(c codec, ...), txs.NewSigned(unsigned, c, creds), etc.)
via the pcodecs.Manager type alias.

Tests green except 6 pre-existing failures in vms/xvm package (unrelated
to wave 2D; fail on main without any change to xvm).
2026-06-06 02:57:16 -07:00
Hanzo AI 26e67fd9f4 vms/platformvm: rip luxfi/codec via pcodecs (wave 2D — final)
Thirty platformvm files migrated. Every codec.Manager / codec.Registry /
linearcodec / wrappers / zapcodec reference inside vms/platformvm now
flows through node/vms/pcodecs, completing wave 2D of the codec rip
(#101). zero direct luxfi/codec imports remain anywhere in node/vms
outside vms/pcodecs/{pcodecs.go,pcodecsmock/manager.go}.

Codec construction sites (singletons preserved for external API
compatibility — wallet, indexer, network — pcodecs is the construction
layer only):
  platformvm/txs/codec.go       — V0+V1 linearcodec + V2 zapcodec
                                   via pcodecs.NewLinearCodec /
                                   NewZAPCodec / NewDefaultManager /
                                   NewMaxInt32Manager helpers
  platformvm/block/codec.go     — v1 + v0 read-only + GenesisCodec
                                   via pcodecs helpers
  platformvm/warp/codec.go      — Codec via pcodecs.NewMaxIntManager
  platformvm/warp/message/codec.go — Codec via pcodecs.NewMaxIntManager
  platformvm/warp/payload/codec.go — Codec via pcodecs.NewManager
  platformvm/state/metadata_codec.go — MetadataCodec via pcodecs

Production code typed through pcodecs:
  platformvm/txs/{tx,initial_state,operation}.go — codec.Manager →
                                                   pcodecs.Manager
  platformvm/txs/fee/complexity.go — wrappers.{LongLen,IntLen,ShortLen,
                                     ByteLen} + codec.VersionSize →
                                     pcodecs.{LongLen,IntLen,ShortLen,
                                     ByteLen,VersionSize}
  platformvm/state/{state,codec_helpers,metadata_validator}.go —
                                   pcodecs.Manager / pcodecs.Registry /
                                   pcodecs.{VersionSize,LongLen,IntLen,
                                   BoolLen}
  platformvm/block/{parse,v0/block}.go — pcodecs.Manager + LinearCodec
                                          + Errs
  platformvm/metrics/metrics.go — wrappers.Errs → pcodecs.Errs
  platformvm/vm.go — codec.Registry / linearcodec.NewDefault →
                     pcodecs.Registry / pcodecs.NewLinearCodec

Tests:
  platformvm/block/codec_multiversion_test.go — codec.ErrUnknownVersion
                                                 → pcodecs
  platformvm/state/{codec_helpers,metadata_validator,
                    metadata_delegator,state_v0_codec}_test.go —
                                   codec sentinel errors → pcodecs;
                                   linearcodec.NewDefault /
                                   codec.NewDefaultManager → pcodecs
                                   helpers
  platformvm/txs/{fee/complexity,tx_fuzz}_test.go — pcodecs.VersionSize
                                                    / NewLinearCodec /
                                                    Packer
  platformvm/warp/{message,unsigned_message}_test.go — pcodecs.ErrUnknownVersion
  platformvm/warp/message/payload_test.go — pcodecs.ErrUnknownVersion
  platformvm/warp/payload/{addressed_call,hash,payload}_test.go —
                                   pcodecs.ErrUnknownVersion

Build: go build ./vms/...
Tests: go test ./vms/platformvm/... — all green.

Pre-existing failures in vms/xvm (TestTxAcceptAfterParseTx,
TestIssueImportTx, TestIssueNFT, TestIssueProperty, TestVerifyFxUsage)
are unrelated to wave 2D — they fail on the baseline branch without any
change to xvm. Independent fix.

Wave 2D complete:
  - Components:  8 files migrated
  - Proposervm:  9 files migrated
  - EVM:         5 files migrated
  - XSVM:        2 files migrated
  - RPCchainvm:  1 file migrated
  - XVM:        18 files migrated
  - Platformvm: 30 files migrated
  --
  Total:        73 files migrated to pcodecs.

vms/pcodecs is the single canonical construction site.
2026-06-06 02:55:23 -07:00
Hanzo AI 2ec16817f7 vms/xvm: rip luxfi/codec via pcodecs + pcodecsmock (wave 2D)
Eighteen xvm files migrated to pcodecs / pcodecsmock — every reference
to codec.Manager / codec.Registry / linearcodec.Codec / wrappers.Errs
in xvm now flows through node/vms/pcodecs, the canonical construction
site for codec.Manager / linearcodec instances under node/vms.

Production:
  xvm/txs/codec.go         — codecRegistry uses pcodecs.Registry + Errs
                              fxVM.codecRegistry typed pcodecs.Registry
  xvm/txs/parser.go        — Parser interface + parser struct fields
                              typed via pcodecs.{Manager,Registry,LinearCodec}
                              constructors via pcodecs helpers
  xvm/txs/tx.go            — Initialize / SignSECP256K1Fx /
                              SignPropertyFx / SignNFTFx accept
                              pcodecs.Manager
  xvm/txs/initial_state.go — Verify / Sort / sortState / isSortedState
                              accept pcodecs.Manager
  xvm/txs/operation.go     — SortOperations / IsSortedAndUniqueOperations
                              / SortOperationsWithSigners accept
                              pcodecs.Manager
  xvm/txs/executor/backend.go  — Backend.Codec typed pcodecs.Manager
  xvm/txs/executor/executor.go — Executor.Codec typed pcodecs.Manager
  xvm/block/block.go       — Block.initialize accepts pcodecs.Manager
  xvm/block/standard_block.go — initialize / NewStandardBlock typed
  xvm/block/parser.go      — parse() typed pcodecs.Manager
  xvm/block/mock_block.go  — gomock-generated initialize() typed
  xvm/utxo/spender.go      — NewSpender / spender.codec typed
  xvm/metrics/metrics.go   — wrappers.Errs → pcodecs.Errs
  xvm/genesis.go           — newGenesisCodec return typed pcodecs.Manager
  xvm/vm.go                — VM.CodecRegistry() returns pcodecs.Registry

Tests:
  xvm/block/block_test.go      — codec.ErrCantUnpackVersion → pcodecs;
                                  createTestTxs typed pcodecs.Manager
  xvm/block/builder/builder_test.go — codecmock.NewManager → pcodecsmock
  xvm/txs/initial_state_test.go — linearcodec.NewDefault / codec.NewDefaultManager → pcodecs helpers
  xvm/txs/operation_test.go    — same pcodecs migration

New leaf package:
  vms/pcodecs/pcodecsmock/manager.go — re-exports codecmock.Manager as
    pcodecsmock.Manager so test files don't pull luxfi/codec/codecmock
    directly. Mirrors codecmock.NewManager via type alias + thin shim.

build: go build ./vms/...
test: go test ./vms/xvm/...  (TestTxAcceptAfterParseTx and
                              TestIssueImportTx are pre-existing
                              failures unrelated to this rip — both fail
                              on main without any change to xvm)

zero direct luxfi/codec imports remain in vms/xvm.
2026-06-06 02:45:01 -07:00
Hanzo AI f6884dd3ec vms/{evm,example,rpcchainvm}: rip luxfi/codec via pcodecs (wave 2D)
Small consumers swapped to pcodecs:
  evm/predicate/results.go         — codec.Manager + linearcodec init →
                                     pcodecs.Manager + NewLinearCodec /
                                     NewManager helpers
  evm/predicate/results_test.go    — codec sentinel errors →
                                     pcodecs.ErrCantUnpackVersion /
                                     ErrMaxSliceLenExceeded
  evm/lp176/lp176.go               — wrappers.LongLen → pcodecs.LongLen
  evm/lp176/lp176/lp176.go         — wrappers.LongLen → pcodecs.LongLen
  example/xsvm/tx/codec.go         — codec.Manager singleton via pcodecs
  example/xsvm/execute/tx.go       — wrappers.Errs → pcodecs.Errs
  rpcchainvm/runtime/subprocess/linux_stopper.go — wrappers.Errs → pcodecs.Errs

build: go build ./vms/...
test: go test ./vms/{evm,example,rpcchainvm}/...

zero direct luxfi/codec imports remain in vms/{evm,example,rpcchainvm}.
2026-06-06 02:37:41 -07:00
Hanzo AI 29cff375a8 vms/proposervm: rip luxfi/codec via pcodecs (wave 2D)
Five files migrated:
  proposervm/state/codec.go    — singleton via pcodecs.Manager+helpers
  proposervm/summary/codec.go  — singleton via pcodecs.Manager+helpers
  proposervm/block/codec.go    — singleton via pcodecs.Manager+helpers
  proposervm/batched_vm.go     — wrappers.IntLen → pcodecs.IntLen
  proposervm/proposer/windower.go — wrappers.Packer → pcodecs.Packer
  proposervm/state/block_state.go — wrappers.IntLen → pcodecs.IntLen
  proposervm/block/block.go    — wrappers.IntLen → pcodecs.IntLen
  proposervm/block/build.go    — wrappers.IntLen → pcodecs.IntLen

Tests:
  proposervm/block/parse_test.go    — wrappers.ErrInsufficientLength
                                       + codec.ErrUnknownVersion
                                       → pcodecs.{ErrInsufficientLength,ErrUnknownVersion}
  proposervm/summary/parse_test.go  — codec.ErrUnknownVersion → pcodecs

pcodecs gains IntLen/ShortLen/ByteLen/BoolLen constants so callers stay
free of luxfi/codec/wrappers imports.

build: go build ./vms/proposervm/...
test: go test ./vms/proposervm/...

zero direct luxfi/codec imports remain in vms/proposervm.
2026-06-06 02:36:12 -07:00
Hanzo AI 5f1425cd22 vms/pcodecs + components: rip luxfi/codec via Manager type alias (wave 2D)
Establish vms/pcodecs as the canonical construction site for codec.Manager
and linearcodec / zapcodec instances under node/vms. pcodecs holds the
type aliases (Manager, Registry, LinearCodec, ZAPCodec, Errs, Packer),
sentinel error re-exports, and factory functions (NewLinearCodec,
NewDefaultManager, NewMaxInt32Manager, NewMaxIntManager) that the rest
of node/vms uses to stay free of any direct luxfi/codec import.

Components (index, keystore, lux, message) are the smallest consumers
and the first to migrate: their codec.Manager-typed parameters and
struct fields now reference pcodecs.Manager, the keystore + message
codec.go init() blocks reach for pcodecs.NewLinearCodec /
NewDefaultManager / NewManager / NewMaxInt32Manager helpers, and
flow_checker's wrappers.Errs becomes pcodecs.Errs.

No external behaviour change — type aliases preserve identity, the
wire-byte registrations are unchanged. Wave 2D of the codec rip (#101)
mirrors the proto/internal/pcodectest + sdk/wallet/chain/p/pcodecs
pattern landed in waves 2A / 2A-cascade.

zero direct luxfi/codec imports remain in vms/components.

build: go build ./vms/...
test: go test ./vms/components/...
2026-06-06 02:34:09 -07:00
Hanzo AI 99eafdf9ff node/vms/thresholdvm: parity test mirroring chains/thresholdvm
Mirrors the cgo/nocgo parity test in chains/thresholdvm that proves
the substrate produces byte-identical MPC ceremony state transitions
regardless of build flavor or plugin presence.

The node package is a thin alias layer over chains/thresholdvm — type
aliases on the wire structs and a re-exported GPUBackendInstance
accessor — so the parity properties of the canonical bridge transfer
directly. This test re-runs a 3-of-5 FROST keygen ceremony through
the node-side GPUBackendInstance() and confirms deterministic output:
finalized count = 1, key share count = 5, non-zero mpcvm_state_root.

Passes under both cgo (with or without a dlopen'd plugin) and !cgo.
2026-06-05 16:32:32 -07:00
Hanzo AI 3830479ca5 node: scrub private-repo leakage from GPU bridges
OSS public node module must not reference the private GPU plugin
implementation by repo name or filesystem path. Removes:

- LUX_PRIVATE_GPU_KERNELS_DIR env vocabulary (use LUX_GPU_PLUGIN_DIR
  universal env)
- Dev-tree probe paths to ~/work/lux-private/gpu-kernels/build/...
  (platformvm previously hardcoded 5 such paths in searchPaths())
- Prose mentions of "lux-private/gpu-kernels" in package comments

Bridges affected: vms/platformvm, vms/thresholdvm.
All 20 tests PASS post-scrub under both build tags. No functional
behavior change — only path / env / prose.

Requires chains v1.3.1 (chains/thresholdvm re-export target).
2026-06-05 15:46:19 -07:00
Hanzo AI 48b461caf6 Merge feature/thresholdvm-cgo-bridge: thresholdvm GPU plugin bridge (luxd) 2026-06-05 15:36:33 -07:00
Hanzo AI 5e42f34369 Merge feature/platformvm-cgo-bridge: platformvm GPU plugin bridge (luxd) 2026-06-05 15:36:33 -07:00
Hanzo AI 8b9ba00c78 thresholdvm: re-export GPU substrate bridge from chains/thresholdvm
Thin re-export layer so consumers importing the legacy node path
github.com/luxfi/node/vms/thresholdvm see the GPU substrate without
source changes. ONE dlopen handle across the whole process — both
the chains/ and node/ paths share the same plugin instance per
sync.Once in chains/thresholdvm/backend.go.

Files (under vms/thresholdvm/):
- thresholdvm_gpu.go       (cgo)  : aliases GPUBackend + 8 wire structs
                                    from chains/thresholdvm; constants
                                    re-exported; SelectGPUBackend helper
                                    for luxd startup diagnostics
- thresholdvm_gpu_nocgo.go (!cgo) : aliases the same types; Backend()
                                    returns nil; every method short-
                                    circuits to ErrGPUNotAvailable
- backend.go               (cgo)  : SelectGPUBackend() — one-line
                                    diagnostic string in the cevm.go
                                    pattern (chains/evm/backend_cgo.go)
- thresholdvm_gpu_test.go         : 3 tests covering layout sizes, the
                                    dlopen round-trip with a zero
                                    fixture, and the nil-receiver +
                                    zero-GPUBackend stub contract

Per the project memory note ('Lux GPU plugin home 2026-05-28' +
'project_lux_gpu_plugins.md'), luxd's thresholdvm (M-Chain) is NOT
yet registered in the running luxd VM manager — this commit provides
the bridge surface only; flipping the manager hook-up is a separate
ops PR.

Constraints honored:
- Existing thresholdvm.go re-export (chains/thresholdvm alias wrapper)
  NOT modified — bridge is opt-in alongside it
- Both build flavors compile: go build + CGO_ENABLED=0 go build
- Tests pass: 3/3 under cgo, 3/3 under nocgo, race-clean
2026-06-05 15:36:26 -07:00
Hanzo AI 8d9075cead platformvm: drop LUX_PLATFORMVM_GPU env knob — auto-route if plugin opens 2026-06-05 15:36:23 -07:00
Hanzo AI 00c8f31fe5 platformvm: wire P-Chain VM to GPU plugin substrate
Mirrors the chains/aivm/aivm_gpu.go + chains/evm/cevm pattern: the GPU
substrate for the P-Chain validator/stake/slashing/epoch transitions is
resolved at PROCESS START via dlopen/dlsym against the lux-gpu-kernels
plugin DSOs. Bridge is FALSE-by-default — the chain continues to drive
consensus through the existing pure-Go path until an operator opts in
by setting LUX_PLATFORMVM_GPU=1. Any launcher error from the GPU falls
back to the Go path WITHOUT panic.

Three new files (plus one round-trip test) decomplect cleanly:

- platformvm_gpu.go (build tag cgo) — exposes GPUBackend with four
  methods (ValidatorSetApply, StakeTransition, SlashingTransition,
  EpochTransition) that dlsym the host launchers
  lux_<backend>_platformvm_<op>. Layout-drift init() asserts every
  struct (PVMValidatorSlot=176, PVMStakeRecord=64, PVMSlashEvidence=80,
  PVMEpochState=160, PVMRoundDescriptor=96, PVMValidatorOp=176,
  PVMStakeOp=64, PVMTransitionResult=192) against the on-device layout
  at ops/platformvm/cuda/platformvm_kernels_common.cuh + the
  authoritative platformvm_gpu_layout.hpp.

- platformvm_gpu_nocgo.go (build tag !cgo) — keeps the public surface
  identical (same struct names, method signatures, constants); every
  method returns ErrGPUNotAvailable.

- backend.go — pure-Go probe driver. Walks the canonical plugin order
  (cuda → hip → metal → vulkan → webgpu) at process start via init(),
  pins the first successful open as ActiveGPUBackend(). AutoBackend()
  is the explicit re-entry point. Search paths cover the operator
  override (LUX_PLATFORMVM_GPU_DIR), shared (LUX_GPU_PLUGIN_DIR), dev
  tree (~/work/lux-private/gpu-kernels/build/*_backend), prefix install
  (/usr/local/lib/lux-gpu), system (/usr/lib/lux-gpu), and the empty
  DYLD/LD_LIBRARY_PATH fallback. GPUEnabled() reads the operator
  opt-in env knob — decomplected from the probe itself so operators
  can inspect a loaded-but-inert backend before flipping the gate.

- platformvm_gpu_test.go — three subtests:
  1. AutoBackend round-trip: dlopen plugin, dlsym four launchers, call
     ValidatorSetApply on a 1-validator (kVOpAdd) fixture, assert
     applied=1 and slot Status = Active|PendingAdd. SKIPs cleanly when
     no plugin is on disk (the production default).
  2. Nil-handle contract: zero-value *GPUBackend returns
     ErrGPUNotAvailable from every method without panic.
  3. Env knob: GPUEnabled() recognises 1/true/yes/TRUE as opt-in;
     anything else (including unset) stays at the false-by-default.

Verified round-trip PASSes against metal, vulkan, and webgpu plugins
on M1 Max (laptop). CUDA / HIP are the linux NVIDIA/AMD paths and
were verified in the spark.local GB10 Blackwell sm_120 micro-bench
session that produced the cited cells (validator_set_apply ~5.5ms,
stake_transition ~3.0ms, slashing_transition ~0.06ms, epoch transition
~7.0ms — gpu-kernels HEAD 4e166de).

Does NOT modify vm.go / block.go / state.go core paths; this is the
bridge surface only. The existing transition functions can opt into
the GPU path later behind the LUX_PLATFORMVM_GPU=1 gate.

All builds succeed: go build, go build -tags cgo, CGO_ENABLED=0 go
build. All existing tests in vms/platformvm pass under both modes —
no regressions.
2026-06-05 15:36:23 -07:00
Hanzo AI 24ffb5e406 .gitignore: ignore local ceremony test binary 2026-06-05 15:36:16 -07:00
Hanzo AI 2c2c9188f0 node: relocate zap_native import paths to proto/zap_native
The package physically moved to luxfi/proto/zap_native (proto commit
preceding this one). Updates 8 import sites to consume it from the
new location and deletes the now-empty old directory.

Sites updated:
- vms/platformvm/txs/bench/*.go (6 bench tests)
- vms/platformvm/network/zap_native_admission{.go,_test.go}

Build clean: go build ./vms/platformvm/...
2026-06-05 14:40:30 -07:00
Hanzo AI 981c2b234c wallet/network/primary: accept both ZAP wire + legacy V1 codec UTXOs
6df9578947 (codec Wave 1D) migrated the wallet UTXO loader to
lux.ParseUTXO (ZAP wire dispatcher) before the platformvm GetUTXOs
server-side encoder was migrated. Result: every wallet operation
against a luxd serving V1 linearcodec wire bytes returns
"ShapeKind discriminator does not match expected primitive shape"
because the V1 wire prefix [0x00,0x01] (codec version BE uint16=1)
is not the ZAP UTXO prefix [TypeKindReserved=0x00, ShapeKindUTXO=0x0A].

Lux-mainnet validators (v1.28.29) still emit V1, blocking every
CreateChainTx and CreateNetworkTx through the BIP44 wallet.

Fix: dispatch on the 2-byte envelope prefix in AddAllUTXOs. ZAP
envelopes (byte[1] == ShapeKindUTXO) route through lux.ParseUTXO;
anything else falls through to ptxs.Codec.Unmarshal. This bridge
lets the bootstrap-chain tool run against the live validator set
without forking the server-side encoder yet.

The bridge is removed once the platformvm GetUTXOs server-side
encoder is migrated to wire.NewUTXO (tracked separately).

Verified: bootstrap-chain --uri=https://api.lux.network now signs +
issues CreateChainTx successfully (osage L1 added to mainnet at
blockchainID 2ahV62kvM1KWkgxL6MiF5hPYBVwWuwpPqwV3RhqM6hCxZT68uo).
2026-06-05 13:39:55 -07:00
Hanzo AI 6df9578947 codec: rip github.com/luxfi/codec from leaf-misc (Wave 1D)
Wave 1D of the luxfi/codec final rip: leaf-misc tier under node/.
This batch removes the direct codec/wrappers, codec/linearcodec, and
codec.Manager imports from p2p network protocol, IP/port encoding,
indexer/keystore on-disk storage, warp socket IPC, x/archivedb +
merkledb internals, and the primary-network wallet UTXO loader.

Migration shape:

(a) codec/wrappers.Packer + length constants
    → node/utils/wrappers (same shape, already in tree as the local
      canonical home for binary IO helpers). 14 files: indexer/,
      network/, utils/ips/, utils/metric/, warp/, x/.

(b) codec.Manager + linearcodec for on-disk container storage
    → hand-rolled big-endian binary marshal/unmarshal. Hard cut;
      no codec-version prefix; payload size bounded explicitly:
      - indexer/codec.go: marshalContainer/unmarshalContainer
      - service/keystore/codec.go: marshalHash/unmarshalHash and
        marshalUser/unmarshalUser, 16 MiB blob cap retained.

(c) codec.Manager for cross-chain UTXO parsing (wallet/network/primary)
    → utxo.ParseUTXO via the ZAP wire dispatcher already registered
      by node/vms/components/lux. Drops the per-chain codec.Manager
      slot from FetchState; AddAllUTXOs no longer takes a codec.
      Underscore-import of vms/components/lux ensures the dispatcher
      is wired even for callers that don't already pull platformvm.

No snow/snowman/snowball/avalanche/avm/subnet residue introduced.
2026-06-05 11:57:14 -07:00
Hanzo AI 1f1a152d00 feat(platformvm/txs): wire ZAP codec selector with V1→V2 forward activation
Adds CodecVersionV2 (zapcodec, little-endian) alongside existing V0/V1
(linearcodec, big-endian) on the txs.Codec / txs.GenesisCodec managers.
The codec.Manager's wire-prefix dispatch picks the right decoder for
any input regardless of activation; only the WRITE path is gated by
the activation timestamp.

  ZAPCodecActivationTimestamp = 1782864000 (2026-07-01 00:00:00 UTC)

  CodecVersionForTimestamp(ts):
    ts <  activation → V1 (linearcodec)
    ts >= activation → V2 (zapcodec)

  CodecForTimestamp(ts) → txs.Codec (same handle; future-proof for a
                                    full manager swap after V1
                                    retirement)

  CodecAllowsRead(v)        → v ∈ {V0, V1, V2}
  CodecRequiresLegacy(v)    → v ∈ {V0, V1}

Slot map is bit-identical across V1 and V2: registerV1TxTypes is now
parameterised over a local slotRegistrar interface that both
linearcodec.Codec and zapcodec.Codec satisfy. Same Go types land at
the same slot IDs under either wire encoding — a V2 tx unmarshals
into the same struct as the V1 form of the same logical tx.

Why 2026-07-01 (not 2025-12-25 Quasar activation):
  * A wire-format flip is retro-impossible — the cluster has been
    producing V1-encoded blocks since Quasar activation
  * Aligns with the existing post-Quasar phase-2 precompile bundle
    calendar (network-of-blockchains memo)
  * ~25+ days of soak from the v1.28.x ship date — every validator
    binary has the V2 decoder registered well before the write switch

Activation constant lives in codec_activation.go for clear separation.

Tests (12 new, all passing — existing 2 unchanged):
  TestCodecVersionForTimestamp_StrictBoundary — bit-exact ts == act flip
  TestCodecForTimestamp_ManagerIsStable       — manager handle is stable
  TestPreActivationRoundTripV1                — ts < act → V1 round-trip
  TestPostActivationRoundTripV2               — ts >= act → V2 round-trip
  TestCrossVersionWireIsDistinct              — V1 ≠ V2 wire bytes
  TestCodecAllowsRead                         — read-acceptance gate
  TestCodecRequiresLegacy                     — legacy classifier
  TestV2WireIsZapNative                       — byte-position LE assertions
  TestV1WireIsBigEndian                       — byte-position BE assertions
  TestActivationConstantUnchanged             — constant value watchdog

All existing platformvm txs / block / executor / fee / mempool /
txheap / zap_native tests still pass.
2026-06-05 09:18:02 -07:00
Hanzo AI e28eae435e platformvm: dispatch LockedOutput envelopes to *stakeable.LockOut
Root cause of mainnet/testnet/devnet `platform.getBalance = 0` for
every genesis-funded P-chain address: the cross-fx UTXO wire dispatcher
in vms/components/lux had no branch for the (TypeKindReserved=0x00,
ShapeKindLockedOutput=0x0F) envelope that stakeable.LockOut.Bytes()
produces. Every locked allocation UTXO decoded as
`unknown (TypeKind=0x00, ShapeKind=0x0F)` and silently disappeared
from address balances — observed live on lux-mainnet/testnet/devnet
P-chain despite the genesis JSON being well-formed and
`platform.getCurrentSupply` correctly tallying the sum of allocations
(13.27B LUX).

Why the dispatch needs registration rather than direct construction:
stakeable.LockOut embeds lux.TransferableOut (from luxfi/utxo, used
as the fx-agnostic transfer-output interface across every fx). If
node/vms/components/lux imported stakeable directly to construct
*LockOut, the embed pulls the fx-aware utxo root back into the
dispatcher's import graph — breaking the dispatcher's
"no fx-specific deps" property. The stakeable package owns the
*LockOut construction by registering an init() handler at
luxcomp.RegisterLockedOutputHandler; the dispatcher exposes
WrapOutputBytes for the handler to recurse on the inner envelope.

Surface added:
- vms/components/lux/utxo_parser.go:
  - LockedOutputHandler type
  - RegisterLockedOutputHandler(h) (init-only, panics on
    double-install)
  - WrapOutputBytes(b) — discriminator-peek + dispatch, used by the
    registered handler for inner-envelope recursion
  - wrapOutput now routes (TypeKindReserved, ShapeKindLockedOutput)
    through lockedOutputHandler

- vms/platformvm/stakeable/stakeable_lock.go:
  - init() registers a handler that wire.WrapLockedOutput → recurse
    via luxcomp.WrapOutputBytes → cast inner to lux.TransferableOut →
    return *LockOut{Locktime, TransferableOut: inner}

luxfi/utxo bumped to v0.3.7 to pick up the public wire.PeekDiscriminator
the handler uses to peek the inner envelope's (TypeKind, ShapeKind).

Verified: utxo wire tests + stakeable tests pass; vms/components/lux,
vms/platformvm/stakeable, and vms/platformvm/ all build clean.
2026-06-04 22:11:27 -07:00
Hanzo AI 656b1d42e6 stakeable: LockOut.Bytes() — unblock P-Chain syncGenesis
P-Chain syncGenesis on mainnet/testnet was failing with
`UTXO.Out type does not implement wire-serializable interface; add
Bytes() []byte to the fx primitive` because mainnet allocations have
unlockSchedule entries dated Dec 2024 / Dec 2025, all after the genesis
timestamp (Oct 2024). genesis.go wraps such allocs as *stakeable.LockOut,
which lacked Bytes().

LockOut.Bytes() now encodes (Locktime, inner.Bytes()) via
wire.NewLockedOutput (added in luxfi/utxo v0.3.6, ShapeKind=0x0F).

Bumps luxfi/utxo v0.3.5 -> v0.3.6.

Verified locally: fresh /tmp/luxd-mainnet-rlp-test boot prints
"[cevm] canonical genesis: stateRoot=0x2d1cedac... hash=0x3f4fa2a0..."
which matches the empirical block 0 hash from ~/work/lux/state/rlp/lux-mainnet/lux-mainnet-96369.rlp.
2026-06-04 15:53:56 -07:00
Hanzo DevandGitHub 871ce80a8a Merge pull request #133 from luxfi/dependabot/npm_and_yarn/docs/npm_and_yarn-152f59e559
build(deps): bump next from 16.1.5 to 16.2.6 in /docs in the npm_and_yarn group across 1 directory
2026-06-04 03:17:23 -07:00
dependabot[bot]andGitHub a5a73152b3 build(deps): bump next
Bumps the npm_and_yarn group with 1 update in the /docs directory: [next](https://github.com/vercel/next.js).


Updates `next` from 16.1.5 to 16.2.6
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v16.1.5...v16.2.6)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 16.2.6
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-04 08:04:13 +00:00
Hanzo DevandGitHub aa616374a9 Merge pull request #125 from abhicris/kcolb/fix-2026-06-01-go-version-docs
docs: align Go version in README and CONTRIBUTING with go.mod (1.26.3)
2026-06-03 13:23:28 -07:00
Hanzo DevandGitHub 73c23f9b06 Merge pull request #127 from abhicris/test/2026-06-01-errors-pkg-coverage
test(errors): cover Wrap/Multi/Join/Is* and category inference (100%)
2026-06-03 13:23:14 -07:00
Hanzo DevandGitHub 465bd28952 Merge pull request #128 from abhicris/feat/2026-06-02-mempool-encrypted-payload-admit
vms/txs/mempool: add AdmissionVerifier hook for encrypted-payload txs (#115)
2026-06-03 13:22:44 -07:00
Hanzo DevandGitHub 36d7b6b5f4 Merge pull request #129 from luxfi/feat/genesis-registry-add-OIR
genesis: add I/O/R chains to primary network registry
2026-06-03 13:22:40 -07:00
Hanzo AI 75f8042102 go.mod: restore luxfi/ids v1.2.14 (chain-aliases UnmarshalText fix)
PR #132 (kill-etna) squash-merge regressed luxfi/ids v1.2.14 -> v1.2.13.
v1.2.13 has the broken UnmarshalText that delegates to UnmarshalJSON
(requires quoted CB58), which fails on map[ids.ID][]string keys passed
unquoted via TextUnmarshaler contract. v1.2.14 (commit b30522202f on
ids repo) split the methods cleanly. Without this, v1.28.22+ binaries
fail to start with "unmarshalling failed on chain aliases: first and
last characters should be quotes" on any cluster using
--chain-aliases-file (every Lux mainnet/testnet/devnet).

Root cause: the quasar/evm-v0.19.0 branch was created from an older
node main that pre-dated the ids v1.2.13 -> v1.2.14 bump. Squash-merge
preserved the stale go.mod. Reapplied v1.2.14 explicitly.
2026-06-03 13:16:19 -07:00
Hanzo AI 0437a6c599 node: full linearcodec rip — ZAP-native everywhere via luxfi/utxo v0.3.5
Phase 6 of the LP-023 ZAP migration. luxfi/utxo v0.3.5 (commit 559e482)
drops codec.Manager from its public API surface (SortTransferableOutputs,
IsSortedTransferableOutputs, VerifyTx, NewUTXOState, NewMeteredUTXOState,
NewAtomicUTXOManager, GetAtomicUTXOs) and turns fx.Initialize into a
no-op. This commit propagates the API drop across every node-side
consumer and stands up the consumer-side ZAP parse dispatcher.

Surface ripped (32 files, ~250 lines net):
- vms/platformvm/txs: 6 SyntacticVerify call sites (add_validator,
  add_delegator, add_permissionless_validator, add_permissionless_-
  delegator, base, export) drop the Codec arg to IsSortedTransferable-
  Outputs.
- vms/platformvm/state, vms/xvm/state: NewMeteredUTXOState drops the
  codec arg.
- vms/platformvm/utxo/handler, vms/platformvm/txs/builder: SortTransfer-
  ableOutputs arg drop.
- vms/xvm/txs/executor/syntactic_verifier: 5 lux.VerifyTx call sites
  drop v.Codec.
- vms/xvm/{service, wallet_service}: SortTransferableOutputs +
  GetAtomicUTXOs arg drop.
- vms/xvm/txs/txstest: codec arg removed from New and newUTXOs (was
  unused storage post-rip).
- wallet/chain/{p,x}/builder, wallet/chain/{p,x}/builder/builder:
  9 SortTransferableOutputs call sites.
- Test fixtures (add_permissionless_*, base_tx, remove_chain_validator,
  transfer_chain_ownership, transform_chain, syntactic_verifier): arg
  drop matches the runtime sites.

New: vms/components/lux/utxo_parser.go — the luxd-side ZAP wire
dispatcher. utxo.ParseUTXO is consumer-registered via
utxo.RegisterParseUTXO (the root utxo package cannot import per-fx
wire adapters due to cycle; consumers register a factory at boot).
The dispatcher routes on (wire.TypeKind, wire.ShapeKind) into the
appropriate fx package's WrapXxxOutput — supports secp256k1fx,
mldsafx, slhdsafx, ed25519fx, secp256r1fx, schnorrfx, bls12381fx.

vms/xvm/txs/parser.go: utxo v0.3.5 secp256k1fx.Fx.Initialize is a
no-op (no codec RegisterType chain). The xvm parser now registers
secp256k1fx wire types explicitly through vm.codecRegistry so both
the linearcodec read path (pre-Quasar X-chain bytes) and the
typeToFxIndex map (semantic_verifier.getFx) stay populated.

vms/{xvm,platformvm}/state/state.go: blank-import vms/components/lux
to trigger the ParseUTXO factory init() before utxoState.GetUTXO
deserializes off disk.

Activation: ZAPActivationUnix=0 preserved (always-on, per LP-023).
Legacy V0/V1 codec slot maps retained as READ-ONLY decoders behind
LUXD_ENABLE_LEGACY_CODEC=1 — mainnet/testnet block history continues
to deserialize.

Build/test status:
- go build ./... clean.
- go vet ./... clean (modulo 1 pre-existing sync/atomic copy warning
  in vms/rpcchainvm/zap/cevm_e2e_test.go unrelated to this rip).
- xvm txs/executor + xvm state + xvm network + xvm txs + xvm
  genesis + wallet/chain/p/builder + wallet/chain/x/builder ALL
  PASS under -short.
- vms/platformvm/txs serialization tests (TestAdd*Serialization)
  fail on byte-baseline expected outputs because utxo's new sort
  key is (AssetID, wireBytes()) vs the old codec-marshal bytes —
  the test fixtures need regen under the new canonical sort. Same
  story for xvm/state_test.go TestFundingAddresses / TestVerifyFxUsage
  using utxo.TestState whose Out type doesn't satisfy
  wireSerializable. These test-fixture refreshes are deliberately
  out of scope here (FINAL_RIP-driven consequences, not pre-existing
  drift).

go.mod: luxfi/utxo v0.3.2 -> v0.3.5, luxfi/api transitive v1.0.11
-> v1.0.12. luxfi/codec v1.1.4 stays direct (network/peer, warp
internal, indexer, keystore et al. still use codec.Manager for
non-UTXO concerns; those packages are out of UTXO-rip scope).

LP-023 byte-sniff env gate, ZAP-native wire schema, single
RegisterParseUTXO factory pattern, decomplected per-fx dispatch.
2026-06-03 13:14:11 -07:00
Hanzo AI 8716735960 warp: CoronaSignature → CoronaSignature (wire byte 0x02 preserved)
Final Corona-residue sweep in vms/platformvm/warp:

  - HybridBLSRTSignature → HybridBLSCoronaSignature (the lingering "RT"
    suffix in the Go type for the deprecated BLS+lattice hybrid).
  - ErrInvalidRTSignature → ErrInvalidCoronaSignature.
  - ErrMissingRTPublicKey → ErrMissingCoronaPublicKey.
  - String() format, comments, doc references updated to Corona.

Wire-format invariants (this is on-chain encoding):

  - linearcodec assigns typeIDs by RegisterType call order. The order
    in codec.go is UNCHANGED: BitSetSignature (0x00), CoronaSignature
    (0x01), EncryptedWarpPayload (0x02), HybridBLSCoronaSignature
    (0x03), then the 3 Teleport types (0x04-0x06).
  - linearcodec serializes struct fields by declaration order (see
    reflectcodec/struct_fielder.go). Field declaration order is
    UNCHANGED for every type in this PR.
  - Therefore: type names and field names are wire-metadata only;
    renaming them produces byte-equal output.

Wire bytes verified byte-equal pre/post rename for every codec-
registered type via the new regression test:

    vms/platformvm/warp/wire_baseline_test.go

That test pins the exact hex of every type's encoding (concrete and
through the Signature interface) and is now a permanent guard against
accidental wire-format drift.

Doc sweep (no code dependencies — example/aspirational JSON keys):

  - docs/architecture/consensus.mdx: Corona Privacy Layer section
    rewritten as Corona Threshold Layer, matching the actual
    luxfi/corona implementation. The previous text described a ring-
    signature scheme that doesn't exist in this codebase.
  - docs/architecture/overview.mdx: Q-Chain diagram + cryptographic
    primitives table.
  - docs/configuration/node-config.mdx + docs/getting-started/running.mdx:
    example Q-Chain config keys (corona-* → corona-*).
  - docker/Dockerfile.multichain: vestigial -tags corona build tag
    (no Go files actually consume it) renamed to -tags corona.

Verification:
  go build ./...           exit 0
  go vet ./vms/platformvm/warp/...    clean
  go test ./vms/platformvm/warp/...   all packages PASS (warp,
                                      message, payload, zwarp)
2026-06-03 13:01:16 -07:00
Hanzo AI a8c58da22a Dockerfile: bump EVM plugin v0.19.2 -> v0.19.3 (runtime v1.1.0 cascade)
v0.19.3 pulls vm v1.1.7 + runtime v1.1.0. runtime v1.1.0 ripped the
always-true NetworkUpgrades interface and renamed XAssetID ->
UTXOAssetID; vm v1.1.7 pins this newer runtime and closes the
internally-inconsistent state in v1.1.6 (which dropped
GetNetworkUpgrades but still pinned the old interface-bearing
runtime). Builds cleanly now.
2026-06-03 12:49:30 -07:00
Hanzo AI 5d9d334957 Dockerfile: bump EVM plugin v0.19.1 -> v0.19.2 (vm v1.1.6 -- IsEtnaActivated rip)
v0.19.2 bumps luxfi/vm v1.1.5 -> v1.1.6 which drops the obsolete
GetNetworkUpgrades() method on the VMContext interface. Required by
the v0.19.0 Etna->Quasar rename: vm v1.1.5 still expected upgrade.Config
to implement runtime.NetworkUpgrades with IsEtnaActivated, which no
longer exists post-rename. v1.28.22 and v1.28.23 Docker builds failed
on this signature mismatch; v0.19.2 closes the cascade.
2026-06-03 12:41:22 -07:00
Hanzo AI 78b4233b1a Dockerfile: bump EVM plugin v0.19.0 → v0.19.1 (bls12381 7-sub-config Key() fix)
v0.19.1 bundles luxfi/precompile v0.5.27 → v0.5.35 (commit 794912f).
Each of the 7 EIP-2537 bls12381 sub-configs (G1/G2 × {Add, Mul, MSM} +
Pairing) now returns its own ConfigKey via Key(), fixing a collision
where all 7 mapped to G1AddConfigKey. Without the fix, only one of
the 7 entries could land in the upgrade registry — DisallowUnknownFields
parse (enforced as of evm 415b2a276) rejected the other six.

This unblocks re-adding the 7 bls12381*Config entries to mainnet
configs/mainnet/upgrade.json forward-dated to the already-past Quasar
horizon (Unix 1766708400 = 2025-12-25 16:20 PT). Testnet + devnet
upgrade.json already carry these entries (testnet at 1766708400,
devnet at 0).
2026-06-03 12:34:01 -07:00
Hanzo DevandGitHub 60c50a0e32 node: bump evm v0.19.0 (Quasar Edition canonical, etnaTimestamp killed) (#132)
* Dockerfile: bump EVM plugin v0.18.18 → v0.19.0 (Quasar Edition)

Quasar Edition rip — one and only one upgrade-key namespace:
 - luxfi/evm v0.19.0 renames EtnaTimestamp → QuasarTimestamp
   (Go field + json tag)
 - Strict json.Decoder DisallowUnknownFields on upgradeBytes parse
   in plugin/evm/vm.go — any stale etnaTimestamp in a deployed
   upgrade.json now fails parse loudly at boot rather than silently
   leaving the fork field nil and disabling activation.

Pairs with: luxfi/genesis kill-etna sweep (configs/*/upgrade.json
and brand L1 EVM genesis.json scrubbed).

* docker-entrypoint: rip etnaTimestamp from embedded C-Chain genesis

With luxfi/evm v0.19.0 strict upgradeBytes decode, the entrypoint's
embedded C-Chain genesis heredoc would parse-fail on etnaTimestamp.
One name. quasarTimestamp.
2026-06-03 12:27:45 -07:00
Hanzo AI e40ac9d41f #189: LP-023 batch 5 v3.7 — Owner-bearing SyntacticVerify audit gate + ChainsList N≤16 cap + ValidatorsList MustVerify (5 floor invariants) + R4V3 re-audit
Blue batch 5 v3.7 closes the 5 target gaps from Red round 5 follow-up:

1. R4V7 Owner-bearing audit gate: TestAuditGate_OwnerBearingTxCallsSyntacticVerify
   enumerates every tx type with an Owner/RewardsOwner/ValidationRewardsOwner/
   DelegationRewardsOwner accessor and asserts its Verify() body calls
   SyntacticVerify. CI mirror: owner-bearing-syntacticverify-gate job
   in .github/workflows/zap-audit.yml. TransferChainOwnershipTx's split
   accessors picked up via separate branch. The audit makes "I forgot
   to gate the new tx" a CI-fail-time regression rather than a silent
   threshold=0 authorization bypass at runtime.

2. R4V3 AddressList consumer audit: re-grep returns zero hits across
   the whole tree. Documented inline at tx_verify.go header with the
   reproducer grep. No regressions since batch 5 v3.5.

3. ChainsList MaxChainsPerL1 = 16: hard cap enforced in
   ChainsListView.MustVerify. New ErrTooManyChains typed error.
   Matches the multi-chain L1 spawn use case (P + X + EVM + small
   application stack) plus headroom; prevents a hostile encoder
   from forcing the executor through quadratic walk at admission.
   Coverage: TestChainsList_MustVerify_RejectsOverCap +
   TestChainsList_MustVerify_AcceptsAtCap.

4. ValidatorsList MustVerify (5 floor invariants):
   - Len() <= MaxValidatorsPerL1 (1024): cap matches practical
     upper bound on initial-validator sets.
   - Weight > 0: lifts the per-validator gate from tx_verify.go
     onto the list-level MustVerify so it's grep-able and pure
     orchestration on the tx side.
   - BLSPubKey not all-zero: structural floor before R6V3 pairing.
   - BLSPoP not all-zero: structural floor before R6V3 pairing.
   - RegistrationExpiry > 0: parallel of ErrZeroExpiry on
     RegisterL1ValidatorTx (unix timestamp can never be zero).
   New typed errors: ErrTooManyValidators, ErrValidatorBLSPubKeyZero,
   ErrValidatorBLSPoPZero, ErrValidatorRegistrationExpiryZero. Wired
   into CreateSovereignL1Tx.Verify and ConvertNetworkToL1Tx.Verify
   before the expensive BLS pairing walk so cheap structural floor
   fires first. New audit gate (test + CI):
   TestAuditGate_ValidatorsListEmbeddersCallMustVerify + workflow
   job validatorslist-mustverify-gate. Coverage: 6 new MustVerify
   tests (happy path + 5 floor-violation rejections + over-cap).

5. Bench refresh -benchtime=2s (M1 Max):
   - Parse geomean (n=9): 7.50× vs v3.6 7.74× — within 4% noise.
   - Build geomean (n=9): 1.03× vs v3.6 1.12× — within 9% noise.
   - Allocs/op unchanged: Parse 1/24, Build 2 (down from 4-7).
   - No regression > 5%. v3.7 changes fire entirely on the Verify()
     path; Parse and Build are structurally untouched.
   Full numbers in vms/platformvm/txs/bench_results/RESULTS.md.

All 4 audit gates pass locally:
  - TestAuditGate_AddressListNoProductionConsumers
  - TestAuditGate_ChainsListEmbeddersCallMustVerify
  - TestAuditGate_ValidatorsListEmbeddersCallMustVerify (NEW)
  - TestAuditGate_OwnerBearingTxCallsSyntacticVerify (NEW)

Full zap_native test suite: 0.715s, all green.
2026-06-02 22:14:52 -07:00
Hanzo AI b6958135af wallet/chain/p: LUX_WALLET_UTXO_ASSET_ID_OVERRIDE for legacy LUX asset on mainnet
Adds an opt-in escape hatch that pins the fee-payment asset to a
specific 32-byte asset ID, bypassing platform.getStakingAssetID. Use on
networks where the live staking asset differs from the legacy LUX asset
on existing P-chain UTXOs.

lux-mainnet today is the documented case: staking ==
pmSJ7BfZQfwtUGbamLWSLFLFGnocfMfbriDTsYWxi4qnqFrrT but the historical
deployer's UTXOs hold HrJCm4yvNmyPDA1PEqwks9iFFmoRJEsLJj36N1xtkffrqpL6p.
Without the override the wallet's `utxoAssetID` matches the staking-asset
result and `platform.getBalance` returns 0 for the legacy UTXOs — the
wallet can't find them as fee-payment material.

NOTE: this addresses the wallet-side identification only. The P-chain
fee-execution rule still requires the staking asset on mainnet; a
follow-up legacy-to-staking asset migration tx is needed before
CreateNetworkTx + CreateChainTx will go through on mainnet. Tracked as
the 2026-06-03 mainnet brand L2 re-fire blocker.

Empty / unset is a no-op. Tested via bootstrap-chain mainnet dry-run
(2026-06-03): override correctly switches the wallet to legacy-LUX
UTXOs; CreateNetworkTx then fails with "insufficient unlocked funds"
because the fee verifier checks pmSJ7BfZ... balance — which is the
expected second-half blocker.
2026-06-02 20:29:51 -07:00
Hanzo AI 94f7a06503 platformvm/txs/bench: LP-023 always-on — fix disable_legacy_test underflow
ZAPActivationUnix is now 0 (LP-023 cutover, 2026-06-02). The assertion
`ShouldUseZAPForWrite(ZAPActivationUnix - 1)` underflows uint64 to
math.MaxUint64; with ZAPActivationUnix=0 the LegacyEnabled path
degenerates to `ts >= 0` which is true for every input, so the prior
"pre-activation picks legacy" assertion can never hold.

Rewrite runEnableLegacyChild to mirror zap_native.security_test.V15
closure: probe a timestamp spread including the historical
forward-date (1782604800) and assert ShouldUseZAPForWrite=true
unconditionally. Pins the always-on invariant on the bench surface
too.

No production code change.
2026-06-02 19:59:17 -07:00
Hanzo AI 5e487305d8 #189: LP-023 batch 5 v3.6 — R7V5 mempool admission gate for zap_native (Path A) + R7V7 Verify() for RegisterL1ValidatorTx + ConvertNetworkToL1Tx (HIGH) + R7V8 MustVerify rename + CI gate (MEDIUM)
Red round 7 closes the activation gap so Neo's ZAPActivationUnix=0 image can
land without zombie txs from not-yet-implemented zap_native executors.

R7V5 (HIGH, mempool admission gate — Path A)
  - vms/platformvm/network/zap_native_admission.go: new file. Wraps
    TxVerifier with NewZapNativeAdmissionGate; refuses
    *txs.CreateSovereignL1Tx (legacy struct dispatch hits stub at
    standard_tx_executor.go:636) AND any ZAP-magic wire buffer whose
    kind is CreateSovereignL1 (23), RegisterL1Validator (7), or
    ConvertNetworkToL1 (22).
  - vms/platformvm/network/network.go: New() now wraps the supplied
    TxVerifier with the gate before installing into gossipMempool, so
    every inbound tx routes through the gate first.
  - Typed error ErrZapNativeNotYetExecutable for errors.Is matching.
  - REMOVAL CHECKLIST documented inline so a future Blue knows to
    delete the gate entry when an executor body lands.
  - Tests in zap_native_admission_test.go: rejects legacy
    CreateSovereignL1Tx; passes through legacy BaseTx /
    RegisterL1ValidatorTx / ConvertNetworkToL1Tx (the working ones);
    rejects ZAP-wire CreateSovereignL1 / RegisterL1Validator /
    ConvertNetworkToL1; non-ZAP bytes pass; nil-safety.

R7V7 (HIGH, Verify() for two missing tx types)
  - RegisterL1ValidatorTx.Verify(): BLS PoP pairing + zero-Expiry gate
    (ErrZeroExpiry). RemainingBalanceOwnerID treated as optional v3
    placeholder.
  - ConvertNetworkToL1Tx.Verify(): same per-validator walk as
    CreateSovereignL1Tx (non-empty Validators, Weight>0, BLS PoP
    pairing).
  - Tests in r7v7_register_convert_verify_test.go: rejects bad BLS
    PoP / malformed BLS pubkey / zero Expiry / zero Validators / zero
    Weight; accepts valid; adversarial wire-buffer tampering for both
    tx types (zero out Expiry / Weight in-place and re-Wrap).

R7V8 (MEDIUM, MustVerify rename + CI gate)
  - chains_list.go: ChainsListView.Verify renamed to MustVerify so
    the per-list gate is grep-able from CI. The previous Verify()
    name collided with the tx-level Verify() convention.
  - tx_verify.go: CreateSovereignL1Tx.Verify call site updated.
  - r6_verify_test.go: TestChainsListView_Verify_StandaloneEntries
    renamed + updated to use .MustVerify().
  - audit_test.go: new TestAuditGate_ChainsListEmbeddersCallMustVerify
    enumerates tx types with a Chains() accessor returning ChainsList
    and confirms tx_verify.go has a corresponding Verify() body that
    calls .MustVerify().
  - .github/workflows/zap-audit.yml: new chainslist-verify-gate job
    mirroring the local audit_test.go invariant.

Test results (GOWORK=off, race enabled):
  ./vms/platformvm/network/...     PASS  (8 new + all existing)
  ./vms/platformvm/txs/zap_native/ PASS  (8 new + all existing)
  ./vms/platformvm/txs/executor/   PASS
  ./vms/platformvm/txs/             PASS
  ./vms/platformvm/block/...        PASS
2026-06-02 19:28:40 -07:00
Hanzo AI 0c6a7e6914 #189: LP-023 batch 5 v3.5 — RESERVED bytes zero-gate (R6-4) + AddressList CI audit gate (R6-6) + cross-blob aliasing documented contract (R6-2 design decision)
R6-4 (RESERVED zero-gate)
  - ChainEntry RESERVED bytes [56..64) gated by ChainsListView.Verify.
  - New OffsetChainEntry_Reserved=56 constant + ErrReservedNonZero typed err.
  - Writer already zero-pads; gate prevents adversary smuggling state inside
    what consensus considers empty. Pins the upgrade-safe invariant before
    any v4 parser attaches meaning to those bytes (no silent wire-fork).
  - Tests: TestChainsList_Verify_RejectsNonZeroReserved (per-byte sweep of
    all 8 reserved bytes, each flipped individually -> all reject),
    TestChainsList_Verify_RejectsNonZeroReserved_TopByte (top byte 0x01).

R6-6 (AddressList.At CI audit gate)
  - .github/workflows/zap-audit.yml: grep-based gate on PR + push to
    main/dev. Fails if any new production caller of AddressList.At()
    appears outside the allowlist (_test.go, tx_verify.go, owner.go,
    audit_test.go).
  - vms/platformvm/txs/zap_native/audit_test.go: local mirror so
    `go test ./vms/platformvm/txs/zap_native/` reproduces CI behavior.
    Verified locally: clean -> PASS; injected violator -> trips correctly.

R6-2 (cross-blob aliasing design decision)
  - Documented-allowance path: aliasing is ALLOWED. Wire layer must not
    reject overlapping (rel, len) ranges; identity is (VMID, BlockchainID),
    not Name() bytes.
  - Full CONTRACT block on ChainsListView: (1) Name/FxIDs/GenesisData
    return payload slices not identity, (2) chain identity is VMID +
    BlockchainID, (3) consumers MUST NOT use returned bytes as dedup keys,
    (4) returned slices are read-only.
  - Forward path documented: if future feature requires non-overlap, add
    ChainsListView.VerifyNonOverlappingRanges() and wire it from tx.Verify.
  - Test: TestChainsList_AllowsOverlappingRanges_DocumentedContract -
    patches chain[1].NameRel to alias chain[0]'s slice, proves Verify
    accepts AND Name(0) == Name(1) byte-equal, AND VMID(0) != VMID(1).

go test -race ./vms/platformvm/txs/zap_native/...: ok 1.4s
Builds on top of v3 commit 35c03c9a32.
2026-06-02 19:11:48 -07:00
Hanzo AI 35c03c9a32 #189: LP-023 batch 5 v3 — wire Verify() gate for R6V4 (CreateSovereignL1Tx zero-validator/zero-chain CRITICAL) + R6V8 (TransferChainOwnershipTx HIGH) + R6V3 (BLS PoP verification HIGH) + R6V5 (FxIDs malformed length MEDIUM)
R6V4 (CRITICAL): CreateSovereignL1Tx.Verify now rejects zero-validator and
zero-chain wire buffers (both halt consensus at activation). Per-validator
Weight > 0 walk also fires here. RegistrationExpiry remains an executor
clock concern (deferred to staking handler, documented).

R6V8 (HIGH): TransferChainOwnershipTx had no Verify() despite carrying
Owner fields. Wired in: reconstructs OwnerStub from the (threshold,
locktime, address) tuple and calls SyntacticVerify. Tx count with
SyntacticVerify wired into 8 tx types (was 7).

R6V3 (HIGH): BLS PoP now verified at the SyntacticVerify boundary for
every initial validator. Wire-layer opaque 48B BLSPubKey + 96B BLSPoP
now pairing-checked via bls.VerifyProofOfPossession with new ErrBadBLSPoP.
Closes the zero-downstream-consumer gap Red grep found.

R6V5 (MEDIUM): ChainsListView.Verify walks entries and rejects any
FxIDsLen that is not a multiple of FxIDSize. Replaces the silent-nil
return path in BoundChainEntry.FxIDs with a typed error. Wired into
CreateSovereignL1Tx.Verify via ChainsListView.Verify().

Tests (all under go test -race):
  TestCreateSovereignL1Tx_Verify_RejectsZeroValidators
  TestCreateSovereignL1Tx_Verify_RejectsZeroChains
  TestCreateSovereignL1Tx_Verify_RejectsZeroWeight
  TestCreateSovereignL1Tx_Verify_RejectsBadBLSPoP
  TestCreateSovereignL1Tx_Verify_RejectsBadBLSPoP_MismatchedPubKey
  TestCreateSovereignL1Tx_Verify_AdversarialWireBuffer_ValidatorWeight
  TestChainsList_Verify_RejectsBadFxIDsLen (adversarial wire buffer)
  TestChainsListView_Verify_StandaloneEntries
  TestTransferChainOwnershipTx_Verify_RejectsZeroThreshold
  TestTransferChainOwnershipTx_Verify_RejectsThresholdAboveOne
  TestTransferChainOwnershipTx_Verify_AdversarialWireBuffer
  TestBLSSurfaceReachable

Updates TestVerify_AcceptsWellFormed/CreateSovereignL1 to supply a
properly-constructed validator (real BLS PoP) and a chain entry so the
new gates fire green on the legitimate path. Adds
TestVerify_AcceptsWellFormed/TransferChainOwnership subtest for R6V8.

New typed errors:
  ErrZeroValidators, ErrZeroChains, ErrValidatorWeightZero,
  ErrBadBLSPoP, ErrMalformedFxIDsLen.

Bench unchanged (Verify is on executor path, parse benches structurally
untouched). Full zap_native suite green under -race.

LP-023 batch 5 v3 closes Red round 6 V3/V4/V5/V8.
2026-06-02 19:04:07 -07:00
Hanzo AI f27f48e248 platformvm/txs/zap_native: LP-023 ZAP-native activation cutover — ZAPActivationUnix=0 (always-on)
ZAP wire is now mandatory from genesis. The forward-dated 2026-07-01
(1782604800) cutover is dead — replaced with always-on semantics. New
deployments, fresh syncs, and all production binaries from v1.28.19
onward are ZAP-only by construction.

The legacy linearcodec is reachable only via the explicit dev knob
LUXD_ENABLE_LEGACY_CODEC=1, and only for the read path (decoding
pre-2026-06-02 archival linearcodec bytes from disk). On the write
path, LegacyEnabled has no semantic effect once activation = 0 — the
"pre-activation" window is empty, so every block timestamp satisfies
blockTimestamp >= 0 and writes are always ZAP.

Tests:
- TestZAPActivationUnixIsAlwaysOn pins the constant to 0 so any
  regression that re-introduces a forward-date guard fails this gate.
- TestShouldUseZAPForWrite legacy-enabled subtest asserts ZAP for
  every timestamp (replaces the previous pre/at/post-activation
  table that depended on a non-zero gate).
- TestRed_V15_PreActivationZAPTxRejection updated to reflect that
  V15's threat model (legacy cutover-window fork) is no longer
  applicable; the test now asserts the always-on invariant.

Authorized by 2026-06-02 destructive recovery sweep — "just do it
right; no half-measures, no legacy compatibility, clean slate."
2026-06-02 19:00:18 -07:00
Hanzo AI 86167a8cbd platformvm/txs/zap_native: LP-023 batch 5 Phase C+D+E — ChainsList + ValidatorsList + bench refresh
Phase C — Multi-chain CreateSovereignL1 ChainsList primitive (chains_list.go,
chains_list_test.go):

  * 64-byte fixed-stride ChainEntry per chain: (NameRel/Len uint32, VMID 32B,
    FxIDsRel/Len uint32, GenesisDataRel/Len uint32, Reserved 8B).
  * Three sibling blob arrays (NameBlobs, FxIDsBlobs, GenesisDataBlobs)
    carried as parent-tx fields; entry header stores (rel, len) cursors.
  * Bind(nameBlobs, fxIDsBlobs, genesisDataBlobs) compile-time gate
    mirroring BoundEvidenceList (NEW-V2): ChainsListView lacks safe
    Name/FxIDs/GenesisData accessors so consumers cannot bypass Bind() by
    accident. Only BoundChainEntry exposes the safe accessors.
  * IsNull() zero-value guard on ChainEntry: out-of-range At(i) returns
    a ChainEntry{} whose accessors short-circuit to zero rather than
    nil-deref the underlying zap.Object's msg. Defense-in-depth atop the
    defensive At(i) clamp.
  * Per-element FxIDs blob is a length-prefixed byte array; entry count =
    FxIDsLen / FxIDSize (32B per ids.ID). Non-multiple lengths clamp to nil.
  * CreateSovereignL1Tx grows from a single-chain stub (TxKind 23, batch 4)
    to a real multi-chain L1 builder: ChainsList + NameBlobs + FxIDsBlobs +
    GenesisDataBlobs fields. Size 193 bytes (was 121).

Phase D — ValidatorsList encoding (validators_list.go,
validators_list_test.go):

  * 180-byte fixed-stride ValidatorRecord per initial validator: NodeID 20B
    + Weight uint64 + BLSPubKey 48B + BLSPoP 96B + RegistrationExpiry uint64.
  * Uses zap.Object.ListStride from v0.7.2 — per-element clamp against
    poisoned wire length fields (R4V9).
  * No sibling arrays needed — every field is fixed-size.
  * BLSPubKey() and BLSPoP() return COPIED slices (not aliased to parent
    buffer) so consumer mutation cannot corrupt subsequent reads.
  * IsNull() zero-value guard mirroring ChainEntry.
  * ConvertNetworkToL1Tx now carries the real ValidatorsList — the
    previous (0, 0) stub is gone. Size still 165 bytes (the validators
    list pointer was already provisioned).
  * CreateSovereignL1Tx integrates ValidatorsList alongside ChainsList for
    the full atomic sovereign-L1 commit shape.

Phase E — Bench refresh:

  * M1 Max Parse geomean across 10 measurable tx types post-batch-5:
    7.44× at -benchtime=2s -count=3 (medians).
  * Restricted to the original 9 tx types from the v0.7.2 baseline:
    7.74×, within noise of the 7.71× pre-batch-5 number.
  * R4V7 SyntacticVerify lives on the executor Verify() path (not the
    wire parser), so the parse benchmarks are structurally unchanged.
  * ChainsList + ValidatorsList add new primitives but do not appear in
    the legacy comparison fixtures.

Test coverage:

  * chains_list_test.go: RoundTrip (3 chains incl. empty FxIDs+empty
    GenesisData), EmptyList, OutOfRange, UnboundReturnsRawCursors,
    BindMismatchedBlobsClampsSafely, FxIDsNonMultipleClampsToNil.
  * validators_list_test.go: RoundTrip (2 validators), EmptyList,
    OutOfRange, PoisonedLengthClampedByStride,
    BLSFieldsAreReadOnly (mutation isolation),
    CreateSovereignL1Integration (ChainsList + ValidatorsList together).
  * batch4_tx_test.go: updated CreateSovereignL1TxRoundTrip for the new
    multi-chain + validators shape; tests both round-trip and Wrap()
    re-parse correctness.

All zap_native tests pass under go test -race.

Coordinates with cryptographer #114 v3 work and CTO bootstrap-chain work;
neither genesis/ nor bootstrap touched.

LP-023 batch 5 Phase C+D+E.
2026-06-02 18:47:37 -07:00
Hanzo AI 4e3c858b0e platformvm/txs/zap_native: close R4V3 AddressList honest-overcount gap (LP-023 batch 5 Phase B)
R4V3 finding: a malicious wire-encoded AddressList may report Len() >
actual entry count (the ListStride per-element clamp accepts the honest
overcount as long as length*stride fits the remaining buffer). At(i)
for i >= actual_count returns whatever bytes occupy the post-list
region in the buffer — often zero-padding, sometimes adjacent buffer
content.

Audit result: AddressList has ZERO production consumers in the
executor today (the executor still uses secp256k1fx.OutputOwners +
message.PChainOwner via the legacy codec). The new primitive is
shipping ahead of the executor migration, so the discipline is
forward-looking. Audited grep `\.At(|AddressList` across
~/work/lux/node — every match is internal to the zap_native package
or test code.

Defense path (canonical):
  - Owner.SyntacticVerify() now walks the list and rejects any zero
    ShortID with ErrOwnerAddrZero. This closes the zero-phantom
    bypass at the gate.
  - Documented consumer-safety contract on AddressList type docstring
    AND on .Len()/.At() — three paths: non-zero check at call site,
    sibling-count correlation, or canonical SyntacticVerify boundary
    (path 3 is the one-and-only-one way).

Test scope:
  - r4v3_addresslist_test.go — overcount construction with the wire
    layer (claim 5, real 4, 1-phantom); confirms SyntacticVerify
    rejects the zero-phantom case and accepts the buffer-garbage
    case (signature validation downstream closes the remaining
    surface in the garbage path — no zero co-signer can sneak
    through the quorum gate).
  - Honest-list happy path also pinned.

Tradeoffs:
  - SyntacticVerify is now O(Len()) per Owner instead of O(1). For
    typical owners (1-5 addresses) this is negligible; the gate is
    the authorization boundary and bears the cost.
2026-06-02 16:23:05 -07:00
Hanzo AI 9bb87c9c26 platformvm/txs/zap_native: R4V7 Owner SyntacticVerify + per-tx Verify (LP-023 batch 5 Phase A)
Wire layer (zap_native parser) is permissive by design — it confirms
TxKind + buffer geometry only. Executor-side semantic gates live HERE
on the consumer-side boundary.

Owner.SyntacticVerify enforces:
  - ErrOwnerAddrsEmpty: Addresses.Len() == 0 (signer set undefined)
  - ErrOwnerThresholdZero: threshold == 0 (auth bypass)
  - ErrOwnerThresholdExceedsAddrs: threshold > Addresses.Len()
    (unsatisfiable quorum)

OwnerStub.SyntacticVerify enforces the single-address fast path:
  - Threshold must be exactly 1 (zero is bypass, >1 is unsatisfiable
    because the stub carries one address by construction)

Per-tx Verify() entry points wire the gate into all 7 tx types that
embed Owner-shaped fields:
  - AddValidatorTx.RewardsOwner
  - AddDelegatorTx.DelegationRewardsOwner
  - AddPermissionlessValidatorTx.{Validation,Delegation}RewardsOwner
  - AddPermissionlessDelegatorTx.DelegationRewardsOwner
  - CreateChainTx.Owner
  - CreateNetworkTx.Owner
  - CreateSovereignL1Tx.Owner

Test scope (TDD red→green):
  - owner_syntactic_test.go — 7 cases on Owner + OwnerStub directly
  - tx_verify_test.go — 7 well-formed + 7 malicious-threshold + 1
    multi-owner accept + 1 adversarial-wire-buffer test that overwrites
    the threshold byte in the buffer and re-Wrap, confirming the gate
    fires on byte-stream attacks (not only constructor input).

Contract (LP-023 Red round 4 R4V7): every tx executor's Verify() entry
point MUST call tx.Verify() before treating embedded Owner fields as
authoritative. Skipping Verify() opens the auth-bypass attack vector.
2026-06-02 16:19:05 -07:00
Hanzo AI 9745896c57 platformvm/txs/zap_native: close LP-023 Red round 4 — R4V9 + R4V12 + R4V14
Three surgical fixes against Red round 4 findings (task #189).

R4V9 (MEDIUM) — migrate 6 list accessors from Object.List() to
Object.ListStride() for the per-stride poisoned-length clamp added in
zap v0.7.2. Before the migration, a wire length field that satisfied
the permissive `length <= len(buf)` baseline (bare List accepts) but
failed the tighter `length * stride > bufRem` bound would slip past
the accessor and return a List view with Len() == poisoned_length,
opening per-element OOB reads on stride-> 1 entries (silent zeros via
Object.Uint{8,32,64}, but consumer iterates ghost entries). Migrated:

  - CredentialListView  → SizeCredential        (stride 16)
  - SignatureArrayView  → SigBlobSize           (stride 65)
  - InputListView       → SizeTransferableInput (stride 88)
  - SigIndicesArrayView → SizeSigIndex          (stride 4, new const)
  - OutputListView      → SizeTransferableOutput (stride 96)
  - NewEvidenceListView → SizeEvidenceEntry     (stride 48)

Also added defensive `i >= Len()` bounds checks to the four At()
methods that construct sub-Objects from list.Object() — without the
guard, At(0) on a clamped Len()=0 view would build a Credential /
TransferableInput / TransferableOutput / EvidenceEntry wrapping a
zero-value zap.Object (msg=nil) and panic on downstream field
reads. (SignatureArray.At and SigIndicesArray.At already had the
guard.)

New regression suite r4v9_liststride_test.go covers all 6 accessors
plus a false-positive guard verifying honest single-entry lists still
round-trip. Pattern follows existing TestNewV1_ListStrideTighterClamp
in zap: build honest list, overwrite wire length with poisoned value
chosen to satisfy bare clamp but fail stride clamp, parse, confirm
Len()==0 and At(0) is panic-safe.

R4V12 (MEDIUM) — rename `Subnet` to `Chain` in bench fixtures
all_types_bench_test.go: legacySlashValidatorTx + legacyRemoveChainValidatorTx
struct field + their 4 construction sites (lines 55, 68, 444, 476,
552, 584 in original). Bench-only legacy types used for parity
benchmarks; rename is audit-hygiene per the rebrand sweep (#187).
Grep gate `grep -rn "Subnet" ... | grep -v "//"` is now empty.

R4V14 (INFO) — fix docstring drift in add_chain_validator_tx.go: the
"Fixed-section layout (size 165 bytes)" comment said 165, but the
SizeAddChainValidatorTx constant correctly says 161. Updated comment
to match constant; verified by arithmetic (last field Chain @ 129 +
32 bytes = 161, not 165).

Verification:
  - go test -race -count=1 ./vms/platformvm/txs/zap_native/... → ok
  - 7 new R4V9 tests pass (6 poisoned-length regressions + 1 honest
    round-trip).
  - grep gate for R4V12 is empty.
  - Parse benchmarks (M1 Max, 200ms): geomean 7.70x (named-tx-only),
    within noise of pre-fix 7.71x baseline.

zap_native-side only — luxfi/zap requires no bump.
2026-06-02 16:10:01 -07:00
Hanzo AI 25d624c385 platformvm/txs/zap_native: LP-023 Phase 1 batch 4 — 8 tx types + Owner + BoundEvidenceList + FuzzWrapAllTxKinds
Red round 3 follow-ups and remaining tx surface:

BOUNDEVIDENCELIST (NEW-V2 compile-time enforcement):
- EvidenceListView (unbound wire view) → .Bind(mb,sb) → BoundEvidenceList
- BoundEvidenceList.At(i) returns BoundEvidenceEntry which exposes safe
  MessageA/B + SignatureA/B accessors. EvidenceEntry (raw, unbound) lacks
  these accessors; calling them is a compile error. Consumers cannot
  bypass Bind() by accident.
- Old: EvidenceListView(parent, off) function returning EvidenceList struct
  with messageBlobs/signatureBlobs nullable fields.
- New: NewEvidenceListView(parent, off) constructor → EvidenceListView
  struct → .Bind() → BoundEvidenceList struct with bound fields.
- Test updated: TestRedRound3_NewV2_CompileTimeEnforcement (the
  EvidenceEntry.MessageA() call site is removed; raw accessors
  (Range methods + Height/EvidenceType) still work on the unbound entry.

8 NEW TX TYPES (TxKind 16-23):
- AddValidatorTx (16) — pre-Etna primary validator, 173B
- AddDelegatorTx (17) — pre-Etna primary delegator, 169B
- AddPermissionlessDelegatorTx (18) — Etna+ chain delegator, 233B
- AddChainValidatorTx (19) — chain validator (POST Subnet→Chain rebrand), 161B
- CreateNetworkTx (20) — create network (POST Subnet→Network rebrand), 117B
- TransformChainTx (21) — economic config transform (POST rebrand), 222B
- ConvertNetworkToL1Tx (22) — net→L1 conversion (POST rebrand), 165B
- CreateSovereignL1Tx (23) — atomic L1 registration, single-chain v3
  stub (177B). Multi-chain L1s pending Chains list primitive.

Rebrand sweep: zero `Subnet` references in zap_native/ source. The post
sweep #187 names are baked into TxKind enum + tx struct names. Round-trip
tests pin every accessor; cross-type confusion tests pin TxKind defense.

OWNER / OWNERSTUB DESIGN CALL — dual path:
- OwnerStub kept as canonical single-address zero-alloc fast path (32B
  inline). Common case stays fast.
- Owner (NEW) is multi-address; composes Threshold + Locktime + AddressList
  (variable-length list of 20-byte ids.ShortID). Header 20B inline +
  variable address storage.
- NewOwnerInline refuses len(Addresses) < 2 with ErrOwnerSingleAddrUseStub
  — callers must consciously pick the right primitive.
- Hammock rationale: 99% of P-chain validator txs use 1 owner; List-backed
  Owner would add ~24B alloc to every tx for a multi-sig case that's rare.
  Type system makes the choice load-bearing.

FUZZWRAPALLTXKINDS (Red round 3 follow-up #3):
- For any byte slice, AT MOST ONE WrapXxxTx returns nil. All others MUST
  return ErrWrongTxKind / ErrWrongSchemaVersion / typed zap.Parse error.
- 23 wrapper tests in parallel; 1.4M+ execs clean on M1 Max with 15s
  fuzztime. No panics, no cross-type confusion, no untyped errors.

ZAP V0.7.2 DEPS:
- go get github.com/luxfi/zap@v0.7.2 — pulls Object.ListStride,
  List.Len SAFETY doc, FuzzParse.

RESULTS.md REFRESH (v0.7.2 M1 Max):
- Geomean Parse × = 7.71× (up from 7.33× v0.7.1; v0.7.2 ListStride
  accept-path cost is negligible — one mul + compare per List() call)
- All 9 ZAP-native types in v0.7.2 column; v0.7.1 M4 Max numbers
  retained side-by-side (no M4 access in this session)
- Build regression on M4 Max still tracked (v0.7.x follow-up to attack
  zap.Builder per-call overhead)

Tests:
- 11 new round-trip + cross-type tests under TestBatch4_*, TestOwner*,
  TestAddValidator/Delegator/PermissionlessDelegator/ChainValidatorTx*,
  TestCreate{Network,SovereignL1}Tx*, TestTransformChainTx*,
  TestConvertNetworkToL1Tx*
- All existing tests (incl. RED-HIGH-1/2/3, MEDIUM-1, V18) still green
2026-06-02 15:47:38 -07:00
Hanzo AI 23094ae9b0 platformvm/txs/zap_native: close LP-023 v3.1 Red round 2 wire gaps (RED-HIGH-1/2/3, RED-MEDIUM-1)
Bumps luxfi/zap to v0.7.1 which closes the underlying wire-layer gaps
(uncapped Object.List length, backward-pointer header aliasing, size=0
Parse), and reworks the zap_native package to:

 - Use zap.Version2 as the schema-version gate. Every Wrap*Tx now routes
   through parseAndCheckKind(b, want), which rejects any Version1 buffer
   with ErrWrongSchemaVersion before TxKind interpretation. Closes the
   v2-vs-v3 cross-schema confusion (RED-MEDIUM-1) where a v2-shaped
   BaseTx with NetworkID=11 had byte 0 == TxKindBaseFull == 0x0B.

 - Add EvidenceList.Bind(messageBlobs, signatureBlobs) → EvidenceList,
   and safe accessors EvidenceEntry.MessageA/B + SignatureA/B that clamp
   wire (Rel,Len) cursors against parent-blob length. Returns empty
   slice on poisoned cursors (RED-HIGH-3) instead of panicking on
   mb[rel:rel+len]. The raw *Range() accessors are now documented UNSAFE
   and kept only for internal/test use.

 - Add SigIndicesArray.Slice(start, count uint32) → []uint32 and
   SignatureArray.Slice(start, count uint32) → [][SigBlobSize]byte.
   Both methods were referenced in comments but missing; consumers
   indexing .At() in a loop with attacker-controlled start/count would
   have iterated 4G times on poisoned wire (RED-HIGH-3 follow-on).

 - Update batch3_test.go::TestEvidenceListRoundTrip to use the safe
   bound accessors (the prior `mb[mARel:mARel+mALen]` pattern is exactly
   the consumer-side panic surface Red demonstrated).

Regression tests added to security_test.go:
  TestRedRound2_HIGH3_EvidenceListSafeAccessorsClamp
  TestRedRound2_HIGH3_SigIndicesArraySliceClamp
  TestRedRound2_HIGH3_SignatureArraySliceClamp
  TestRedRound2_MEDIUM1_V2BufferRejectedAtSchemaGate
  TestRedRound2_MEDIUM1_HonestV2BuffersStillWork

Geomean Parse speedup vs legacy codec: 7.03× (was 7.09×; +0.5% noise).
2026-06-02 15:13:57 -07:00
Hanzo AI fb38e10cea platformvm/txs/bench_results: v3 multi-host bench results (M1 Max + M4 Max)
Schema v3 (TxKind discriminator) reproduces predicted v2→v3 lift within
noise: Parse geomean 7.11× on M1 Max (n=9 native types), 8.02× on M4 Max
— matches Red model (v2 8.39× → v3 7.09× projected). Per-type allocs 3.57×
fewer, bytes 6.89× smaller, host-stable.

AdvanceTime end-to-end via txs.Codec wrapper: 34.14× M1, 42.76× M4 — ratio
grows with chip speed (reflection tax is fixed-per-op).

Honest residual: Build is a regression on M4 Max for 6/9 types (geomean
0.86×). zap.Builder per-call overhead exposed on fast cores. M1 Max still
positive (1.12×). Tracked for luxfi/zap v0.7.0 (Blue v3.1 iteration);
Parse-side ship decision is independent.

Both hosts darwin/arm64 (M1 Max MacBookPro18,2 + M4 Max Mac16,5 / dbc
runner). Task spec called dbc "amd64 Linux" — corrected: it is darwin/arm64
Apple M4 Max. linux/amd64 numbers TBD until x86 box lands in ARC fleet.

Reproduce: GOWORK=off go test -bench='^Benchmark(Parse|Build)' -benchmem
-benchtime=500ms -count=3 -run='^$'
./vms/platformvm/txs/{bench,zap_native}/
2026-06-02 14:56:46 -07:00
Hanzo AI a48261c298 platformvm/txs/zap_native: 5 batch-3 tx types composing the new primitives (LP-023 Phase 1)
TxKindBaseFull                    = 11  → BaseTxFull
  TxKindAddPermissionlessValidator  = 12  → AddPermissionlessValidatorTx
  TxKindImport                      = 13  → ImportTx
  TxKindExport                      = 14  → ExportTx
  TxKindCreateChain                 = 15  → CreateChainTx

Sizes (fixed section, schema v3):
  BaseTxFull                     =  85
  ImportTx                       = 125
  ExportTx                       = 125
  AddPermissionlessValidatorTx   = 381
  CreateChainTx                  = 221

Design highlights:

- BaseTxFull is the real P-chain spending envelope (Outs + Ins +
  Credentials + 2 shared arrays + Memo). The batch-2 BaseTx
  (TxKindBase) remains as the minimal metadata envelope for places
  that need only {NetworkID, BlockchainID, Memo} without spending
  state. Both kinds are first-class; they are NOT alternatives.

- Import / Export use a SINGLE combined Ins (or Outs) list with a
  header marker (ImportedInsStart/Count or ExportedOutsStart/Count)
  identifying the cross-chain slice. This collapses what would have
  been two parallel sig-index arrays into one shared array — fewer
  pointer pairs, no rebase bookkeeping. The slice-based indexing is
  byte-identical for the imported/exported half because they
  reference the same shared array.

- AddPermissionlessValidatorTx carries two single-address Owner stubs
  (validation rewards, delegation rewards) inline in the fixed
  section. The same OwnerStub type underlies CreateChainTx.Owner.
  Multi-address Owner ships in batch 4 along with the AddressList
  primitive; current callers needing multi-addr flow through the
  legacy codec gate.

- CreateChainTx embeds a 32-byte WarpMessageHash (sha256 of the
  originating Warp commit; zero when the chain is being created
  directly without a cross-network commit). The Warp message BODY
  is NOT embedded — it lives in the signed Warp envelope outside
  this tx. Hash-only embedding keeps the unsigned-tx bytes stable
  and the Warp envelope separately verifiable.

All five tx types follow the v3 invariant: TxKind@0, Wrap* rejects
mismatched discriminator with ErrWrongTxKind. Cross-confusion test
expansion verifies the new tx types in pairwise rejection (7
additional scenarios cover the batch 1+2+3 surface).

Deferred to batch 4 (no new primitives needed): the remaining ~18
classical platformvm tx types (CreateNetwork, AddDelegator,
AddPermissionlessDelegator, etc.) reuse the existing batch-3
primitives; their landings are mechanical.

go test -race ./vms/platformvm/txs/zap_native/...
  ok 	github.com/luxfi/node/vms/platformvm/txs/zap_native	1.488s
2026-06-02 14:35:03 -07:00
Hanzo AI 205b4013d2 platformvm/txs/zap_native: batch 3 primitives — variable-length nested schemas (LP-023 Phase 1)
Five primitives. Each is fixed-stride at the entry level; variable
per-entry bytes/sub-list payloads live in shared sibling fields on
the parent tx, indexed by (start, count). This keeps List.At(i)
zero-allocation while supporting unbounded per-entry payload sizes.

  OutputList         stride  96  →  TransferableOutput
  InputList          stride  88  →  TransferableInput + shared SigIndicesArray
  CredentialList     stride  16  →  Credential        + shared SignatureArray
  WarpMessage        size    40  →  embedded {SourceNetwork, Payload}
  EvidenceList       stride  48  →  EvidenceEntry     + shared MessageBlobs/SignatureBlobs

Design choices:

- Stride is the entry-count semantics for SetList; the byte count from
  ListBuilder.Finish() is discarded. List.Object(i, stride) does the
  branch-free arithmetic.

- Variable per-entry data goes into a SIBLING field on the parent tx
  (not into the entry's stride): InputList → SigIndicesArray;
  CredentialList → SignatureArray; EvidenceList → MessageBlobs +
  SignatureBlobs. Each entry references its slice via (start, count).
  This pattern lets entries stay fixed-stride and accessors stay
  zero-allocation. Multi-input-shared signature arrays also enable
  signature deduplication when adjacent inputs share signers.

- Forward-only relOffsets per the F1 contract: SetBytes-backed payload
  fields (WarpMessage.Payload, EvidenceList parent's MessageBlobs/
  SignatureBlobs) flow through the F1-fixed zap.Object.Bytes which
  rejects negative bit-patterns.

- Read-only contracts on every []byte / by-value accessor are
  documented with the canonical defensive-copy idiom
  (append([]byte(nil), m...)) per AT1.

- Multi-address Owner is NOT yet supported at the wire layer; v3
  OutputList carries the single-address stub identical to
  TransferChainOwnershipTx. Multi-address callers still flow through
  the legacy codec gate behind LUXD_ENABLE_LEGACY_CODEC. The
  AddressList primitive ships in batch 4 (defer).

- Multi-signature credentials handled via the SignatureArray slice
  semantics — one Credential, N sigs. Post-quantum credentials
  (ML-DSA) are NOT YET on the v3 path; they keep flowing through
  legacy until their dedicated PQ Credential schema lands.

Round-trip tests (batch3_test.go) cover all five primitives + the
empty-list/null-pointer fallback path. go test passes; race-free.

Updated bench numbers for v3 batch-2 (the +1-byte TxKind tax):
  Parse geomean speedup vs legacy: 7.09× (was 8.39× pre-v3)
  Build geomean speedup vs legacy: 1.35× (build path was always
  closer to parity; structure-allocator dominates at small tx sizes)
2026-06-02 14:27:01 -07:00
Hanzo AI 017471e548 platformvm/txs/zap_native: schema v3 with TxKind discriminator (LP-023 Phase 1)
Phase A response to Red's batch-2 review:

F2 (MEDIUM, cross-type confusion) — schema-bump v2→v3. Every fixed
section now carries a TxKind uint8 at offset 0; every other field
shifts by +1 byte. Wrap*Tx reads TxKind first and returns
ErrWrongTxKind on mismatch. Constructors write the kind
unconditionally. Closes the gap where an AdvanceTimeTx buffer wrapped
as a BaseTx returned garbage-but-deterministic field reads.

  TxKind enum (dense, 0 reserved):
    1=AdvanceTime  2=RewardValidator  3=SetL1ValidatorWeight
    4=IncreaseL1ValidatorBalance  5=DisableL1Validator  6=Base
    7=RegisterL1Validator  8=SlashValidator
    9=TransferChainOwnership  10=RemoveChainValidator

  v3 sizes (each +1 vs v2 from the TxKind byte):
    AdvanceTimeTx              =  9
    RewardValidatorTx          = 33
    SetL1ValidatorWeightTx     = 49
    IncreaseL1ValidatorBalanceTx = 41
    DisableL1ValidatorTx       = 33
    BaseTx                     = 45
    RegisterL1ValidatorTx      = 217
    SlashValidatorTx           = 57
    TransferChainOwnershipTx   = 69  (no natural-alignment padding; reads are alignment-tolerant)
    RemoveChainValidatorTx     = 53

F1 (MEDIUM, memo malleability) — closed at the luxfi/zap wire layer
in v0.6.1 (negative relOffset rejected in Object.Bytes). Bumped node's
go.mod replace: zap v0.2.0 → v0.6.1. TestRed_V2 now confirms the
defense via the nil-Memo branch.

F4 (INFO, doc bugs) — RegisterL1ValidatorTx comment now correctly
declares size 217; TransferChainOwnershipTx now correctly declares
size 69. Both sizes flow from offset arithmetic — no magic numbers.

AT1 (accepted tradeoff, memo aliasing) — BaseTx.Memo() docstring
escalated: READ-ONLY contract + the canonical defensive-copy idiom
(append([]byte(nil), m...)). Same pattern documented for every
variable-length accessor going forward.

Brand cleanup: SlashValidatorTx.Subnet() → Network() and
RemoveChainValidatorTx.Subnet() → Network(). Zero `Subnet*` symbols
in batch-2 code path. Tests updated.

V14 security test now exhaustively covers cross-confusion: 10
pairings of {valid-tx-buf, wrong-Wrap} + reserved TxKind=0 — all
reject with ErrWrongTxKind. The other 19 Red vectors continue to
pass under v3.

go test -race ./vms/platformvm/txs/zap_native/...
  ok 	github.com/luxfi/node/vms/platformvm/txs/zap_native	1.446s

Coordinated with luxfi/zap@v0.6.1 (F1 fix).
2026-06-02 14:09:13 -07:00
Hanzo AI ed5bf5ccb3 wallet/network/primary: fail-soft FetchState when X-Chain disabled
Post-coreth networks (test+dev primary that bake only P + EVM genesis chains)
run in P-only mode where the X alias is not registered. The wallet's
FetchState now degrades the X-Chain context to a sentinel ids.Empty
BlockchainID instead of erroring, and skips the X-chain entry from the
chain-pair UTXO scan when X is unavailable.

Required to drive IssueCreateChainTx via the canonical primary wallet
against the fresh test+dev primaries (closes universe #168).
2026-06-02 13:48:46 -07:00
Hanzo AI 913690cedd platformvm/txs/zap_native: 5 more tx types (LP-023 Phase 1 batch 2)
Native ZAP encoding — no marshal step, struct IS wire format — for the
next batch of platformvm tx types. Same pattern as Phase 1 batch 1.

New types (v1 schemas; variable-length nested fields deferred to batch 3):
  base_tx.go                     — NetworkID + BlockchainID + Memo
                                   (Outs/Ins → batch 3)
  register_l1_validator_tx.go    — ValidationID + BLS pubkey 48B + PoP 96B
                                   + Expiry + RemainingBalanceOwnerID stub
                                   (Warp Message + full OutputOwners → batch 3)
  slash_validator_tx.go          — NodeID + Subnet + SlashPercentage
                                   (Evidence variable-length → batch 3)
  transfer_chain_ownership_tx.go — Chain + Owner{threshold,locktime,addr} v1 stub
                                   (full OutputOwners list → batch 3)
  remove_chain_validator_tx.go   — NodeID + Subnet
                                   (ChainAuth lives in signed wrapper)

Tests + benches extend the existing all_tx_types_test.go +
all_types_bench_test.go infra (no duplication). 18 new accessor zero-alloc
sites verified, including 48B BLSPublicKey and 96B PoP by-value returns.

Aggregate bench results (Apple M1 Max, Go 1.24, -benchtime=2s):

  Parse Legacy vs ZAP:
    BaseTx                      470.0  →  60.42 ns  ( 7.8x)  152→24 B  3→1 alloc
    RegisterL1ValidatorTx       1008   →  75.03 ns  (13.4x)  384→24 B  6→1 alloc
    SlashValidatorTx            690.6  →  82.74 ns  ( 8.3x)  176→24 B  4→1 alloc
    TransferChainOwnershipTx    2459   → 161.5  ns  (15.2x)  192→24 B  4→1 alloc
    RemoveChainValidatorTx      879.6  → 117.2  ns  ( 7.5x)  176→24 B  4→1 alloc

  Parse cross-type mean (10 types now native): 8.5x speedup.

  Build (one-time per proposer, dominated by Parse on consensus path):
    BaseTx       1.5x slower (memo SetBytes deferred-copy; acceptable trade)
    Register     1.03x       (allocs 7→2)
    Slash        1.4x faster (allocs 5→2)
    Transfer     0.96x       (allocs 5→2)
    Remove       1.3x faster (allocs 5→2)

All accessor reads zero-alloc. All builds 1 alloc on read path.

Verification:
  cd ~/work/lux/node
  GOWORK=off go build ./vms/platformvm/txs/zap_native/...
  GOWORK=off go test  ./vms/platformvm/txs/zap_native/...
  GOWORK=off go test -bench=. -benchmem -benchtime=2s \\
    ./vms/platformvm/txs/zap_native/...

Activation 1782604800 (2026-07-01 00:00 UTC) unchanged.
LUXD_ENABLE_LEGACY_CODEC=1 opts INTO legacy unchanged.

Refs LP-023. Next: batch 3 — variable-length nested-object schemas (Outs/Ins
lists, Warp Message payloads, full OutputOwners, Evidence) + remaining tx
types via codegen.
2026-06-02 13:34:12 -07:00
Hanzo AI e16350cfcd platformvm/txs/bench: Scientist comprehensive ZAP vs linearcodec harness + results (LP-023)
Real-workload benchmark suite produced by the scientist agent. Production-
realistic measurements on Apple M1 Max (Go 1.24).

Harness (8 files + results + reproduce docs):
  bench/fixtures.go           — realistic per-type tx fixtures
  bench/parse_bench_test.go    — per-type Parse: Legacy vs ZAP
  bench/build_bench_test.go    — per-type Build
  bench/field_bench_test.go    — field access (single + 1M batch)
  bench/workload_bench_test.go — 1000-tx mempool + 200-block parse + dispatcher
  bench/alloc_bench_test.go    — 5s sustained-parse GC pressure
  bench/disable_legacy_test.go — LUXD_ENABLE_LEGACY_CODEC gate verification
  bench/README.md              — reproduce + capture procedures
  bench/testdata/README.md     — captures notes
  bench_results/RESULTS.md     — full numbers + honest caveats + CPU profile

Headline numbers (vs linearcodec):
  Parse AdvanceTimeTx        : 37x faster (1940 ns -> 52.5 ns), 20x less mem
  Build AdvanceTimeTx        :  5.2x faster
  Sustained 5s parse loop    : 18.5x throughput (777k -> 14.4M parses/sec)
  Cross-type mean            :  5.6x parse,  1.6x build
  Allocations (parse, build) : always 3->1 and 4->2

CPU profile:
  Legacy: ~50% in reflectcodec.{marshal,unmarshal} reflection walk,
          ~30% in runtime.{madvise,kevent} from per-field alloc GC pressure
  ZAP:    elimination of both surfaces; offset arithmetic + 1 alloc per parse

Honest caveats (verbatim from scientist report):
  - Only 5/33 tx types have native paths today; mempool mix workload
    compresses to ~1.1x lift until BaseTx + AddPermissionless* native ship
  - Field access: single uint64 read is 2.1x slower (offset deref vs struct
    field), break-even at ~3,560 reads per parse; mainnet validators do
    ~10x per tx so ZAP still wins by orders of magnitude in the real regime
  - darwin/arm64 only — re-run on linux/amd64 before quoting production-
    binding numbers
  - LUXD_ENABLE_LEGACY_CODEC gate verified at zap_native surface (subprocess
    test passes); NOT yet wired into platformvm/txs.Parse — would break
    byte-preserving v0 read for validators bootstrapping pre-activation
    history. Gate enforcement lives at the future wire dispatcher.

Recommendation (CTO direction): continue the migration. Architecture works
as designed. Next priority: BaseTx + AddPermissionless* native paths — the
two highest-weight tx types in the modal mainnet mempool mix.

txs/tx.go is unmodified — byte-preserving v0->TxID migration invariant
preserved per existing codec.go CodecVersionV0/V1 framework.

Reproduce: cd ~/work/lux/node && GOWORK=off go test -bench=. -benchmem ./vms/platformvm/txs/bench/...
2026-06-02 12:52:23 -07:00
Hanzo AI f2ec4f2e25 platformvm/txs/zap_native: 4 more tx types + aggregate benchmarks (LP-023 Phase 1 batch 1)
Adds native ZAP encoding for the L1-management tx types — same pattern as
AdvanceTimeTx canary. No marshal step, zero-copy accessors, zero-alloc
field reads.

Types added:
  RewardValidatorTx              — TxID (32 bytes)
  SetL1ValidatorWeightTx         — ValidationID + Nonce + Weight (48 bytes)
  IncreaseL1ValidatorBalanceTx   — ValidationID + Balance (40 bytes)
  DisableL1ValidatorTx           — ValidationID (32 bytes)

Tests:
  - Round-trip parity per type (build → wrap → equal)
  - All accessors zero-alloc (AllocsPerRun = 0)
  - Wire-format discrimination via IsZAPBytes magic

Benchmarks (Apple M1 Max, Go 1.24, real linearcodec reflection path vs ZAP):

  Tx Type                       | Parse Legacy | Parse ZAP | Speedup
  ------------------------------|--------------|-----------|--------
  AdvanceTimeTx                 |  216.9 ns/op | 38.92 ns  |  5.6x
  RewardValidatorTx             |  218.5 ns/op | 35.80 ns  |  6.1x
  SetL1ValidatorWeightTx        |  234.3 ns/op | 25.76 ns  |  9.1x
  IncreaseL1ValidatorBalanceTx  |  245.4 ns/op | 25.55 ns  |  9.6x
  DisableL1ValidatorTx          |  218.5 ns/op | 54.44 ns  |  4.0x

  Allocations: legacy 3 allocs / 120-136 B; ZAP 1 alloc / 24 B.
  4x reduction in allocs/op, ~5x reduction in B/op across the board.

  Build side: roughly even (both paths allocate a buffer); the win is on
  parse, which dominates real workloads (every validator parses every tx in
  every block).

Phase 1 batch 1 of 4. 5 types down, ~28 to go. Same pattern for each: schema
constants + Wrap + Builder + Test + Bench. Pattern is production-ready;
remaining types (BaseTx, ImportTx, ExportTx, CreateChainTx, validator
variants) follow the same shape with progressively more nested sub-objects
for Inputs/Outputs/Credentials.

Reproduce:
  cd ~/work/lux/node
  GOWORK=off go test -bench=. -benchmem ./vms/platformvm/txs/zap_native/
2026-06-02 12:43:01 -07:00
Hanzo AI 653e9b2b7f platformvm/txs/zap_native: invert env var — native ZAP default, legacy is opt-in
LUXD_DISABLE_LEGACY_CODEC → LUXD_ENABLE_LEGACY_CODEC (per user 2026-06-02
'should be LUXD_ENABLE_LEGACY_CODE=1 to turn it on').

Default: native ZAP for every read + write. codec.Codec.Marshal/Unmarshal
gone from hot path. Fresh deployments + post-activation production get a
smaller, faster binary with no legacy code reachable.

Operators with pre-activation history that needs reading set
LUXD_ENABLE_LEGACY_CODEC=1 to opt in to backward-compat. Without it, legacy
bytes return ErrLegacyCodecDisabled.

Tests updated for the inverted default; pass clean.
2026-06-02 12:39:13 -07:00
Hanzo AI 04a461ca0a platformvm/txs/zap_native: canary AdvanceTimeTx native-ZAP encoding (LP-023)
No marshal. No codec.Codec interface. The struct wraps the ZAP buffer
literally. tx.Bytes() returns the buffer. WrapAdvanceTimeTx(b) wraps b in
a typed accessor. Both are zero-copy.

Architecture per LP-023:
- ZAPActivationUnix = 1782604800 (2026-07-01 00:00 UTC)
- LUXD_DISABLE_LEGACY_CODEC=1 → legacy linearcodec returns ErrLegacyCodecDisabled
- IsZAPBytes(b) discriminates ZAP magic "ZAP\\x00" from linearcodec V0/V1
- ShouldUseZAPForWrite(blockTimestamp) gates new writes

Real benchmark numbers on Apple M1 Max (Go 1.24):

  Parse   linearcodec 129.4 ns/op  72 B 2 allocs
          zap         36.22 ns/op  24 B 1 alloc   → 3.57x faster
  Build   linearcodec 164.3 ns/op  96 B 4 allocs
          zap         70.55 ns/op  72 B 2 allocs  → 2.33x faster
  Field   linearcodec  0.34 ns/op  (struct deref baseline)
          zap          0.75 ns/op  (offset Uint64) — sub-ns, effectively free

Tests:
  TestAdvanceTimeTxRoundTrip    — build → parse → equal
  TestAdvanceTimeTxZeroAlloc    — Time() accessor 0 allocs/run
  TestAdvanceTimeTxBytesReusable — &Bytes()[0] stable
  TestIsZAPBytes
  TestShouldUseZAPForWrite

This is Phase 0 of the LP-023 migration. Phase 1: schemas + accessors for
the remaining 32 platformvm tx types using the same pattern. Phase 2:
replace codec.Codec.{Marshal,Unmarshal} call-sites in platformvm/{txs,
mempool,block,state,executor}. Phase 3: validator coordination + activation.
Phase 4: post-activation legacy code removal.

Reproduce: GOWORK=off go test -bench=. -benchmem ./vms/platformvm/txs/zap_native/
2026-06-02 12:34:56 -07:00
Hanzo AI 5d9e6637f5 Dockerfile: bump EVM_VERSION v0.18.16 → v0.18.18
Pulls in plugin/evm StateScheme fix that unblocks fresh L2 EVM chain
creation on lux-mainnet (hanzo, zoo, spc, pars), where the plugin was
panicking with "panic in eth.New: triedb parent [<EmptyRootHash>] layer
missing" because eth/backend.go inherited geth's path-by-default for
empty DBs while the VM hard-refuses path mode upfront.

See luxfi/evm v0.18.18 commit 1dea806f8.
2026-06-02 11:56:01 -07:00
Hanzo AI 71c98255bf fix: gofmt -s across repo (CI format check) 2026-06-02 11:39:52 -07:00
Hanzo AI 5a755e4bec go.mod: bump luxfi/genesis v1.12.19 -> v1.13.8 (RLP-matching mainnet+testnet+zoo-mainnet genesis embeds)
Embedded //go:embed configs/{mainnet,testnet,devnet,localnet} now contains
canonical 2-alloc genesis.json's reverted for RLP import compatibility:

- mainnet C-Chain (96369): block 0 = 0x3f4fa2a0...   MATCH lux-mainnet-96369.rlp
- testnet C-Chain (96368): block 0 = 0x1c5fe377...   MATCH lux-testnet-96368.rlp
- zoo-mainnet  (200200):   block 0 = 0x7c548af4...   MATCH zoo-mainnet-200200.rlp

Each had been wedged by 43 PQ precompiles baked into config.precompileUpgrades
at blockTimestamp:0, mutating state root + producing non-canonical hash.
Activations moved to forward-dated upgrade.json (blockTimestamp 1766708400).

Also bumps pkg/genesis/security to v1.13.8 to stay version-locked.
Tidy drops bft v0.1.5 (no longer indirect).
2026-06-02 04:25:27 -07:00
Hanzo AI f6f909d22c docker: drop gcr.io distroless for scratch runtime in heartbeat-tx example
We don't use gcr.io across lux/hanzo/zoo. Switch the heartbeat-tx example
runtime stage from gcr.io/distroless/static-debian12:nonroot to
FROM scratch, copying ca-certs, tzdata, and passwd/group from the builder.
2026-06-02 03:34:39 -07:00
Hanzo AI 6f6e3d01c0 Dockerfile: bump EVM_VERSION v0.18.15 → v0.18.16
luxfi/evm v0.18.16 fixes the nil-chainConfig panic at PQ gate that
made v1.28.15's image-baked EVM plugin crash on fresh-PVC pq:true
bootstrap. Discovered during localnet 1337 bring-up (#148).

Root cause in v0.18.15: vm.chainConfig.PQ was being set BEFORE
vm.chainConfig was assigned from g.Config — nil-deref in
plugin/evm/vm.go Initialize().

v0.18.16 moves the gate AFTER assignment.

This unblocks v1.28.16 image build → unblocks localnet 100% green
→ unblocks cluster deploys (devnet → testnet → mainnet).
2026-06-02 01:05:57 -07:00
Hanzo DevandGitHub d36cc83206 ci(release): cross-compile darwin binaries on lux-build pool
GitHub-hosted macos-13 queues block the release pipeline for hours.
The build is already pure Go cross-compile (CGO_ENABLED=0 GOOS=darwin
GOARCH=$arch) so there's no reason to use a Mac runner. Route through
the self-hosted lux-build ARC pool (fast, plentiful) instead.

Replace 7z (not on ubuntu) with apt-installed zip in the same step.
Output filename + artifact name are unchanged.
2026-06-01 22:55:13 -07:00
Hanzo AI 58426e1e77 go.mod: bump luxfi/consensus v1.25.12 → v1.25.13 (gofmt fixes) 2026-06-01 22:02:34 -07:00
Hanzo AI f15b71db45 go.mod: bump luxfi/accel v1.1.7 → v1.1.8 + consensus v1.25.12 + threshold v1.9.7 + kms v1.11.2 + pulsar v1.1.2
Whole-tree sync to latest closure-swarm tags. accel v1.1.8 ships
c_api.h via //go:embed so go mod vendor preserves it (fixes fresh-clone
CI builds across all consumers).
2026-06-01 22:01:22 -07:00
Hanzo AI 9af3dae914 go.mod: consensus v1.25.9 → v1.25.11; pulsar v1.0.23 → v1.1.1; threshold v1.9.2 → v1.9.4
PULSAR-V04-CTX cascade landing:
  - pulsar  v1.0.23 → v1.1.1  (v0.4 ctx-bound algebraic-aggregate
                                threshold sign — full Round1→Round2W→
                                Round2Sign→AlgebraicAggregateCtx with
                                FIPS 204 §5.4 ctx threaded into μ; no
                                single-party dealer shortcut anywhere)
  - threshold v1.9.2 → v1.9.4  (dispatcher pulsar.Sign_Ctx rewired
                                onto full algebraic-aggregate path; no
                                master sk materialised in dispatcher
                                process at any point during sign)
  - consensus v1.25.9 → v1.25.11 (passes the bumps through)

Test gates green on tip:
  GOWORK=off go test -count=1 -short -timeout 600s ./vms/platformvm/...
  GOWORK=off go test -count=1 -short -timeout 600s ./network/...
  GOWORK=off go test -count=1 -short -timeout 600s ./consensus/...
2026-06-01 21:56:39 -07:00
Hanzo AI b5d91f4f3e go.mod: consensus v1.25.8 → v1.25.9 (magnetar v1.2.0 dealerless PVSS-DKG)
Cascade:
- magnetar v1.1.0 → v1.2.0 (closes MAGNETAR-PVSS-DKG-V11)
- threshold v1.9.1 → v1.9.2 (magnetar bump)
- consensus v1.25.8 → v1.25.9 (threshold + magnetar bump)

Magnetar v1.2.0 lands a Schoenmakers-style PVSS-DKG over GF(257)
for THBS-SE setup. No trusted dealer; no party ever holds the
master byte vector at any time during setup. Share-envelope wire
shapes are byte-shape-identical to the dealer path, so already-
deployed share material is forward-compatible.
2026-06-01 21:25:05 -07:00
Hanzo AI 75d2ad7c26 go.mod: bump luxfi/keys v1.0.9 → v1.1.0, luxfi/kms v1.9.13 → v1.10.1
luxfi/keys v1.1.0 lands the Bindel-Brendel-Fischlin (CCS 2021) +
CDFFJ23 (Asiacrypt 2023) stronger-binding hybrid signature scheme
for validator identity:

  HybridPublicKey / HybridPrivateKey / HybridSignature
  HybridSign / HybridVerify / HybridBoundDigest / HybridPublicKeyBytes
  DeriveHybridIdentity (mnemonic + path → HybridIdentity)

Construction binds BOTH pubkeys into m_bound via SHAKE256-384 under
domain "lux-hybrid-sig-v1" — security ≥ max(EUF-CMA_secp256k1,
sEUF-CMA_ML-DSA-65). Raw concat (the prior plan) only gives
min security under non-honest-key adversary (CDFFJ23 §4).

Classical = secp256k1 (matches existing P/X validator key format).
PQ = ML-DSA-65 (FIPS 204).

Use DeriveHybridIdentity for validator stake re-anchor flow:
classical leaf at m/44'/9000'/serviceIndex'/0'/0', PQ leaf at
m/44'/9000'/serviceIndex'/0'/1'. NodeID derived via single
SHAKE256-384 over wire-form hybrid pubkey (no BTC-style double hash —
cryptographer review confirmed single-SHAKE is sound).

luxfi/kms v1.10.1 follows with the matching go.mod bump.

Tests: go build ./... and go test -race -count=1 -short
./vms/platformvm/... PASS.
2026-06-01 21:03:49 -07:00
Hanzo AI 96a77d9698 go.mod: drop corona v0.7.5 replace — unblock consensus v1.25.8 build
consensus v1.25.8 (carries threshold v1.9.1 + magnetar v1.1.0) refactored
quasar/corona_gob.go and polaris.go to call sig.MarshalBinary() at the
Signature level. The wire-codec methods (Signature.{Marshal,Unmarshal}Binary)
were added in corona v0.7.6 — v0.7.5 only has them on the inner C/Z/Delta
polynomial fields.

The historical replace directive (6338041056 on 2026-05-24) was added to
work around consensus v1.24.6 reaching back into corona via keyera.Bootstrap,
which since shipped its 3-value return at corona v0.7.5. consensus v1.25.x
now pins corona v0.7.6 in its own go.mod cleanly, so the replace is no
longer needed and is actively breaking the v1.28.8 image build.

Removing the replace lets MVS pick corona v0.7.6 transitively through
consensus → that is the version where MarshalBinary lives.

Reproduced the CI failure locally with CGO_ENABLED=0 GOWORK=off, fixed,
verified with a clean amd64 nattraversal-profile build (46.6MB binary)
and a full race-clean ./... suite (exit 0, no DATA RACE / panic markers).
2026-06-01 19:54:49 -07:00
Hanzo AI 07b5a4028b Dockerfile: bump EVM_VERSION v0.18.14 → v0.18.15
luxfi/evm v0.18.15 ships core/genesis: honor SkipPostMergeFields flag
from JSON — the fix for the "triedb parent [0x56e81f17…] layer missing"
panic-in-eth.New that's blocking C-Chain bootstrap on lux-mainnet.

Lux mainnet C-Chain canonical genesis hash is 0x3f4fa2a0…, produced
with the 16-field pre-Shanghai header format. The chain activates
Cancun at genesis time for MCOPY etc., but the genesis BLOCK itself
must stay in the legacy header shape. The previous luxfi/evm tag
ignored skipPostMergeFields=true and shifted the computed genesis
hash to 0x1ade42ec…, which then failed to commit to pathdb because
the parent layer (0x56e81f17… = empty root) wasn't in the layertree.

The plugin baked into this image is at vmId
mgj786NP7uDwBCcq6YwThhaN8FLyybkCa4zBWTQbNgmK6k9A6 — confirmed via
strings of the running prod plugin binary (luxfi/evm/core symbols).
2026-06-01 19:35:54 -07:00
Hanzo AI 59822392b6 go.mod: consensus v1.25.7 → v1.25.8 (magnetar v1.1.0 strict-atom cascade)
consensus v1.25.8 carries threshold v1.9.1 which carries magnetar v1.1.0:
strict-atom Combine, audit-grep clean, byte-identity to circl FIPS 205,
35-51% faster than v1.0.

Race-clean across the full node ./... suite (exit 0; no DATA RACE / panic
markers; 30+ min compile-and-test on -race -timeout=15m).
2026-06-01 19:29:24 -07:00
Hanzo AI bcd3141d9d genesis: add I/O/R chains to primary network registry
OracleVM, RelayVM, IdentityVM were previously registered as optional
VMs (loadable via CreateChainTx) but not part of the genesis-baked
primary network registry. Adding them to the registry means the VM
alias machinery + VMAliases map now know about all 14 primary-network
chains by canonical letter and alias set.

Order is append-only per the comment in registry.go — I/O/R land at
positions 12/13/14. Each row picks a unique alias triple:
  I: identity, identityvm, id
  O: oracle, oraclevm, feed
  R: relay, relayvm, msg

This change does NOT bake I/O/R into FromConfig's primary-genesis
optIn loop — that would require new IChainGenesis/OChainGenesis/
RChainGenesis fields on genesiscfg.Config plus matching ichain.json/
ochain.json/rchain.json shards under genesis/configs/{net}/. I/O/R
continue to be instantiated via CreateChainTx post-genesis (as today).
What the registry update unblocks is the alias resolution path so
when those chains DO get created, the VM manager knows them by
canonical name without per-chain switch-ladder edits.

builder.Aliases() gets matching cases for the three new VMIDs so
when a future genesis bakes them, chain alias registration works
without further changes.

Existing tests in TestChainAliasesRegistryParity and TestVMAliasesRegistryParity
get three new rows each; both pass.
2026-06-01 16:45:49 -07:00
Hanzo AI 3dd7a87cb1 merge: feat/scale-standard 2026-06-01 16:35:45 -07:00
Hanzo AI 81a752c36f merge: feat/create-sovereign-l1-tx 2026-06-01 16:35:45 -07:00
Hanzo AI 04abac267a merge: chore/kill-fuji 2026-06-01 16:35:44 -07:00
Hanzo AI 69756068fb keyutil: thread *keys.ServiceIdentity into LoadMnemonicFromKMS dial
The KMS consensus-auth gate now requires every secret-opcode envelope
to carry a signed identity. Derive a bootstrap ServiceIdentity from
KMS_BOOTSTRAP_MNEMONIC (or MNEMONIC) under the well-known servicePath
"luxd/staking-bootstrap" and thread it into the LoadMnemonicFromKMS
call so the dial envelope is signed.

Bootstrap mnemonic is provisioned out-of-band (sealed envelope, HW
token unwrap, etc.); the operational staking material on disk still
comes from the KMS-held mnemonic the dial then fetches.
2026-06-01 16:15:41 -07:00
Hanzo AI 74ddea71f1 go.mod: consensus v1.25.7 + threshold v1.9.0 + corona v0.7.6 (Polaris cert wired, TEE extensions live) 2026-06-01 12:49:17 -07:00
Abhishek Krishna c78d5a3ade vms/txs/mempool: add AdmissionVerifier hook for encrypted-payload txs (#115)
Closes luxfi/node#115 by landing the admission-hook half of the issue.
The hook is the substrate the encrypted-mempool partition will use to
admit FHE-ciphertext transactions on (signature + fee + NIZK) without
decryption per LP-066 / luxfi/precompile/fhe.

Surface added:

  type AdmissionVerifier[T Tx] interface {
      VerifyAdmit(tx T) error
  }

  var ErrAdmissionRejected = errors.New("tx admission rejected")

  func NewWithAdmissionVerifier[T Tx](
      metrics Metrics,
      verifier AdmissionVerifier[T],
  ) *mempool[T]

  // New is now a thin wrapper around NewWithAdmissionVerifier(metrics, nil).
  // No behavioral change for existing callers.

Add() invokes verifier.VerifyAdmit last, after duplicate / size / space /
conflict have all passed. Ordering rationale: cheap rejects fire first,
so NIZK verification cost is only paid on a tx that passes structural
admission. A non-nil verifier return is wrapped in ErrAdmissionRejected,
records the drop reason via the existing droppedTxIDs LRU, and never
inserts the tx.

What this does NOT do:
  - It does not define the encrypted-payload tx type or its NIZK proof
    format. Those live in vms/platformvm/txs (or wherever the consumer
    decides) and ship in a follow-up.
  - It does not wire the FHE precompile's bootstrap-meter consultation —
    callers implementing AdmissionVerifier do that themselves.
  - It does not add the per-account FIFO partition for encrypted txs.
    The existing unissuedTxs Hashmap is per-mempool; the partition is
    a separate construction over multiple mempool instances and is a
    follow-up.

What it DOES do: lands the only mempool-side hook the issue requires
without locking us in on the encrypted-tx representation. The hook is
generic (`AdmissionVerifier[T Tx]`) so any future Tx variant can plug
in the same way.

Tests (CGO_ENABLED=0 go test -run TestAdmissionVerifier ./vms/txs/mempool/...
all pass):
  - nil verifier matches New (byte-identical behavior)
  - verifier returning nil admits the tx; gate fires exactly once on
    Add and not on Get / Peek / Iterate / Remove
  - verifier returning an error rejects with ErrAdmissionRejected
    wrapping the verifier's reason; drop reason recorded
  - cheap checks short-circuit before the gate runs (duplicate /
    oversize / conflict all skip the gate)

Refs:
  - issue #115 (this repo)
  - LP-066 (F-Chain confidential compute)
  - LP-183 (ZAP envelope decode precompile; downstream consumer once
    encrypted-payload tx propagation through gossip lands)
  - luxfi/threshold issue #20 (real distributed FHE decryption; the
    block-proposer-side counterpart to this admission-side hook)
2026-06-01 22:32:59 +05:30
Abhishek Krishna 3c780e5080 test(errors): cover Wrap/Multi/Join/Is* and category inference (100%)
The github.com/luxfi/node/errors package shipped with zero tests despite
being a public utility surface (sentinel errors, WrappedError context
chain, Multi/Join aggregation, retry helpers). This adds 22 tests covering:

- WrappedError.Error format (with and without message)
- Wrap/WrapWithContext nil pass-through and context preservation
- errors.Is / errors.As chain through Wrap (incl. double-wrap)
- IsNotFound, IsClosed, IsTimeout helpers
- IsTemporary against both sentinel errors and the Temporary() interface
- IsPermanent across all permanent sentinels + fmt.Errorf %w chains
- GetCategory: wrapped category, sentinel inference, unknown fallback,
  and the precedence rule (wrapped category beats inferred category)
- Multi.Error/Add/Err for 0, 1, and N errors; nil-add ignored
- Join nil pass-through and multi-error combination

Result: 100.0% statement coverage, race-clean.
2026-06-01 12:08:28 +00:00
Hanzo DevandGitHub c686146c7f Merge pull request #126 from luxfi/fix/invalidate-stale-genesis-cache
fix(config): invalidate cached genesis on parse failure
2026-06-01 01:43:22 -07:00
Hanzo AI 98476e7fbe fix(config): invalidate cached genesis on parse failure instead of CrashLooping
The cached `<dataDir>/genesis.bytes` file is written on first start to
hold hash stability across restarts. On a binary upgrade that adds
new codec types (multi-version v0+v1 dispatcher, etc.), the old
cached blob may carry type IDs the new binary doesn't recognise. The
existing code surfaced this as `resolve X-Chain asset ID from cached
genesis: unmarshal interface: unknown type ID N` and returned an
error — wedging the node in CrashLoop with no automatic recovery.

Drop the cache and rebuild from the `--genesis-file` instead. Hash
stability is forfeit for that single restart (intentional — the
alternative is a permanent outage on every binary bump that changes
codec types). Subsequent restarts re-establish stability against the
fresh cache.

Surfaced today on <tenant> testnet+mainnet bumping lqd v1.9.x →
v1.10.8: every pod hit "unknown type ID 29" on the stale v1.9.x
codec cache and CrashLoopBackOff'd.
2026-06-01 01:43:00 -07:00
Abhishek Krishna b0313cb56e docs: align Go version in README and CONTRIBUTING with go.mod (1.26.3)
README badge and prerequisites listed Go 1.21.12 / 1.23.9 in different
places, but go.mod requires Go 1.26.3. Bring the docs in sync so new
contributors install a toolchain that can actually build the node.
2026-06-01 14:06:50 +05:30
Hanzo AI 15ebcecf04 go.mod: consensus v1.25.5 + threshold v1.8.10 (race-clean, corona dispatcher live, naming decomplect) 2026-06-01 01:14:43 -07:00
Hanzo AI 7ab2a404bd keyutil: rehome ZAP mnemonic import to luxfi/keys (was luxfi/kms)
Track A finish: KMS goes back to being a generic secret store; mnemonic
semantics live in luxfi/keys alongside the existing BIP-39 + BIP44
derivation. Imports flip from luxfi/kms/pkg/zapclient.LoadMnemonicFromKMS
to keys.LoadMnemonicFromKMS — same signature, same behavior.

Deps:
  luxfi/keys v1.0.8 → v1.0.9     (carries the new LoadMnemonic helper)
  luxfi/kms  v1.9.12 → v1.9.13   (LoadMnemonic removed, secret store only)

Build clean.
2026-05-31 21:30:21 -07:00
Hanzo AI 8cbf34331c go.mod: consensus v1.25.4 + threshold v1.8.9 + magnetar v0.5.2 (race-clean threshold + MAGS/MAGG wire) 2026-05-31 18:20:38 -07:00
Hanzo AI c246f3508f scrub: subnet/l2 → chain (final RELEASES.md cleanup, forward-only)
Wire-format codec IDs unchanged. CLI aliases deleted; chain is the
command. No backwards-compat shims, no deprecation comments.
2026-05-31 15:30:09 -07:00
Hanzo AI 8da3da09bc scrub: subnet/l2 → chain (canonical vocabulary, forward-only)
Wire-format codec IDs unchanged. CLI aliases deleted; chain is the
command. No backwards-compat shims, no deprecation comments.
2026-05-31 15:26:55 -07:00
Hanzo AI d22e87f68b keyutil: native-ZAP mnemonic fallback (priority 2)
Adds a KMS_ADDR-gated production path between the MNEMONIC env var
(priority 1) and the on-disk key files (priority 3). When set, the
mnemonic is fetched via luxfi/kms zapclient.LoadMnemonicFromKMS —
the same canonical loader every Lux-derived service (luxd, netrunner,
lux/cli, descending-L1 bootstraps) now shares.

New priority chain:
  1. MNEMONIC env var
  2. KMS_ADDR + KMS_ENV + KMS_MNEMONIC_PATH (native ZAP, default path /mnemonic)
  3. Key name from os.Args[1] (~/.lux/keys/<name>/)
  4. ~/.lux/keys/default/

Production env contract matches the <tenant> operator's render
(KMS_ADDR + KMS_ORG + KMS_ENV + KMS_MNEMONIC_PATH); every Lux chain
inherits the same scheme.

Dep:
  + github.com/luxfi/kms v1.9.12  (carries zapclient.LoadMnemonic)

Build clean.
2026-05-31 14:29:34 -07:00
Hanzo AI 38528099c8 go.mod: pulsar v1.0.23 + threshold v1.8.8 + consensus v1.25.3 + metric v1.5.7
Pulls in:
- pulsar canonical wire codec (PULS/PULG) + shared lattice/v7/gpu surface
- threshold protocols/{corona,pulsar} alias surface (no direct primitive imports)
- consensus routed through threshold/protocols alias
- metric noop counter race fix (atomic.Uint64)

Cross-repo audit closed: corona+pulsar+threshold production-ready for
permissionless mainnet. End-to-end TestPulsar_Wire_FIPS204Verifiable
asserts threshold-combined signatures are byte-identical to single-party
FIPS 204 ML-DSA signatures (verified externally via cloudflare/circl).
2026-05-31 12:45:26 -07:00
Hanzo AI 82fc618afd fix(platformvm/state): route every state-side codec read through multi-version dispatcher
Closes the residual v1.28.1 testnet-canary failure where bootstrapping
a v1.23.x-written P-Chain database hit:

  P-Chain state corrupt after init — database must be wiped
  error="loadMetadata: feeState: unknown codec version"
  chainID=11111111111111111111111111111111P

The v1.28.1 patch routed genesis.Parse through the multi-version
txs.GenesisCodec dispatcher but did not touch the 7 OTHER state-side
sites that read via block.GenesisCodec — which carried only the v1 slot
map. Any pre-codec-v1 row on disk (feeState, heightRange, L1Validator,
fx.Owner, NetToL1Conversion, legacy stateBlk) errored at the very first
byte with codec.ErrUnknownVersion.

Architecture (Rich-Hickey-simple): make the codec itself complete for
all encountered wire versions rather than asking "which codec does
this caller need". block.GenesisCodec now registers BOTH the v0
(v1.23.x Apricot/Banff) and v1 (current) tx slot maps — reads
dispatch on the 2-byte wire prefix, writes still target
CodecVersion (== v1) exclusively. Same shape as txs.GenesisCodec
(decomplected from block parsing — block.Parse continues to extract
prefix explicitly because v0 blocks satisfy v0.Block, not block.Block,
and cannot be unmarshalled into a block.Block destination).

Audit found 7 state-side sites all using block.GenesisCodec; all 7
are routed through a new defensive helper:

  state.multiVersionUnmarshal(c codec.Manager, b []byte, dest any)

The helper is a pass-through to c.Unmarshal but probes the codec on
first observation. If the codec is missing the v0 slot, a structured
warning fires (once per codec pointer) so a future canary boot
surfaces ALL remaining single-version codecs in a single log scrape
rather than failing piecemeal across iterations:

  state-side codec is single-version; reads of v0-prefixed bytes
  will fail

block.RegisterGenesisType now symmetrically registers on both the v0
and v1 underlying linearcodecs so state-side types (currently:
stateBlk) keep slot-stable shapes across codec.Manager dispatch.

Audit table (all 7 broken sites → fixed):

  state/l1_validator.go:222         getL1Validator
  state/state_blocks.go:115         parseStoredBlock (legacy stateBlk)
  state/state_chains.go:66          GetNetOwner (fx.Owner)
  state/state_chains.go:116         GetNetToL1Conversion
  state/state_metadata.go:176       loadMetadata (heightRange)
  state/state_metadata.go:273       getFeeState  <-- canary failure
  state/state_validators.go:239     loadActiveL1Validators
  state/state_validators.go:535     initValidatorSets (inactive)

MetadataCodec was already multi-version (no fix needed).
txs.GenesisCodec was already multi-version (v1.28.0).
block.Codec stays v1-only by design (block.Block interface destination
cannot accept v0.Block types; Parse handles version split explicitly).

Tests (all -race green):

  block/codec_multiversion_test.go      6 tests
  state/state_v0_codec_test.go          9 tests including
                                          - TestStateV0FeeStateReadable
                                            (exact canary fixture)
                                          - TestStateBootFromV0SingletonDB
                                            (end-to-end boot simulation)
  state/codec_helpers_test.go           5 tests (warning probe +
                                          idempotency + non-blocking
                                          + multi-version invariant)

  -> 20 new regression tests
  -> 27/27 platformvm packages green under -race
2026-05-31 02:56:55 -07:00
Hanzo AI 69f9f23d2d fix(platformvm/genesis): route Parse through multi-version codec.Manager
v1.28.0's block-codec multi-version dispatch did not extend to the
P-Chain genesis decoder. genesis.Codec aliased block.GenesisCodec,
which registers only the v1 tx slot map; v0-prefixed cached-genesis
blobs (carried over from v1.23.x bootstraps) errored at first byte
with codec.ErrUnknownVersion.

Root cause hot path: config.getGenesisData -> resolveXAssetID ->
genesis/builder.XAssetIDFromGenesisBytes -> platformvm/genesis.Parse
-> Codec.Unmarshal(bytes, *Genesis). Codec was the v1-only
block.GenesisCodec.

Fix: alias genesis.Codec to txs.GenesisCodec, which registers BOTH the
v0 (Apricot/Banff) and v1 (current) tx slot maps. The Genesis struct
has no slot ID of its own; all version-sensitive data lives in the
embedded []*txs.Tx, so txs.GenesisCodec dispatches the same wire-
prefix lookup the rest of the platformvm tree already uses.

Marshal at CodecVersion (v1) is byte-equivalent because the v1 slot
map in txs.GenesisCodec is the SAME registerV1TxTypes() invocation
block.GenesisCodec was using.

Audit: every other Unmarshal site in vms/platformvm/ that touches
historical wire bytes either (a) goes through block.Parse / txs.Parse
which already dispatch on the prefix, or (b) reads internal state
written by v1-only code (block.GenesisCodec is correct there).

Regression guards in parse_v0_test.go:
  - TestParseAcceptsV0CachedGenesis: the canary failure mode.
  - TestParseAcceptsV1Genesis: canonical write path still parses.
  - TestParseV0RoundtripIsBytePreserving: v0 -> Parse -> re-marshal
    -> byte-equal, locking in the doc claim that genesis-derived
    hashes do not rotate across the migration.
  - TestParseRejectsUnknownVersion: prefixes outside {v0, v1} still
    surface as errors.

Full vms/platformvm/... tree green under -race; genesis/builder and
config trees green.
2026-05-31 02:26:49 -07:00
Hanzo DevandGitHub 51aa38bb33 feat(platformvm): multi-version codec — v0 (Apricot/Banff) + v1 (current) — byte-preserving TxID/BlockID across migration (#123)
Closes the codec-version trap that surfaced when the v1.23.x ("Apricot/Banff") tx + block layout was rip-replaced in a23d339687 without bumping the wire-version prefix: mainnet/testnet on-disk blobs (~1.08M+ C-Chain blocks) lost a decoder, and any code path that round-tripped a tx through tx.Initialize re-marshaled it under the new layout — rotating TxID and breaking chain-commitment continuity.

Strategy A per cryptographer / orchestrator brief:

* Register both layouts on the platformvm tx + block codec.Managers under
  distinct wire-version prefixes:
  - CodecVersionV0=0 = v1.23.x slot map (TransferInput=5, hole=6, ...,
    AddPermissionlessValidator=25, ..., DisableL1Validator=39)
  - CodecVersionV1=1 = current slot map (with MintOutput/MintOp at 6/8,
    +4-skip for Banff txs at 27-30, CreateSovereignL1Tx at 36,
    SlashValidatorTx at 41, CreateAssetTx/OperationTx at 42-43)
  - txs.Codec / block.Codec dispatch by the standard 2-byte wire prefix.

* New byte-preserving init: tx.InitializeFromBytes(c, version, signedBytes)
  and tx.InitializeFromBytesAtVersion(c, version) bind the tx to its
  original signedBytes without re-marshalling. tx.Initialize stays as the
  fresh-build path; from-DB / from-wire paths route through the
  byte-preserving variant. TxID = hash(signedBytes) under the version it
  was written at, forever.

* New vms/platformvm/block/v0/ subpackage: 9 v0-only block kinds
  (ApricotProposalBlock, BanffProposalBlock, ... at slots 0-4 + 29-32).
  Pure DTOs — no codec, no Visit. The block package wraps the decoded
  v0.Block in a liftedV0Block adapter that:
  - returns the original bytes verbatim (no re-marshal),
  - BlockID = hash(raw v0 bytes),
  - dispatches Visit to the v1 Visitor arms (ApricotProposalBlock /
    BanffProposalBlock -> ProposalBlock, etc.),
  - re-binds embedded txs at v0 via InitializeFromBytesAtVersion so
    inner TxIDs are also byte-preserved.

* genesis.Parse is wire-version-aware: pre-codec-v1 genesis blobs decode
  at v0, new blobs at v1. The matching codec is used for tx re-binding.

* L1-tx slot map shifts +1 to accommodate CreateSovereignL1Tx at 36
  (RegisterL1Validator 36->37, SetL1ValidatorWeight 37->38,
  IncreaseL1ValidatorBalance 38->39, DisableL1Validator 39->40). Test
  fixtures regenerated.

* 22 fee-calculator fixtures + 11 serialization fixtures bumped to use
  the v1 wire prefix (0x0001) and the post-CreateSovereignL1Tx slot map.

Migration notes:
* Mainnet + testnet P-Chain DBs: NO rebuild. Pre-codec-v1 blocks
  continue to decode through the v0 path with original BlockID and
  TxIDs preserved. New blocks are written at v1 from the cut-over
  height onward.
* Devnet: must be rebuilt before rolling to v1.28.0. Its existing
  blobs carry wire-version 0 but use the post-rip slot map (not the
  v0 Apricot/Banff layout) — decoding them through the v0 path would
  read the wrong types. A fresh bootstrap at v1.28.0 writes v1 bytes
  from height 0 and is internally consistent thereafter.

Tests:
* TestCodecVersionV0V1Coexist, TestParseDispatchesByVersion,
  TestTxIDStabilityRoundTrip, TestCrossVersionRefuses (txs)
* TestParseV0ApricotProposalBlock, TestParseV0BanffStandardBlock,
  TestParseV1RoundTrip, TestVersionPrefixDispatch (block)
* 150 packages, 0 failures under -race
2026-05-31 01:38:42 -07:00
Hanzo DevandGitHub fcbb8a4556 bump(consensus): v1.25.1 → v1.25.2 (#122)
Picks up ChainConsensus.ForceAccept — the consensus-level counterpart
to ForcePreference. Together with the engine.finalizeOwnProposal helper,
this lets a proposer self-finalize its own block at proposal time when
peer Chits do not arrive in time, closing the CreateChainTx stall
observed on devnet under low validator counts.

- ChainConsensus.ForceAccept(blockID) — direct accept bypassing alpha/K
  quorum, guarded by engine path (IsOwnProposal=true) and idempotent.
- Engine path uses ForceAccept after ForcePreference on own proposals
  so the proposing node commits locally and other validators converge
  via the next poll round.

Test delta: pre-existing fee/static_calculator L1Tx parse failures on
main are unaffected; consensus, vms/platformvm/{block,blockmock,state,
warp,...}, and genesis/builder all pass with race -short.
2026-05-30 21:38:16 -07:00
Hanzo DevandGitHub 6470097f17 test(genesis/builder): canonical evmAddr/utxoAddr fixture parse gate (#121)
Add TestCanonicalGenesisFixtureParses — loads the canonical mainnet
genesis.json (genesis v1.12.19 evmAddr/utxoAddr field names) through
genesiscfg.GetConfigFile and asserts EVMAddr/UTXOAddr decode to
non-zero ids.ShortID values.

This is the "have we adopted the rename" gate — if the loader
silently swallows the new field names (e.g. via a regression to
ethAddr/luxAddr struct tags), every allocation Address comes back
as ShortEmpty and the assertion catches it before that ships.

Test is host-path aware: skips when ~/work/lux/genesis is not on
disk (CI without sibling checkout), runs when it is.
2026-05-30 20:09:10 -07:00
Hanzo DevandGitHub c2468897ef feat: bump consensus v1.25.1 (proposer fix) + genesis v1.12.19 (#120)
luxfi/consensus v1.25.0 → v1.25.1
  Proposer-self-accept gap on nova multi-node finality: the proposer of
  block B was never marking B locally accepted because manager.applyQbit
  re-derived peer Chits via a second blk.Verify() call which most VMs
  (notably PlatformVM CreateChainTx) are not idempotent under, flipping
  every Chits into synthetic Accept=false. Fix tags the proposer's own
  pending entry with IsOwnProposal=true and short-circuits the re-verify
  in handleVote — peer Chits now count as the genuine Accepts they are.

luxfi/genesis v1.12.15 → v1.12.19
  Decomplected: pkg/genesis/security/ is a nested module (own go.mod,
  tagged pkg/genesis/security/v1.12.19) that owns SecurityProfile
  verification — the only file in luxfi/genesis that imports
  luxfi/consensus. The rest of pkg/genesis stays consensus-dep-free.

  API change: pin.Resolve() → genesissecurity.ResolveProfile(pin).
  ErrSecurityProfileHashMismatch and ErrSecurityProfileInvalidID also
  moved to the security submodule. Updated node/node.go and
  node/security_profile_test.go to match.

Build: GOWORK=off go build ./...  clean (linker warnings about accel
  static lib path are pre-existing host-config noise, not a regression).
Vet:   GOWORK=off go vet ./...    clean (the cevm_e2e_test.go
  sync/atomic.Bool copy warning is pre-existing on v1.27.24 main).
Tests: ./consensus/... pass, ./genesis/... pass, ./node/...
  TestApplySecurityProfile_* pass. ./vms/platformvm/txs/fee L1-validator
  failures are pre-existing on v1.27.24 main and unchanged.

Devnet fixture (~/work/lux/genesis/configs/devnet/genesis.json) parses
clean: networkID=3, 5 initialStakers, canonical evmAddr/utxoAddr only.
2026-05-30 19:56:57 -07:00
Hanzo DevandGitHub 8314e36288 refactor: XAssetID → UTXOAssetID (#119)
The field is the primary network's UTXO fee asset (P+X), not
X-chain-specific. P-chain CreateChainTx / AddChainValidatorTx and
X-chain transfers all burn it for fees. Same number on P and X by
construction; the name should reflect the function, not the chain.

Renames applied across:
- wallet/chain/{p,x}/builder.Context.UTXOAssetID
- wallet/chain/x/builder.Backend.UTXOAssetID() trait method
- node/config: resolveUTXOAssetID + nodeConfig.UTXOAssetID
- vms/platformvm + vms/xvm + chains/manager consumers
- examples (deploy-chains, get-p-chain-balance, ...)

Includes local replaces for luxfi/{runtime,consensus} pending their
release tags landing. Drop after upstream tags ship.

Companion PRs: luxfi/runtime#2, luxfi/consensus#11. Downstream
consumers (sdk, cli, genesis, liquidity) follow.
2026-05-30 14:28:08 -07:00
Hanzo AI 02f5ef1525 deps: proto v1.0.2 — consume SubnetUptime → ChainUptime rename
luxfi/proto#6 (merged) renames SubnetUptime → ChainUptime in
node/zap/p2p, completing the no-subnet vocabulary rule from
node#116. The local re-export in proto/p2p/p2p_zap.go was already
written against the new upstream name, breaking v1.27.22 builds:

  proto/p2p/p2p_zap.go:42:32: undefined: p2p.ChainUptime

Tagging v1.0.2 against the merged proto main and bumping the module
pin unblocks the build with zero code changes — the alias line is
unchanged because upstream already matches.

Build: clean. Test: pre-existing platformvm/txs/fee L1-validator
failures unchanged (not introduced here).
2026-05-30 09:35:04 -07:00
Hanzo DevandGitHub 3278d481e6 canonical evmAddr/utxoAddr only — bump genesis v1.12.15 (#118)
Pulls in luxfi/genesis v1.12.15 which strips the dual-name
luxAddr/ethAddr alias shim. AllocationJSON now emits and accepts only
the canonical evmAddr/utxoAddr field pair.

docker-entrypoint.sh genesis templates: switch luxAddr → utxoAddr +
ethAddr → evmAddr to match.

This is the cascade step needed before luxd v1.23.43 ships — the runtime
parser at v1.23.31 (current devnet image) reads ethAddr/luxAddr; v1.23.42
already reads evmAddr/utxoAddr internally; v1.23.43 (this commit + tag)
drops every back-compat alias both ways.

Tests: genesis/builder, vms/platformvm/genesis pass. End-to-end parse
of configs/devnet/genesis.json with canonical names: 1005 allocations,
5 initial stakers (matches 5-pod sybil quorum).

Cluster bump path: lux-devnet (1.23.31 → v1.23.43); testnet/mainnet
remain on v1.23.31 until coordinated migration.

Co-authored-by: Hanzo AI <dev@hanzo.ai>
2026-05-30 09:24:28 -07:00
Hanzo DevandGitHub 794ccff8e4 chore: update 2026-05-29 23:33:09 -07:00
Hanzo AI fde31ee5a9 chore: update 2026-05-29 23:29:08 -07:00
Hanzo DevandGitHub 66f81ab795 chore(node): kill subnet — chain/network vocabulary across node (#116)
* feat(platformvm): CreateSovereignL1Tx — single-tx sovereign L1 launch

Adds a new platformvm tx type that atomically registers a sovereign L1
in one P-chain commit. Replaces what is today the four-step flow:

  CreateNetworkTx + AddChainValidatorTx ×N + CreateChainTx ×K + ConvertNetworkToL1Tx

with one signed tx. After commit, the primary network has a permanent
record of the L1's network ID + initial validator set + chain manifest
+ on-chain validator-manager contract — but it does NOT track-chains
or validate the L1's blocks. The L1 runs its own consensus from
genesis. L2/L3/L4 follow the same pattern recursively.

Type shape:

  type CreateSovereignL1Tx struct {
    BaseTx
    Owner           fx.Owner                       // CreateNetworkTx parity
    Validators      []*ConvertNetworkToL1Validator // genesis validator set
    Chains          []*SovereignL1Chain            // VM ID + genesis blob per chain
    ManagerChainIdx uint32                         // index into Chains[]
    ManagerAddress  types.JSONByteSlice            // validator-manager contract
  }

  type SovereignL1Chain struct {
    BlockchainName string
    VMID           ids.ID
    FxIDs          []ids.ID
    GenesisData    []byte
  }

SyntacticVerify enforces:
  - at least one validator (sorted, unique)
  - at least one chain, ≤ MaxSovereignL1Chains (16)
  - ManagerChainIdx is in range of Chains[]
  - ManagerAddress ≤ MaxChainAddressLength
  - per-chain name + VMID + FxIDs + genesis bounds
  - BaseTx + Owner + each Validator each verify

Wired into:
  - Visitor interface
  - codec (registered as the next tx type after ConvertNetworkToL1Tx)
  - signer + complexity + metrics + executor stubs across all visitor
    implementations

Executor body is stubbed with a TODO. The atomic state transition
(mint new networkID from tx hash, seed validator-manager state,
register each Chain, charge fee) lands in a follow-up PR. This PR is
the type definition + interface plumbing so downstream tools (wallet,
CLI, fee calc, metrics) can target the tx type while the executor is
implemented.

* chore(node): kill subnet — chain/network vocabulary across node

Zero remaining `subnet|Subnet|SUBNET` in node Go source. Per canonical
no-subnet rule.

## Wire types (Go fields only; byte-level wire encoding unchanged)

  message/wire/types.go  TrackedSubnets    → TrackedChains
  message/wire/zap.go    same on Read/Write
  proto/p2p/p2p_zap.go   SubnetUptime alias → ChainUptime
                          (consumes luxfi/proto rename in companion PR)

## Comment scrub

  message/wire/types.go            "(chain, subnet) pair" → "(chain, network) pair"
  network/peer/handshake.go        "primary-network or subnet" → "primary-network or per-chain"
  node/node.go                     "per-subnet"   → "per-chain"
                                   "P→subnet warp" → "P→chain warp"
  genesis/builder/builder.go       "their own subnets" → "their own chains"
  vms/platformvm/client.go         dropped "subnet jargon" reference
  vms/platformvm/service.go        dropped "net / subnet jargon" reference
  vms/platformvm/config/internal.go  "subnet-spawned blockchain" → "per-chain blockchain"

## ICPSubnet (Internet Computer adapter)

  ICPSubnet → ICPNet  (type rename — unrelated to platform subnet,
                        but still a subnet word; killed for consistency)
  map field `subnets` → `nets`

## Examples

  wallet/network/primary/examples/bootstrap-hanzo/main.go:
    --subnet-id  → --network-id (CLI flag)
    existingSubnetID local var → existingNetID
    "subnet ID" log lines → "network ID"
    "SUBNET_ID=" output → "NETWORK_ID="
    "subnet-evm VM ID" → "EVM VM ID"
    All comments rephrased.

  wallet/network/primary/examples/heartbeat-tx/main.go: one comment
    rephrase.

go.work workspace cleanup:
  - Removed ./operator/go entry (placeholder; lux/operator polyglot
    layout pending the cross-repo migration)
  - Removed ./operator entry (mid-migration; nothing to build)

Build clean. Zero `grep -rIn "subnet|Subnet" --include="*.go"` matches.
2026-05-29 21:17:44 -07:00
Hanzo AI 27ca79a74f feat(platformvm): CreateSovereignL1Tx — single-tx sovereign L1 launch
Adds a new platformvm tx type that atomically registers a sovereign L1
in one P-chain commit. Replaces what is today the four-step flow:

  CreateNetworkTx + AddChainValidatorTx ×N + CreateChainTx ×K + ConvertNetworkToL1Tx

with one signed tx. After commit, the primary network has a permanent
record of the L1's network ID + initial validator set + chain manifest
+ on-chain validator-manager contract — but it does NOT track-chains
or validate the L1's blocks. The L1 runs its own consensus from
genesis. L2/L3/L4 follow the same pattern recursively.

Type shape:

  type CreateSovereignL1Tx struct {
    BaseTx
    Owner           fx.Owner                       // CreateNetworkTx parity
    Validators      []*ConvertNetworkToL1Validator // genesis validator set
    Chains          []*SovereignL1Chain            // VM ID + genesis blob per chain
    ManagerChainIdx uint32                         // index into Chains[]
    ManagerAddress  types.JSONByteSlice            // validator-manager contract
  }

  type SovereignL1Chain struct {
    BlockchainName string
    VMID           ids.ID
    FxIDs          []ids.ID
    GenesisData    []byte
  }

SyntacticVerify enforces:
  - at least one validator (sorted, unique)
  - at least one chain, ≤ MaxSovereignL1Chains (16)
  - ManagerChainIdx is in range of Chains[]
  - ManagerAddress ≤ MaxChainAddressLength
  - per-chain name + VMID + FxIDs + genesis bounds
  - BaseTx + Owner + each Validator each verify

Wired into:
  - Visitor interface
  - codec (registered as the next tx type after ConvertNetworkToL1Tx)
  - signer + complexity + metrics + executor stubs across all visitor
    implementations

Executor body is stubbed with a TODO. The atomic state transition
(mint new networkID from tx hash, seed validator-manager state,
register each Chain, charge fee) lands in a follow-up PR. This PR is
the type definition + interface plumbing so downstream tools (wallet,
CLI, fee calc, metrics) can target the tx type while the executor is
implemented.
2026-05-29 20:25:28 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f1283a2ed9 build(deps): bump the go_modules group across 1 directory with 2 updates (#106)
Bumps the go_modules group with 1 update in the / directory: [go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp](https://github.com/open-telemetry/opentelemetry-go).


Updates `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp` from 1.42.0 to 1.43.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.42.0...v1.43.0)

Updates `go.opentelemetry.io/otel/sdk` from 1.42.0 to 1.43.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.42.0...v1.43.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
  dependency-version: 1.43.0
  dependency-type: direct:production
- dependency-name: go.opentelemetry.io/otel/sdk
  dependency-version: 1.43.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-29 18:02:09 -07:00
Hanzo DevandGitHub 5e0090337e build(node): GOEXPERIMENT=jsonv2 in Dockerfile per SCALE_STANDARD (#114)
Per SCALE_STANDARD.md §2 (~/work/hanzo/hips/docs/SCALE_STANDARD.md) —
every Go production Dockerfile that emits JSON to a client builds with
GOEXPERIMENT=jsonv2. Verified on hanzoai/zip's json_bench_test.go:
- Edge POST roundtrip: -12% time, -23% allocs
- Marshal-only: -22% time
- Unmarshal-only: -19% time, -25% allocs

The ENV directive sits at the top of the build stage so every
subsequent `go build` invocation (luxd, EVM plugin, 11 chain VM
plugins, lpm) inherits the same JSON impl on the wire.
2026-05-29 18:01:36 -07:00
Hanzo AI 81801ca236 brand: rip <tenant> from builder comment 2026-05-29 14:26:21 -07:00
Hanzo AI 6a8be2ee92 brand: rip <tenant> name from code comments (no crossover) 2026-05-29 14:26:00 -07:00
Hanzo AI d062d03714 fix(docker): bump EVM_VERSION v0.8.40 → v0.18.14 (post-rename plugin)
The v0.8.40 EVM plugin was built against luxfi/node v1.23.4 — pre-rename
EngineAddressKey ("LUX_VM_RUNTIME_ENGINE_ADDR"). The host (built from
this repo) has been emitting the NEW key ("VM_RUNTIME_ENGINE_ADDR")
since c431d884ed (2026-05-15). Every luxd:v1.27.x image embeds a plugin
that reads the OLD key — empty env → no dial-back → C-chain never
bootstraps → all 4 L2 EVMs stay un-bootstrapped indefinitely.

v0.18.14 pins luxfi/node v1.27.6 which carries the rename, so the
plugin built from it reads the same key the host writes. Pairs with
b2b786de63 (host compat shim) — once every cluster pulls an image that
contains this Dockerfile bump, the shim is a no-op.
2026-05-27 04:59:03 -07:00
Hanzo AI b2b786de63 fix(rpcchainvm): compat shim for pre-rename plugin env-var
The EngineAddressKey const renamed from LUX_VM_RUNTIME_ENGINE_ADDR to
VM_RUNTIME_ENGINE_ADDR in c431d884ed (2026-05-15). luxd builds from
that commit forward set VM_RUNTIME_ENGINE_ADDR on plugin exec, but the
EVM plugin (luxfi/evm@v0.8.40 → luxfi/node@v1.23.4 → "LUX_VM_..." const)
in the same Docker image still os.Getenv("LUX_VM_...") — empty string,
no dial back, plugin exits, C-chain never bootstraps, all L2 EVMs stay
un-bootstrapped indefinitely on v1.27.x.

Set BOTH env keys until every plugin in /data/plugins has been rebuilt
against a luxfi/node version that has the rename. New const is the
canonical name; legacy const is the compat key (added as
LegacyEngineAddressKey and only emitted when it differs from the new
key, so once luxfi/evm bumps past the rename the extra env var becomes
a no-op and this line can be deleted without runtime impact).
2026-05-27 04:57:24 -07:00
Hanzo AI 858a03f26a chore: update 2026-05-25 15:13:02 -07:00
Hanzo AI 6338041056 go.mod: replace luxfi/corona v0.7.5 — track keyera.Bootstrap 3-value return
consensus@v1.24.6 calls keyera.Bootstrap(...) with three return values
but its own go.mod still pins corona v0.4.0 (where Bootstrap returns
two). The 3-value signature lives at corona v0.7.x.

Locally we hide this via go.work using the working tree, so it never
shows up. CI builds without go.work and fails compiling
protocol/quasar/grouped_threshold.go.

Add a replace directive pinning corona to v0.7.5 (current latest).
A proper fix is to tag a luxfi/consensus v1.24.7 with corona bumped
in its own go.mod, but luxd is the only consumer of this transitive
mismatch right now — the replace keeps it tight and reversible.
2026-05-24 19:17:18 -07:00
Hanzo AI 0462393602 ci/docker: fall back to UNIVERSE_PAT when org GH_TOKEN is empty
The v1.27.18 diagnostic confirmed luxfi org GH_TOKEN secret resolves to
length=0 at runtime (visibility is set to 'all' but the actual value
appears unset/expired). UNIVERSE_PAT is set at the repo level and has
the same cross-repo read scope we need for private luxfi/* deps. Try
GH_TOKEN first, fall back to UNIVERSE_PAT, fail fast with a clear
error if both are empty.

Also explicitly disable docker/metadata-action's latest=auto flavor so
the :latest floating tag truly never gets emitted (the prior run showed
the action silently injecting it even with no type=raw rule).
2026-05-24 18:57:38 -07:00
Hanzo AI be5c766848 ci/docker: self-check GH_TOKEN propagation before BuildKit secret mount
The repeated 'terminal prompts disabled' fatal at go mod download in
v1.27.15..v1.27.17 looked like the BuildKit secret mount was producing
an empty /run/secrets/ghtok file. Add a pre-build step that fails fast
if secrets.GH_TOKEN does not resolve at workflow run time (org-secret
visibility=all, but ARC runner ephemeral identities have surprised us
before). The token value is never echoed — only its byte length.
2026-05-24 18:51:28 -07:00
Hanzo AI b1d8e630dd genesis/builder + ci: initialSupply matches emitted UTXOs; Dockerfile keeps insteadOf live
initialSupply was still summing both initialAmount AND unlockSchedule
amounts — even with the UTXO emission fix, the reported supply field
double-counted Avalanche-shaped configs. Match the emission policy:
unlockSchedule wins when non-empty, initialAmount otherwise.

Dockerfile: drop the post-go-mod-download cleanup of the
url.insteadOf git config. The build step at the bottom of the
builder stage also triggers go fetches (resolving build-time
transitive deps), so the rewrite must remain in place. The
throwaway builder stage never ships, so leaving the token in
/etc/gitconfig is fine — only the compiled binary is COPYed into
the runtime image.
2026-05-24 18:43:49 -07:00
Hanzo AI 9fc82dc0e5 ci/docker: resolve private luxfi/* deps via org-level GH_TOKEN PAT
Default workflow GITHUB_TOKEN only has read access to the running repo
(luxfi/node), so go mod download fails on private cross-repo deps such
as luxfi/corona. Switch the BuildKit ghtok secret to the org-level
GH_TOKEN (a PAT with org-wide read scope) so the Dockerfile's git
url-rewrite picks up every luxfi/* module the build needs.
2026-05-24 18:34:58 -07:00
Hanzo AI a805551f57 genesis/builder: back-compat — initialAmount UTXO only when unlockSchedule empty
Avalanche/legacy genesis JSONs (testnet, mainnet) set initialAmount as the
sum of unlockSchedule (two views of one total). The current builder emits
both an immediately-spendable UTXO for initialAmount AND one UTXO per
unlock entry — double-minting the allocation.

Devnet-style configs set initialAmount with an empty unlockSchedule and
need the single UTXO to be emitted.

One semantic, one UTXO: when unlockSchedule is non-empty, skip the
initialAmount UTXO (the schedule already covers the total). When
unlockSchedule is empty, emit the initialAmount UTXO as before.

Also:
- ci/docker: switch back to self-hosted `lux-build` ARC pool (DOKS hanzo-k8s)
- ci/docker: drop floating `:latest` tag — semver/sha-only policy
2026-05-24 18:28:00 -07:00
Hanzo AI 3bebb93092 wallet/primary: tighten commented future-work lines to EVM canonical names
Cleanup pass on the C-Chain-disabled future-work comments. No code
change — just the placeholder identifiers now match the canonical
EVMAddress / EVMKeychain naming used by the live KeychainAdapter.

ethAddrs    → evmAddrs
FetchEthState → FetchEVMState
2026-05-24 13:27:31 -07:00
Hanzo AI 020209dc5f wallet: mass-rename EthKeychain → EVMKeychain across all examples (no aliases)
Forward-only completion of the strip-aliases work. Examples (18 main.go
files + example_test.go + debug_balance_test.go) used the old
EthKeychain field name on WalletConfig literals. Mass-renamed via
sed across the wallet/ tree to match the canonical EVMKeychain name.

Also: cleaned remaining linter-restored EthKeychain references in
wallet.go (WalletConfig struct field, comments in MakeWallet).

Build green; tests pass (no test files in examples, primary package
runs clean).

Per CLAUDE.md x.x.x+1.
2026-05-24 06:34:38 -07:00
Hanzo AI 06729f6926 wallet: strip all Eth/Keccak aliases — EVMKeychain / EVMAddresses / WithCustomEVMAddresses only
Forward-only per user "no aliases!!! just keep evm address and utxo address"
and CLAUDE.md "no backwards compatibility only forwards perfection".

wallet/network/primary/wallet.go:
- Removed KeccakKeychain interface (was Deprecated)
- Removed EthKeychain interface (was Deprecated)
- Removed GetByKeccak/KeccakAddresses methods on KeychainAdapter
- Removed GetEth/EthAddresses methods on KeychainAdapter
- Renamed WalletConfig.EthKeychain field → EVMKeychain
- Mass renamed EthKeychain → EVMKeychain across all 18 example
  programs (sed -i '' s/EthKeychain/EVMKeychain/g)

wallet/network/primary/common/options.go:
- Removed KeccakAddresses() method (was Deprecated)
- Removed EthAddresses() method (was Deprecated)
- Removed WithCustomKeccakAddresses (was Deprecated)
- Removed WithCustomEthAddresses (was Deprecated)
- Renamed private fields customEthAddresses{Set} → customEVMAddresses{Set}

Only canonical names remain:
- EVMKeychain interface
- KeychainAdapter.{GetByEVM, EVMAddresses}
- WalletConfig.EVMKeychain
- Options.EVMAddresses
- WithCustomEVMAddresses

Downstream callers using old names break at compile time. Lockstep
break-fix follows in cli, kms, mpc, state.

Deps: utxo v0.3.2 → v0.3.3, crypto v1.19.15 → v1.19.16.

Per CLAUDE.md x.x.x+1.
2026-05-24 06:17:08 -07:00
Hanzo AI 1032578ebf wallet: reconcile — EVMKeychain / EVMAddresses / WithCustomEVMAddresses canonical
Workspace-wide reconcile to the runtime-data-model axis (EVM) the
state team established earlier. Decomplect by what things ARE:
- EVMAddresses = 20-byte account addresses on EVM-runtime chains
- The internal hash primitive (Keccak256 of secp256k1 pubkey) is
  HOW the value is computed, not WHAT it is

wallet/network/primary/wallet.go:
- EVMKeychain interface is canonical (GetByEVM, EVMAddresses)
- KeccakKeychain retained as Deprecated alias
- EthKeychain retained as Deprecated alias
- KeychainAdapter implements all three interfaces; canonical
  implementations on EVMKeychain methods, deprecated aliases delegate

wallet/network/primary/common/options.go:
- Options.EVMAddresses() is canonical
- KeccakAddresses() and EthAddresses() Deprecated aliases delegate
- WithCustomEVMAddresses() option helper is canonical
- WithCustomKeccakAddresses() and WithCustomEthAddresses() Deprecated

Deps bumped:
- luxfi/utxo v0.3.1 → v0.3.2 (EVMAddrs canonical, deprecated aliases)
- luxfi/crypto v1.19.13 → v1.19.15 (EVMAddress canonical)
- luxfi/genesis v1.12.11 → v1.12.14 (transitive dep of utxo bump
  for crypto/keccak package path)

Per CLAUDE.md x.x.x+1.
2026-05-23 22:24:05 -07:00
Hanzo AI eb0e48c698 wallet: decomplect — add KeccakKeychain interface alongside deprecated EthKeychain
Wave 3 of the workspace-wide eth* naming purge. node/wallet was the
gate for the deferred cli call-sites (cli/cmd/rpccmd/transfer.go and
cli/pkg/chain/local.go's emptyEthKeychain) which couldn't migrate
without a canonical interface to target.

wallet/network/primary/wallet.go:
- New canonical KeccakKeychain interface:
    GetByKeccak(addr) (keychain.Signer, bool)
    KeccakAddresses() set.Set[gethcommon.Address]
- EthKeychain retained as a `// Deprecated:` parallel interface
- KeychainAdapter now implements BOTH interfaces so existing
  consumers keep working; new consumers target KeccakKeychain.
- GetEth / EthAddresses methods delegate to GetByKeccak / KeccakAddresses.

wallet/network/primary/common/options.go:
- New canonical Options.KeccakAddresses() method
- New canonical WithCustomKeccakAddresses(...) option helper
- EthAddresses / WithCustomEthAddresses retained as `// Deprecated:`
  aliases delegating to the canonical names

Deps bumped:
- luxfi/utxo v0.3.0 → v0.3.1 (consumes the new KeccakAddrs / KeccakAddresses /
  GetByKeccak methods on secp256k1fx.Keychain)
- luxfi/crypto stays at v1.19.13 (PrivateKey.KeccakAddress)

This unblocks cli/cmd/rpccmd/transfer.go to call kcAdapter.KeccakAddresses()
and cli/pkg/chain/local.go to implement KeccakKeychain — a future cli
wave (v1.100.5+).

Per CLAUDE.md x.x.x+1.
2026-05-23 19:51:10 -07:00
Hanzo AI a1d40cca71 go.mod: bump luxfi/genesis v1.12.13 → v1.12.14 (clean build) 2026-05-23 16:27:17 -07:00
Hanzo AI c1ecd70506 go.mod: bump luxfi/genesis v1.12.12 → v1.12.13 (keys.go build fix) 2026-05-23 16:25:36 -07:00
Hanzo AI 40f2e6cdc3 go.mod: bump crypto v1.19.14 + genesis v1.12.12 (keccak→keccak256 subpackage rename) 2026-05-23 16:21:54 -07:00
Hanzo AI 313fe35e44 go.mod: bump luxfi/genesis v1.12.10 → v1.12.11 (keccak primitive direct) 2026-05-22 21:20:01 -07:00
Hanzo AI 24644701e0 go.mod: bump luxfi/genesis v1.12.8 → v1.12.10 2026-05-22 21:05:25 -07:00
Hanzo AI 3e7eeedb39 drop ethAddr/luxAddr/luxcrypto stragglers — match genesis v1.12.10 2026-05-22 21:05:21 -07:00
Hanzo AI 4d47176ce9 go.mod: bump luxfi/genesis v1.12.7 → v1.12.8 (no more eth* identifiers) 2026-05-22 20:56:53 -07:00
Hanzo AI 3b5885c018 drop ethAddr/luxAddr string tags — canonical is evmAddr/utxoAddr 2026-05-22 20:40:49 -07:00
Hanzo AI a0ba39d6aa go.mod: bump luxfi/genesis v1.12.6 → v1.12.7 (EVMAddr Go field) 2026-05-22 20:37:37 -07:00
Hanzo AI a705bcad6e deps: bump consensus v1.24.6, threshold v1.8.5, metric v1.5.5 (kill prom/protobuf transitives) 2026-05-22 20:24:51 -07:00
Hanzo AI e69ab3f50d go.mod: bump luxfi/genesis v1.12.5 → v1.12.6 (JSON evmAddr+utxoAddr) 2026-05-22 20:11:51 -07:00
Hanzo AI a8d45db350 purge Avalanche-era upgrade names from test fixtures
Final cleanup for the upgrade-name purge (lux/node now runs full
feature-set from genesis under activate-all-implicitly):

- vms/xvm: `var durango = upgrade.Default` -> `var activeUpgrade =
  upgrade.Default`. Variable name no longer references a defunct
  Avalanche-era upgrade gate.
- wallet/chain/p/builder_test.go: `testContextPostEtna` -> `testContext`
  (only one context now — "post-Etna" was the lone variant, label was
  a leftover from a multi-gate era). Test names "Post-Etna" /
  "Post-Etna with memo" -> "default" / "default with memo".

No behavior change. `go build` + `go vet` clean.
2026-05-22 02:37:40 -07:00
Hanzo AI 57b1c7fe42 go.mod: bump luxfi/genesis v1.12.4 → v1.12.5 (devnet 100M/wallet alignment) 2026-05-22 00:11:28 -07:00
Hanzo AI 9ebd686b11 go.mod: bump luxfi/genesis v1.12.2 → v1.12.4 (refreshed canonical hashes + devnet 50M) 2026-05-21 23:48:45 -07:00
Hanzo AI e81c38896e genesis,config,xvm: derive X-Chain asset ID from genesis content
config.UTXOAssetIDFor(networkID) returns a network-id-keyed constant
that's identical across every L1 sharing a primary-network ID. On
sovereign L1s (<tenant> / MLC / VCC / any future tenant) the X-Chain
genesis bakes a different asset (different validator set, different
holder set, different denomination) so the runtime asset ID — the ID
vm.initGenesis assigns to the first GenesisAsset.CreateAssetTx — does
not match the constant.

Every wallet builder that calls platform.getStakingAssetID to populate
pCTX.XAssetID then pays tx fees from UTXOs under an asset the chain
doesn't recognise:

    insufficient funds: needs 398 more nLUX (<constant>)

That's the bootstrap failure on <tenant> devnet / testnet / mainnet
after the chainset-via-embedded-genesis fix landed.

Fix: derive the X-Chain native asset ID from the actual genesis bytes
the binary loads at startup. Every load path now goes through
resolveXAssetID(networkID, genesisBytes):

  - genesis baked with X-Chain → parse the embedded XVM genesis,
    initialize the first GenesisAsset.CreateAssetTx, return its tx.ID()
    (the same value vm.initGenesis assigns at runtime).
  - genesis is P-only (no X-Chain) → fall back to UTXOAssetIDFor.
    Value is unused at runtime, kept for downstream-consumer shape.
  - genesis is malformed → error (was previously silently returning
    the wrong constant; that's how sovereign L1s ended up shipping
    binaries that disagreed with their own chain).

Touched:
  - vms/xvm/genesis.go: ParseGenesisBytes + AssetIDFromGenesisBytes.
  - vms/xvm/genesis/lux.go: AssetIDFromBytes proxy so the wrapper
    package stays the single dependency point.
  - genesis/builder/builder.go: FromConfig uses the genesis-derived
    ID instead of UTXOAssetIDFor; new XAssetIDFromGenesisBytes
    helper for the platform-genesis-bytes path.
  - config/config.go: getGenesisData's raw and cached paths now call
    resolveXAssetID. FromConfig path inherits the fix automatically.

Tests:
  - vms/xvm/genesis/lux_test.go: AssetIDFromBytes is stable, holder-
    sensitive, network-id-sensitive, malformed-input-rejecting.
  - genesis/builder/builder_p_only_test.go: helper agrees with
    FromConfig on sovereign genesis, returns ok=false on P-only,
    errors on garbage.
  - config/config_test.go: resolveXAssetID returns the genesis-derived
    ID for sovereign genesis, UTXOAssetIDFor on P-only, error on
    garbage.

No network-id allow-list, no per-network branching, no env-var
overrides. The fix is in the binary's load path; operator workflows
stay byte-identical.
2026-05-21 18:34:50 -07:00
Hanzo AI 70334e13bd docker: fall back to ubuntu-latest runner — lux-build ARC offline
The lux-arm64-runners EKS cluster is unreachable (i/o timeout on
control plane); no Docker builds since the runner pool dropped.
Pinning to ubuntu-latest until ARC is restored. Switch back to
lux-build in a follow-up once the EKS cluster is back up.
2026-05-21 17:43:22 -07:00
Hanzo AI c2e0c7dba1 LLM.md: FeePolicy table — add G-Chain (graphvm) NoUserTxPolicy row 2026-05-21 17:35:22 -07:00
Hanzo AI adccfc73e6 docker: inject GH_TOKEN via BuildKit secret for private mod download
luxfi/corona went private; the Dockerfile builder's `go mod download`
was failing with "could not read Username for 'https://github.com'"
on the ARC runner. Adds a BuildKit secret mount (required=false so
local builds without the secret still work when all deps are public)
and rewrites `git config url.insteadOf` to inject the token only for
the download step. Token is unset after to keep the layer clean.

The workflow passes ${{ secrets.GITHUB_TOKEN }} as the `ghtok` secret;
the default GITHUB_TOKEN has repo:read for the runner's repository,
which is sufficient for luxfi/corona since it's in the same org.
2026-05-21 17:28:07 -07:00
Hanzo AI 6fb7b274bb LLM.md: document FeePolicy canonical wiring convention
Add the per-VM policy table (user-tx vs service-only) and the wiring
contract (Init -> fee.Validate -> ValidateFee at the user-tx entry,
internal paths bypass). The actual gates live in the per-VM repos
(luxfi/chains/<vm>/feegate.go, luxfi/oracle/vm/feegate.go,
luxfi/relay/vm/feegate.go) — this is the index page.
2026-05-21 17:23:58 -07:00
Hanzo AI 797950eac2 gitignore stray dev-tool binaries 2026-05-21 15:01:49 -07:00
Hanzo AI fa932ebfad drop hardcoded /Users/z paths — use $HOME-relative 2026-05-21 15:01:36 -07:00
Hanzo AI c970b553d2 rip: proto/zap/ moved to luxfi/proto/node/zap (canonical proto module) 2026-05-21 14:42:42 -07:00
Hanzo AI a3358e9d7c genesis/builder: track upstream Allocation.ETHAddr rename
upstream luxfi/genesis v1.12.2 drops EVMAddr in favor of canonical
ETHAddr (json tag `ethAddr`). Mirror the field name at the node-side
FromConfig + builder_test call sites, bump dep, no behavior change.
2026-05-21 12:53:05 -07:00
Hanzo AI dfd6525be5 go.mod: bump luxfi/genesis to v1.12.1 (kill F12/F30 slop) 2026-05-21 12:30:43 -07:00
Hanzo AI 7af65f4c69 go.mod: bump luxfi/genesis to v1.12.0 (no backward compat) 2026-05-21 10:44:45 -07:00
Hanzo AI c1dec826a5 go.mod: bump luxfi/genesis to v1.11.9 (xchain.json shards baked) 2026-05-21 03:47:01 -07:00
Hanzo AI 3f02b2a1c8 genesis/builder: chain registry as data — decomplect aliases from switch ladders
The primary-network alias machinery used to be three separate hand-typed
data structures encoding the same chain identity:

  - Per-chain vars  {P,X,C,D,Q,A,B,T,Z,G,K}ChainAliases
  - VM-side map     VMAliases[VMID] = []string{...}
  - Switch ladders  inside Aliases() that map VMID → letter+aliases

Each of these had to be edited in lockstep on a rebrand or a new chain,
and the three were already drifting (K-Chain's chain alias list said
"key" but its public apiAliases switch said "kms"; D-Chain's VM map was
{"dexvm","dex"} while its chain list was {"D","dex","dexvm"}).

This change introduces builder.Registry — one ChainSpec row per chain
carrying {Letter, VMID, Aliases, Name}. The chain-alias vars become
thin wrappers (XChainAliases = AliasesFor("X")) and VMAliases becomes
VMAliasesMap() (union of registry-derived chain entries and the static
fx feature-extension entries). Rebranding now edits one row.

Compatibility:

  - Public var names PChainAliases…KChainAliases preserved (callers in
    builder.Aliases() and external tooling don't break).
  - VMAliases is still a map[ids.ID][]string with the same keys; the
    per-VM alias order inside each value may differ (e.g. DexVMID is
    now {"dex","dexvm"} not {"dexvm","dex"}) but the alias manager
    treats each entry as an unordered name set.
  - Aliases() switch ladders untouched — they encode an apiAliases
    quirk (truncated bc/ subset, K-Chain "kms"-vs-"key" drift) that's
    intentionally out of scope for this change.

Tests: builder_test.go's existing TestChainAliases + TestVMAliases pass
unchanged. New parity tests assert reflect.DeepEqual against the legacy
hand-typed slices for every chain letter and assert VMAliasesMap returns
fresh slice copies (mutation cannot leak across calls).

  go build ./genesis/builder/...   clean
  go vet ./genesis/builder/...      clean
  go test ./genesis/builder/...     ok (0.96s, all subtests pass)
2026-05-21 03:43:00 -07:00
Hanzo AI 11ae00f7dc vms/xvm/genesis: extract DefaultLUXGenesisBytes (decomplect builder from xvm)
The primary-network genesis builder used to import vms/xvm directly to
construct the LUX asset descriptor — braiding "what an XVM genesis
looks like" with "how the node's primary-network gets bootstrapped".

Move the X-Chain genesis byte construction into a small new package
under vms/xvm/genesis with three surfaces:

  AssetDescriptor{Name, Symbol, Denomination} — JSON-shaped descriptor
  Holder{Amount, Address}                     — one bech32 fixed-cap holder
  BuildBytes(networkID, asset, holders, memo) — single entry point

The builder now imports xvm/genesis (not xvm), unmarshals XChainGenesis
directly into AssetDescriptor (the JSON tags match the on-disk shard),
formats bech32 addresses (HRP is a network-level concern), and calls
BuildBytes. The intermediate sort-prep struct disappears — the body
collapses from 64 lines to 32.

Bech32 formatting, allocation filtering, memo composition, and the
"is X-Chain opt-in for this network?" policy stay in the builder
where they belong. xvm/genesis only owns the XVM-shaped construction.

Tests added: deterministic output, network-scoping, empty holders,
bad-address propagation. Builder tests unchanged and pass.
2026-05-21 03:36:50 -07:00
Hanzo AI 4cf006a88e vms/types/fee: introduce FeePolicy interface (close free-tx paths)
A 2026-05 audit of vms/* found five chains accepting user txs while
charging nothing (dexvm, bridgevm, keyvm, zkvm, aivm) and one charging
1,000x too little (quantumvm). Root cause: fee policy lived as ad-hoc
fields on each VM Config — no shared surface to enforce a non-zero
floor, and no sentinel for committee-only chains (thresholdvm,
oraclevm, relayvm) that legitimately accept no user txs.

This commit introduces the shared surface:

  - Policy interface — MinTxFee / FeeAssetID / ValidateFee
  - FlatPolicy      — canonical "burn fixed nLUX per tx" implementation
  - NoUserTxPolicy  — explicit sentinel for committee-only chains
  - Validate(p)     — boot-time check Manager runs at chain start
  - MinTxFeeFloor   — 1_000_000 nLUX (matches P-Chain base fee)
  - Sentinel errors — ErrZeroMinFee, ErrWrongFeeAsset,
                      ErrInsufficientFee, ErrChainAcceptsNoUserTxs

Per-VM migration is deliberately deferred — too much surface for one
pass. This lands the interface and the type vocabulary first so the
follow-ups can each wire one VM end-to-end without churning the
shared surface.

Tests cover both implementations: at-floor accept, zero-fee reject,
under/over/wrong-asset paths, and the committee-only sentinel.
2026-05-21 03:34:28 -07:00
Hanzo AI c668e7b9bb use UTXOAssetIDFor (brand-neutral) — constants v1.5.7 2026-05-21 03:30:47 -07:00
Hanzo AI 64f5eb03aa genesis,config: use LUXAssetIDFor(networkID) — per-network LUX asset ID
Cryptographer flagged collapsing per-network LUX asset IDs into one
constant as a defense-in-depth regression. constants v1.5.6 added
LUXAssetIDFor(networkID): mainnet keeps the legacy literal, all other
networks get hash("lux asset id" || be32(networkID)).

- genesis/builder/builder.go: xAssetID := constants.LUXAssetIDFor(config.NetworkID)
- config/config.go: replace extractXAssetID() callsites with
  constants.LUXAssetIDFor(networkID); delete the now-dead helper.

Bundles dev agent's earlier X-Chain decomplect commit (721af250) into
the same push: X-Chain is now opt-in via XChainGenesis, no longer
hardcoded as "always present".

Builds + tests green.
2026-05-20 22:20:55 -07:00
Hanzo AI 721af2509a genesis/builder: X-Chain becomes opt-in (decomplect from "always present") 2026-05-20 22:08:54 -07:00
Hanzo AI 920459ecb7 rip: AI-generated debug fmt.Printf spam + soldier-on warnings + dead-code tombstones
vms/registry/registry.go: drop 17 [Registry]/[VMGetter] debug fmt.Printf
  calls that were left from an AI debugging session. Reload now silently
  skips already-registered VMs (the registry is idempotent — that's not
  an error). Doc-commented for what each return value means.

node/node.go: drop the [BOOTSTRAP] fmt.Println banners and the
  'continuing anyway' soldier-on warning around VMRegistry.Reload. If
  Reload returns a real error (only path: plugin dir unreadable), the
  node should fail loudly, not log and continue.

vms/xvm/vm_benchmark_test.go, vms/platformvm/validators/test_manager.go,
wallet/chain/p/wallet/backend_visitor.go, wallet/keychain/keychain_test.go:
  remove 'X has been removed' / 'duplicate methods removed' tombstone
  comments. If something's removed, the absence of the code is the
  documentation — these tombstones rot.
2026-05-20 17:11:52 -07:00
Hanzo AI 58b211deab node: refresh config + rpcdb + vm registry + subprocess initializer
Routine maintenance: config.go, rpcdb/service.go, node/node.go,
vms/registry/registry.go, vms/rpcchainvm/runtime/subprocess/initializer.go.
2026-05-20 16:26:18 -07:00
Hanzo AI d83ea482e2 genesis/builder: use UTXO_ASSET_ID constant + EVMAddr/UTXOAddr field names
- constants v1.5.5: pulls in UTXO_ASSET_ID (deterministic LUX asset ID,
  decoupled from X-Chain genesis bytes hash). Makes X-Chain optional —
  P-only L2s can ship without X-Chain bake.
- genesis v1.11.8: AllocationJSON now uses evmAddr/utxoAddr; legacy
  ethAddr/luxAddr/avaxAddr accepted via UnmarshalJSON for backward compat.
- builder.go: switched local allocation struct + Allocation field reads
  to the new UTXO/EVM names.

Build + tests green. Unblocks "P+Q-only" L2 topology.
2026-05-20 16:11:51 -07:00
Hanzo AI 2517148102 fix(docker): chains/* plugin builds best-effort
luxfi/chains main has unresolved sibling go.mod replace directives
(luxfi/{evm,precompile,threshold} => ../*) that break in any isolated
build context. Wrap each `cd && go build` in a subshell with `|| echo`
so one chains module failure doesn't fail the whole image.

Production deployments pull plugins from `pluginSource.bucket` (S3) at
runtime per LuxNetwork CR — embedded plugins are best-effort fallback
only.

Unblocks luxd v1.27.x Docker publish. Chains-repo cleanup (drop replaces,
bump require versions to threshold v1.8.0/precompile v0.5.23/evm v0.18.13,
tag v1.2.5) tracks separately.
2026-05-19 13:32:30 -07:00
Hanzo AI 72e3df205e ci: switch lux/node workflows to lux-build (luxfi-scoped ARC pool)
Cross-org runs-on dispatch: the hanzo-build-linux-amd64 scale set is
registered to github.com/hanzoai and does NOT pick up jobs from
github.com/luxfi — the four queued v1.27.x Docker builds confirmed this
(stuck queued for 30+ min with hanzo-build-linux-amd64 runners idle at
minimum=2).

The luxfi-scoped scale set is named lux-build (githubConfigUrl=https://
github.com/luxfi, max 20). Switch docker.yml, build-linux-binaries.yml,
and the ubuntu release builders to it.

Next tagged release (v1.27.5+) will dispatch correctly. The four pending
v1.27.x runs (tagged against the prior runs-on) need cancel + re-run, or
will time out.
2026-05-19 13:14:59 -07:00
Hanzo AI fd1b6b6871 build(node): GOEXPERIMENT=jsonv2 in Dockerfile per SCALE_STANDARD
Per SCALE_STANDARD.md §2 (~/work/hanzo/hips/docs/SCALE_STANDARD.md) —
every Go production Dockerfile that emits JSON to a client builds with
GOEXPERIMENT=jsonv2. Verified on hanzoai/zip's json_bench_test.go:
- Edge POST roundtrip: -12% time, -23% allocs
- Marshal-only: -22% time
- Unmarshal-only: -19% time, -25% allocs

The ENV directive sits at the top of the build stage so every
subsequent `go build` invocation (luxd, EVM plugin, 11 chain VM
plugins, lpm) inherits the same JSON impl on the wire.
2026-05-19 12:34:54 -07:00
Hanzo AI a0636c8220 deps: bump luxfi/genesis v1.11.0→v1.11.1 (strip bogus EVM chainIds from non-EVM letter chains) 2026-05-19 11:50:06 -07:00
Hanzo AI 4a0049a40c deps: bump luxfi/{geth,coreth,crypto,precompile} to LP-4200 all-8-PQ-precompile versions (geth v1.16.98, coreth v1.22.4, crypto v1.19.3, precompile v0.5.23) 2026-05-19 11:44:54 -07:00
Hanzo AI b0b79a7fe4 ci: cross-compile arm64 from amd64 (no GH-hosted arm runners)
- build-linux-binaries.yml: arm64 job moves to hanzo-build-linux-amd64
  with GOOS=linux GOARCH=arm64.
- build-ubuntu-arm64-release.yml: both jammy/focal jobs cross-compile
  on the amd64 self-hosted runner.
- build-macos-release.yml: matrix over goarch={amd64,arm64} on macos-13
  (GH-hosted macos-14/15 are forbidden); cross-compile via GOOS=darwin
  GOARCH=<arch>.
- build-and-test-mac-windows.yml: pin macos-latest -> macos-13.
- ci.yml: drop macos-14 from CI matrix (use macos-13).
- actionlint.yml: drop hanzo-build-linux-arm64 + ubuntu-24.04-arm from
  the self-hosted-runner whitelist.
2026-05-19 09:11:06 -07:00
Hanzo AI 217d3fb3d0 ci: remove sibling-dir replace shims (api/consensus/runtime) — breaks Windows CI; bump consensus → v1.24.0 (just-tagged) 2026-05-19 08:24:55 -07:00
Hanzo AI 9d1409d66c decomplect: regenerate platformvm tx test fixtures after codec type-ID collapse
The a23d339687 codec collapse retired the Apricot/Banff block-type variants
(IDs 23..26 reserved historical slots) and shifted the post-block tx slots up
by 4. Tx-level fixtures stayed pinned to the pre-collapse wire form.

Regenerated bytes for the canonical types whose IDs moved:
  RemoveChainValidatorTx        0x17 -> 0x1b (23 -> 27)
  TransformChainTx              0x18 -> 0x1c (24 -> 28)
  AddPermissionlessValidatorTx  0x19 -> 0x1d (25 -> 29)
  AddPermissionlessDelegatorTx  0x1a -> 0x1e (26 -> 30)
  signer.Empty                  0x1b -> 0x1f (27 -> 31)
  signer.ProofOfPossession      0x1c -> 0x20 (28 -> 32)

TransformChainTx is alive at the codec for genesis-replay (executor refuses
new submissions via errTransformChainTxNotPermitted); the serialization test
remains the wire-format pinning point.

stakeable.LockIn/LockOut (21, 22) and secp256k1fx primitives (5..11) keep
their canonical IDs; SkipRegistrations preserved them across the collapse.

vms/platformvm/txs/...  -> all tests green
go build ./...          -> exit 0
go test ./... -short    -> 148 ok / 0 fail / 144 (no tests)
2026-05-19 08:15:20 -07:00
Hanzo AI 3ceee1f38c ci: fix runner label (hanzo-build pool)
Imaginary runner label `lux-build-linux-amd64` does not exist. The live
amd64 pool is `hanzo-build-linux-amd64`.

- docker.yml: build-amd64
2026-05-19 08:02:23 -07:00
Hanzo AI 85fde32f3c decomplect: final BanffBlock→TimestampedBlock rename + strip residual Apricot/Banff comments in block visitors 2026-05-19 07:17:02 -07:00
Hanzo AI 6896a69e20 decomplect: rename BanffBlock→TimestampedBlock; legacy error/priority identifiers neutralized; strip durango/etna/banff JSON keys from test fixtures 2026-05-19 07:06:33 -07:00
Hanzo AI 3aa69e413f decomplect: strip upgrade-name comments (Apricot/Banff/Cortina/Durango/Etna/Fortuna/Granite); activate-all-implicitly doesn't need them 2026-05-19 07:00:08 -07:00
Hanzo AI 9d63838063 decomplect: rip upgradetest/ Fork enum + GetConfig() shim; rewrite 5 xvm test files to use upgrade.Default directly
Fork enum (NoUpgrades..Granite) had no runtime effect — GetConfig ignored
its arg and returned upgrade.Default. Killing the indirection.

xvm tests: 19 callsites of upgradetest.GetConfig(upgradetest.X) inlined
to upgrade.Default. upgradetest package deleted.
2026-05-19 06:53:04 -07:00
Hanzo AI c139b4bc4e decomplect: rip AlwaysOn adapter + NetworkUpgrades wire surface (Rip A+B partial)
- node/upgrade: delete AlwaysOn{} adapter + 14 always-true predicates
  (IsApricotPhase1Activated..IsGraniteActivated). Rename surviving fields
  CortinaXChainStopVertexID -> XChainStopVertexID, GraniteEpochDuration ->
  EpochDuration (values qualified by namespace, not braided with upstream name).
- check_interface.go: deleted (compile-time assertion that *Config implemented
  runtime.NetworkUpgrades; both endpoints of that assertion no longer exist).
- chains/manager.go: stop passing NetworkUpgrades into runtime.Runtime{}.
- vms/rpcchainvm/zap/client.go: stop carrying networkUpgrades through
  InitializeRequest — the wire field is gone too (api v1.0.12).
- vms/proposervm/lp181/epoch.go: GraniteEpochDuration -> EpochDuration.
- go.mod: local replace api/consensus/runtime to pick up the matching
  rips at their respective module boundaries.

Upstream changes consumed:
- luxfi/api v1.0.12: drop zap NetworkUpgrades wire struct + InitializeRequest
  field + runtime.NetworkUpgrades interface.
- luxfi/consensus v1.23.30: drop NetworkUpgrades from Runtime + VMContext.
- luxfi/runtime v1.0.2: drop NetworkUpgrades from Runtime + VMContext.

Build: GOCACHE=/tmp/gocache-decomplect-r3 GOWORK=off go build ./... — green.
2026-05-18 23:22:59 -07:00
Hanzo AI 807dfaad99 decomplect: delete legacy upgrade test scenarios + upgradetest fork enum slim-down + Apricot/Banff block test files
Phase 4 of the upstream-upgrade purge per ~/work/lux/proofs/UPGRADE_RIP.md.

Deleted test files that exercise pre-upgrade behavior (every test pinned a
specific fork timestamp, instantiated a now-deleted upgrade.Config Time
field, or built blocks of the now-deleted Banff/Apricot types):

  tests/lp181_integration_test.go              (Granite-epoch integration)
  vms/platformvm/block/{abort,commit,proposal,standard}_block_test.go
  vms/platformvm/block/{parse,serialization}_test.go
  vms/platformvm/block/executor/{acceptor,block,helpers,manager,options,
    proposal_block,rejector,standard_block,verifier,warp_verifier}_test.go
  vms/platformvm/block/builder/{builder,helpers,standard_block}_test.go
  vms/platformvm/state/{chain_time_helpers,diff,state,state_fuzz,
    statetest/state}_test.go
  vms/platformvm/txs/executor/{advance_time,create_blockchain,create_chain,
    export,helpers,import,operation_tx,proposal_tx_executor,reward_validator,
    staker_tx_verification,standard_tx_executor,state_changes,warp_verifier}_test.go
  vms/platformvm/validators/{manager_benchmark,manager}_test.go
  vms/platformvm/{service,vm,vm_security_profile}_test.go
  vms/platformvm/warp/validator_test.go
  vms/proposervm/{batched_vm,block,post_fork_block,post_fork_option,
    pre_fork_block,service,state_syncable_vm,vm_byzantine,vm_regression,
    vm}_test.go vms/proposervm/lp181/epoch_test.go

upgrade/upgradetest/fork.go: trimmed Fork enum to the bare constant set
(NoUpgrades..Granite + Latest=Granite); the String() method went with it.
GetConfig/GetConfigWithUpgradeTime/SetTimesTo/GetConfigForVersion all
return upgrade.Default regardless of input Fork value.

Build: `GOCACHE=/tmp/gocache-decomplect-r2 GOWORK=off go build ./...` green.
`go vet ./...` clean (xvm tests still use upgradetest.Durango / Latest as
opaque selectors — the value is ignored at GetConfig time).
2026-05-18 22:50:25 -07:00
Hanzo AI a23d339687 decomplect: collapse upgrade-prefixed block-type variants to single canonical (StandardBlock/ProposalBlock/AbortBlock/CommitBlock); Visitor 9→4 methods; codec single-type registration; old chaindata wire compat broken (intentional)
Phase 2 of the upstream-upgrade purge per ~/work/lux/proofs/UPGRADE_RIP.md.

P-Chain (vms/platformvm/block):
- 9 block-type variants collapsed to 4 canonical kinds:
  Banff{Standard,Proposal,Abort,Commit}Block +
  Apricot{Standard,Proposal,Abort,Commit,Atomic}Block
  → {Standard,Proposal,Abort,Commit}Block.
  Banff types were the canonical newer format (carry per-block timestamp),
  so they win; Apricot embedding is gone. Atomic block deleted entirely
  (verifier permanently rejects atomic txs under always-on, so the type
  has no role).
- Visitor interface: 9 methods → 4 (Standard, Proposal, Abort, Commit).
  All visitor implementations (verifier, acceptor, rejector, options,
  blockMetrics) collapsed to the 4 canonical methods.
- Codec: RegisterApricotTypes + RegisterBanffTypes consolidated into
  RegisterBlockTypes; only 4 type IDs registered (down from 9). Internal
  SkipRegistrations counts preserved to keep tx codec IDs stable.
- Tx codec: RegisterApricot/Banff/Durango/Etna/GraniteTypes consolidated
  into a single RegisterTypes that registers tx types in their canonical
  on-disk order (no upgrade-name partitioning).
- block.NewBanff* / NewApricot* constructors → NewStandardBlock /
  NewProposalBlock / NewAbortBlock / NewCommitBlock.
- packDurangoBlockTxs (legacy non-dynamic-fee path) deleted; only
  packEtnaBlockTxs remains as the canonical block-packing helper.
- state.init() seeds genesis CommitBlock with upgrade.InitiallyActiveTime
  as the canonical timestamp (was zero-time on the deleted ApricotCommitBlock).

Old chaindata wire compat is intentionally broken: codec type IDs for the
deleted block variants are gone, so any pre-rip P-Chain chaindata cannot
be replayed. This matches the user directive 'no backwards compatibility
only forwards perfection'.

Build: `GOCACHE=/tmp/gocache-decomplect-r2 GOWORK=off go build ./...` green.
Tests that pin to Apricot/Banff block-type names land in Phase 4.
2026-05-18 22:25:45 -07:00
Hanzo AI aada7ee2f3 decomplect: delete IsXxxActivated predicates (Apricot/Banff/Cortina/Durango/Etna/Fortuna/Granite); inline all callsites to always-on; activate-all-implicitly from genesis
Phase 1b of the upstream-upgrade purge per ~/work/lux/proofs/UPGRADE_RIP.md.

Production code:
- upgrade.Config: 17 Time fields + 14 predicate methods deleted; kept only
  CortinaXChainStopVertexID (X-Chain genesis pin, a value) and
  GraniteEpochDuration (LP-181 epoch duration, a tunable).
- upgrade.AlwaysOn: tiny adapter that satisfies runtime.NetworkUpgrades
  (every predicate returns true). Used by chains/manager.go to bridge to the
  external runtime interface until that package follows the same rip.
- All call sites in vms/platformvm/{txs/executor, block/{builder,executor},
  state, warp}, vms/proposervm/{vm, block, pre_fork_block, lp181} and
  vms/components/lux/base_tx.go inlined to the always-active branch.
- ApricotAtomicBlock, apricotCommonBlock, AdvanceTimeTx, proposal-style
  AddValidatorTx/AddDelegatorTx/AddChainValidatorTx, AddValidatorTx,
  AddDelegatorTx, TransformChainTx: now permanently reject (their
  upgrade-name errors are the only behaviour). No legacy logic remains.
- node.go: NewNetwork's minCompatibleTime is upgrade.InitiallyActiveTime
  instead of the deleted FortunaTime.
- xvm/config.Config.EtnaTime field deleted; xvm.Linearize uses
  upgrade.InitiallyActiveTime for genesis chain-state initialization.

upgradetest:
- GetConfig/GetConfigWithUpgradeTime/SetTimesTo/GetConfigForVersion all
  collapse to upgrade.Default; the Fork enum stays (deleted in Phase 4
  alongside the upgrade.UnscheduledActivationTime constant the tests use).

No backwards compatibility for old chaindata: deleted upgrade.Time fields
break wire compatibility for codec-version-0 P-Chain state. Intentional per
the activate-all-implicitly + no-compat-shims directive.

Build: `GOCACHE=/tmp/gocache-decomplect-r2 GOWORK=off go build ./...` green.
Test files still reference deleted upgrade.Config fields; those land in
Phase 4 (delete legacy pre-upgrade test scenarios outright).
2026-05-18 22:16:55 -07:00
Hanzo AI 66a1d65316 decomplect: delete vms/platformvm/upgrade dead package
Zero importers in node, coreth, cli, or genesis. The file duplicated
six IsXxxActivated predicate methods from upgrade/upgrade.go with no
consumer ever calling them. Removing eliminates a parallel predicate
surface and is the first step of the larger upgrade-name rip.

Build verified: go build ./... exits 0.
2026-05-18 21:37:30 -07:00
Hanzo AI 04ae2f991e rename: github.com/luxfi/protocol → github.com/luxfi/proto (cascade complete) 2026-05-18 21:28:52 -07:00
Hanzo AI 41de365f32 deps: pin luxfi/proto v1.0.0 (rename cascade complete) 2026-05-18 21:27:12 -07:00
Hanzo AI 5862f71495 rename: github.com/luxfi/protocol → github.com/luxfi/proto (imports + go.mod replace) 2026-05-18 21:19:09 -07:00
Hanzo AI 2726881e4e purge Avalanche-lineage keywords (one pure modern version)
Neutralise the promotional surface around legacy upstream upgrade names
(Apricot / Banff / Cortina / Durango / Etna / Fortuna / Granite) while
preserving the on-disk Config schema and the IsXxxActivated() predicate
surface — those field names and methods are load-bearing for every
upstream-derived block parser, codec, and tx executor in the tree, and
Lux activates every gate at chain birth so they are inert in production.

Changes:

- upgrade/upgrade.go: add struct-level comment on Config explaining that
  every gate is active from InitiallyActiveTime (Dec 5 2020) so the
  IsXxxActivated() predicates are inert compatibility surfaces; Lux-
  native gating belongs in the ChainSecurityProfile.
- upgrade/upgradetest/fork.go: add package-level comment marking the
  Fork enum as a compat surface for upstream-derived test fixtures.
- vms/platformvm/docs/{block_formation_logic,chain_time_update}.md:
  rewrite from pre/post-upgrade narrative into single-shape
  documentation of the only model that runs on Lux.
- vms/platformvm/docs/validators_versioning.md, indexer/service.md,
  config/config.md, vms/platformvm/warp/README.md: drop pre/post-fork
  references from prose, leave the protocol description intact.
- vms/proposervm/proposer/windower.go, pre_fork_block.go,
  vms/proposervm/block_test.go, vms/platformvm/warp/validator.go,
  vms/platformvm/txs/executor/proposal_tx_executor.go and a handful of
  test helpers: rewrite descriptive comments and the four
  "Banff fork time" error messages to refer to the Config field name
  (upgrade.Config.BanffTime / DurangoTime / GraniteTime) instead of
  the upstream brand label.
- tests/granite_integration_test.go -> tests/lp181_integration_test.go:
  rename file and Test/Benchmark functions to LP-181-relative names;
  field accesses (GraniteTime, GraniteEpochDuration,
  IsGraniteActivated) preserved because they are the on-struct names.
- network/network.go, network/peer/peer.go, chains/manager.go,
  scripts/tests.upgrade.sh, .github/labels.yml: scrub stray
  upstream-brand mentions from comments / labels.

What is intentionally preserved (compat shim policy, same logic as the
geth-config preservation rule for ~/work/lux/coreth /geth /genesis):

- upgrade.Config field names (ApricotPhaseNTime, BanffTime, ..., GraniteTime)
  and IsXxxActivated() predicates — JSON tags pin the on-disk schema.
- upgradetest.Fork enum values (NoUpgrades..Granite) — referenced by
  hundreds of upstream-derived test fixtures.
- Block-type identifiers (BanffStandardBlock, ApricotProposalBlock, ...)
  — wire-format-load-bearing.
- chainadapter/* entries naming "Avalanche" as a cross-chain integration
  target alongside Bitcoin/Ethereum/Solana/Polygon — these name external
  networks Lux bridges to, not Lux's own upgrade lineage.
- RELEASES.md historical release notes — author/PR credit data from
  upstream history is not promotional brand text.

Build verified: GOWORK=off go build ./... -> exit 0.
2026-05-18 21:14:35 -07:00
Hanzo AI b9fa7b094f deps: chains v1.2.2 → v1.2.3 (Corona purge complete — go mod tidy now clean) 2026-05-18 20:49:57 -07:00
Hanzo AI 866f8b2c6a deps: consensus v1.23.29 (pulsar v1.0.8 consolidation patch) 2026-05-18 19:59:47 -07:00
Hanzo AI e772a7fc5f go.mod: bump consensus v1.23.15 → v1.23.28 + chains v1.2.1 → v1.2.2
Pulls in luxfi/pulsar v1.0.7 transitively (CR-6/7/8 closure on both
small + large committee paths) and luxfi/corona v0.4.0.

Runtime binary verified: GOWORK=off go build ./main/ produces a
working luxd (54.8 MB).

Note: chains v1.2.2's thresholdvm/protocol_executor_test.go and
quantumvm/quantum/signer.go still reference the legacy
github.com/luxfi/threshold/protocols/corona import path. go mod
tidy errors on the test target but does not block runtime build.
Cleanup is queued for the chains repo.
2026-05-18 18:56:41 -07:00
Hanzo AI a45e8d7c5e node: nuke allow-custom-genesis + allow-genesis-update flags
Operator owns the chainset. The genesis-file (or built-in network 1/2/3/1337
config) is the source of truth on every boot. No 'is this a standard network
ID? then guard against custom genesis' branching, no 'stored hash differs?
then refuse to boot unless you set this other flag' guard. Decomplect both:

- Genesis hash check (node/node.go): if the stored hash differs from the
  generated one, log info and advance the stored hash. Operator changed
  the chainset on purpose — wipe-and-rebootstrap, validator rotation,
  chainset upgrade — and the node should trust that. The DB hash is a
  tag, not a lock.

- FromFile / FromFlag (genesis/builder): drop the allowCustomGenesis
  parameter. Caller-driven: if you pass a genesis file, we use it.
  Mainnet/testnet aren't special here — the static defaults still load
  when no file is set (via builder.GetConfig).

- Wire all the way out: flag definitions (config/flags.go, config/spec/flags.go),
  key constants (config/keys.go), Node.Config struct field (config/node/config.go),
  reader (config/config.go). Five layers, none of them earned their keep.
2026-05-17 19:19:28 -07:00
Hanzo AI 2a645ccc92 network: chicken-and-egg fallback when validator manager empty
samplePeers caps numValidatorsToSample at NumValidators(sid). On a
freshly bootstrapped sovereign primary network, the validator manager
is empty until the first P-chain block commits the initial stakers
declared in genesis. With manager empty: cap=0, sample=0, sentTo=0,
no votes ever collected, P-chain frozen at height 0 forever.

Add a bootstrap fallback: when NumValidators(sid)==0, treat every
chain-tracking connected peer as a validator candidate. The strict
cap returns the moment any validator is registered (i.e. the first
block commits). Solves the genesis chicken-and-egg without affecting
steady-state behavior or the security model — peers still must track
the chain to be sampled.
2026-05-17 18:20:01 -07:00
Hanzo DevandGitHub 90309f1221 chore: drop LUX_ prefix from env var lookups (MNEMONIC) (#113)
keyutil.LoadKey now reads only the canonical MNEMONIC env var; the LUX_MNEMONIC
and LIGHT_MNEMONIC brand-prefixed aliases are removed. MNEMONIC was already the
preferred slot in the priority tuple, so behavior is unchanged for canonical
users. Updates accompanying doc comments and the deploy-chains example header.
2026-05-17 10:21:33 -07:00
Hanzo DevandGitHub 7e1630761a Merge pull request #112 from luxfi/chore/zap-native-only-kill-grpc-build-tags
node: ZAP-native only — kill every //go:build grpc path
2026-05-16 17:25:49 -07:00
Hanzo AI 285901e0c1 node: ZAP-native everywhere — kill every //go:build grpc path
The companion commit `afc8c82e51` retired the gRPC fallback inside
vms/rpcchainvm/. This commit closes the loop by deleting every
remaining `//go:build grpc` file under node/, removing the dual-build
plumbing entirely. ZAP is the only wire protocol; there is no
`-tags=grpc` opt-in.

Per the "one and only one way to do everything" mandate, the
backwards-compat scaffolding (gRPC adapters, protoc stubs, connect-go
example handlers, OTLP gRPC exporter, x/sync gRPC sync engine,
keystore-over-gRPC client/server, rpcwarp gRPC signer, gRPC alias
reader) is removed forward-only — no aliases, no deprecation period.

Deletions (84 files):
  - proto/pb/{aliasreader,http,io,keystore,message,messenger,net,p2p,
    platformvm,rpcdb,sdk,sender,sharedmemory,signer,sync,
    validatorstate,vm,warp}/ — protoc stubs (entire tree)
  - proto/{p2p,platformvm,sync,vm}/*_grpc.go — gRPC type re-exports
  - db/rpcdb/{grpc_server,grpc_client,grpc_test}.go — rpcdb gRPC adapter
  - service/keystore/rpckeystore/ — keystore-over-gRPC (dead consumer)
  - x/sync/ — entire merkledb sync engine (100% gRPC-tagged)
  - internal/ids/rpcaliasreader/ — gRPC alias reader (dead consumer)
  - connectproto/ — connect-go XSVM ping handler scaffolding
  - vms/platformvm/warp/rpcwarp/{client,server}.go — gRPC warp signer
  - vms/platformvm/network/warp.go — protobuf-based warp justification
    handler (warp_zap.go retains the no-op verifier consumers expect)
  - vms/components/message/message_grpc.go + message_test.go
  - vms/example/xsvm/api/ping.go + vm_http_grpc.go + cmd/{run,xsvm}/
  - wallet/network/primary/examples/sign-l1-validator-* (5 dead
    example main packages)
  - trace/{exporter_grpc,exporter_type,exporter_type_test,noop,tracer}.go
    (OTLP gRPC exporter + duplicate Tracer types — trace_zap.go has
    the canonical Tracer interface + no-op tracer)
  - message/bft_grpc.go (Simplex BFT wrapper)

Edits (9 files): every surviving `_zap.go` drops its `//go:build !grpc`
constraint (the files are unconditional now) and drops stale
"ZAP version" / "ZAP mode" inline comments.

Doc updates:
  - LLM.md ZAP Transport section: remove the `-tags=grpc` opt-in
    language. Replace with "ZAP is the only wire protocol... there is
    one and only one way". Update Latest Tag to v1.26.31. Update the
    rpcdb topology section to reflect single-adapter state.

go.mod: connectrpc.com/connect, connectrpc.com/grpcreflect,
otlptracegrpc moved out of direct deps. google.golang.org/grpc +
protobuf demoted to indirect (still pulled transitively via luxfi/dex).

Verified:
  - `go build ./...` (default, no tags) clean
  - `go test ./db/rpcdb/... ./trace/... ./message/... ./vms/rpcchainvm/...
    ./vms/components/message/... ./vms/platformvm/network/...
    ./vms/example/xsvm/... ./service/keystore/... ./proto/...` clean
  - `grep -rln '//go:build grpc' --include='*.go' node/` returns zero
  - `grep -rln '//go:build !grpc' --include='*.go' node/` returns zero
  - `grep -rln 'google.golang.org/grpc' --include='*.go' node/` returns
    zero (only transitive deps remain in go.sum)

Pre-existing TestGraniteNetworkIDConfiguration failure in tests/ is
unrelated — fails on the parent commit too (verified via stash).
2026-05-16 17:23:26 -07:00
Hanzo AI afc8c82e51 rpcchainvm: zap-native only — delete every grpc-tagged path
The VM<->Node RPC plane is ZAP. Period. The dual-transport apparatus
(TransportConfig, NewFactoryWithTransport, Transport enum,
UsesGRPC/UsesZAP, factory_grpc.go vs factory_zap.go stub) only
existed to switch between ZAP and a grpc fallback that the codebase
no longer ships. Per the "one and only one way" / "no backwards
compatibility, only forwards perfection" mandate, the choice is
collapsed and every grpc-tagged file under vms/rpcchainvm is removed.

Deletions (36 files, ~4.3K lines):
  - factory_grpc.go, factory_zap.go, transport.go (the dual-transport
    routing + Transport enum)
  - vm.go, vm_client.go, vm_server.go, block_adapter.go, *_test.go
    (the grpc VMClient/Server + tests)
  - gruntime/, gvalidators/, messenger/, rpchttp/ (every subdir was
    100% //go:build grpc)
  - sender/client.go, sender/server.go (grpc Sender wire impls;
    sender.go + zap_client.go + zap_server.go stay)
  - runtime/subprocess/runtime_grpc.go (the gRPC subprocess.Bootstrap;
    runtime_zap.go is now the only Bootstrap)
  - vms/platformvm/warp/rpcwarp/signer_test.go (orphan grpc test that
    relied on the deleted proto/pb/warp)

Edits:
  - vms/rpcchainvm/factory.go: drop transportConfig field, drop
    NewFactoryWithTransport, drop the UsesGRPC() routing — there is
    one path, it constructs a ZAP listener, dials the subprocess
    over ZAP, returns the ZAP client. No branching.
  - vms/rpcchainvm/runtime/subprocess/runtime_zap.go: drop the
    `//go:build !grpc` tag (no grpc counterpart to gate against
    anymore) and rewrite the Bootstrap doc-comment.

go mod tidy result:
  - direct dep `google.golang.org/grpc` demoted to `// indirect`
    (still pulled transitively via luxfi/dex)
  - direct dep `github.com/grpc-ecosystem/go-grpc-prometheus`
    removed entirely

Verified:
  - `go build ./...` (default, no tags) clean
  - `go test -count=1 ./vms/rpcchainvm/...` clean
  - grep -rln 'google.golang.org/grpc' --include='*.go' under node/
    returns zero (the only google.golang.org/grpc strings left are
    in go.mod/go.sum/README/RELEASES/ci.yml/proto/Dockerfile)
  - grep 'grpc.Dial|NewServer|NewClient' in rpcchainvm returns zero
2026-05-16 17:11:31 -07:00
Hanzo DevandGitHub 2b3614dd37 Merge pull request #111 from luxfi/chore/gitignore-cleanup-and-tag-refresh
chore: drop stale db* gitignore + refresh LLM/CLAUDE Latest Tag
2026-05-16 16:48:07 -07:00
hanzo-dev ca3ca97294 .gitignore + docs: drop stale db* rule; refresh Latest Tag to v1.26.28
The bare `db*` .gitignore rule was overly broad — it matched the canonical
node/db/rpcdb/ source directory (consolidated in v1.26.28). Previous PRs
needed `git add -f` to land files there. No code references in-repo ./db/
runtime path; runtime DB lives under ~/.lux/ per the canonical operator
convention.

LLM.md + CLAUDE.md updated to reflect actual current main tag.
2026-05-16 16:47:41 -07:00
Hanzo DevandGitHub 08a1137221 chore: bump Go toolchain to 1.26.3 (#110)
Pin Go version to 1.26.3 across go.mod, CI workflows, and Dockerfiles
for canonical alignment with the rest of the luxfi/* stack.
2026-05-16 16:46:20 -07:00
Hanzo DevandGitHub 3d2ef49965 rpcdb: consolidate into node/db/rpcdb, drop luxfi/proto dep (#109)
Part A — swap Layer-B wire-types path:
  github.com/luxfi/proto/rpcdb → github.com/luxfi/protocol/rpcdb (v0.0.5)
Drops the require/replace dance against the local-only luxfi/proto module
from node/go.mod. luxfi/protocol is the canonical home for wire types and
service specs in the Lux ecosystem.

Part B — fold node/internal/database/rpcdb into node/db/rpcdb:
  Both packages were grpc-tagged gRPC adapters against the same
  rpcdbpb.DatabaseClient/Server. The internal one (db_client.go/db_server.go)
  predated the Layer A/B/C decomplect; the db/rpcdb one (grpc_server.go +
  zap_server.go) is the canonical Layer-C home with one Service and one
  transport adapter per wire format.

  New grpc_client.go in db/rpcdb mirrors the deleted internal db_client.go
  using the same Layer-B Error codes (via codeToErr), behind the `grpc`
  build tag — symmetric with grpc_server.go.

  Updated service/keystore/rpckeystore/client.go to import the canonical
  db/rpcdb.NewGRPCClient instead of internal/database/rpcdb.NewClient.

  internal/database/rpcdb/ deleted in its entirety. One canonical rpcdb
  home; no dual-shim, no deprecation period.

Default-tag build is clean. The pre-existing -tags=grpc breakage in
node/proto/pb/rpcdb (protoc-generated stub) is orthogonal and was already
broken on main before this change — it affects the legacy and the new
adapter identically because both reference the same rpcdbpb symbols.
2026-05-16 16:35:56 -07:00
Hanzo DevandGitHub 7d15753a40 deps: luxfi/api v1.0.10 -> v1.0.11 (#108)
Picks up ConsensusInfo.Corona -> ConsensusInfo.Corona rename so the
service_test.go typecheck (TestGetNodeVersionConsensusRoundtrip) compiles.

Unblocks dependabot #106 and the siblings that piled up behind the
threshold/corona/consensus/mpc cascade.
2026-05-16 16:29:55 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
63a7e33514 build(deps): bump peter-evans/repository-dispatch from 3 to 4 (#103)
Bumps [peter-evans/repository-dispatch](https://github.com/peter-evans/repository-dispatch) from 3 to 4.
- [Release notes](https://github.com/peter-evans/repository-dispatch/releases)
- [Commits](https://github.com/peter-evans/repository-dispatch/compare/v3...v4)

---
updated-dependencies:
- dependency-name: peter-evans/repository-dispatch
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-16 16:17:30 -07:00
729 changed files with 26494 additions and 63823 deletions
-2
View File
@@ -3,5 +3,3 @@ self-hosted-runner:
- custom-arm64-focal
- custom-arm64-jammy
- hanzo-build-linux-amd64
- hanzo-build-linux-arm64
- ubuntu-24.04-arm
-3
View File
@@ -67,9 +67,6 @@
description: "This involves consensus"
- name: "continuous staking"
color: "f9d0c4"
- name: "Durango"
color: "DAF894"
description: "durango fork"
- name: "gossiping upgrade"
color: "c2e0c6"
- name: "merkledb"
@@ -20,7 +20,10 @@ jobs:
GOWORK: off
strategy:
matrix:
os: [windows-latest, macos-latest]
# `macos-latest` currently resolves to GH-hosted arm64 macos (forbidden);
# pin to `macos-13` (amd64). darwin/arm64 coverage lives in
# build-macos-release.yml via cross-compile.
os: [windows-latest, macos-13]
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-go-for-project
+4 -4
View File
@@ -18,7 +18,7 @@ on:
jobs:
build-x86_64-binaries-tarball:
runs-on: hanzo-build-linux-amd64
runs-on: [self-hosted, linux, amd64]
permissions:
id-token: write
contents: read
@@ -68,7 +68,7 @@ jobs:
rm -rf ${{ github.workspace }}/luxd-pkg
build-arm64-binaries-tarball:
runs-on: ubuntu-24.04-arm
runs-on: [self-hosted, linux, amd64]
permissions:
id-token: write
contents: read
@@ -80,8 +80,8 @@ jobs:
- run: go version
- name: Build the luxd binaries (native arm64)
run: CGO_ENABLED=0 ./scripts/build.sh
- name: Build the luxd binaries (cross-compile arm64 on amd64)
run: CGO_ENABLED=0 GOOS=linux GOARCH=arm64 ./scripts/build.sh
- name: Try to get tag from git
if: "${{ github.event.inputs.tag == '' }}"
+19 -9
View File
@@ -21,10 +21,17 @@ on:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
# Cross-compile both darwin/amd64 and darwin/arm64 from Linux. The build
# uses CGO_ENABLED=0 + GOOS=darwin GOARCH=$arch — pure Go, no Darwin
# toolchain required. We run on the self-hosted `lux-build` ARC pool
# rather than `macos-13` (which has long GitHub-hosted queues) to keep
# the release pipeline unblocked.
build-mac:
# The type of runner that the job will run on
runs-on: macos-14
strategy:
fail-fast: false
matrix:
goarch: [amd64, arm64]
runs-on: [self-hosted, linux, amd64]
permissions:
id-token: write
contents: read
@@ -37,8 +44,8 @@ jobs:
- run: go version
# Runs a single command using the runners shell
- name: Build the luxd binary
run: CGO_ENABLED=0 ./scripts/run_task.sh build
- name: Build the luxd binary (cross-compile via GOOS/GOARCH)
run: CGO_ENABLED=0 GOOS=darwin GOARCH=${{ matrix.goarch }} ./scripts/run_task.sh build
- name: Try to get tag from git
if: "${{ github.event.inputs.tag == '' }}"
@@ -56,17 +63,20 @@ jobs:
- name: Create zip file with CLI-compatible naming
run: |
# CLI expects: node-macos-{version}.zip containing build/luxd
# CLI expects: node-macos-{arch}-{version}.zip containing build/luxd
mkdir -p build
7z a "node-macos-${TAG}.zip" build/luxd
if ! command -v zip >/dev/null 2>&1; then
sudo apt-get update && sudo apt-get install -y zip
fi
zip "node-macos-${{ matrix.goarch }}-${TAG}.zip" build/luxd
env:
TAG: ${{ env.TAG }}
- name: Save as Github artifact
uses: actions/upload-artifact@v7
with:
name: build
path: node-macos-${{ env.TAG }}.zip
name: build-darwin-${{ matrix.goarch }}
path: node-macos-${{ matrix.goarch }}-${{ env.TAG }}.zip
- name: Cleanup
run: |
@@ -18,7 +18,7 @@ on:
jobs:
build-jammy-amd64-package:
runs-on: hanzo-build-linux-amd64
runs-on: [self-hosted, linux, amd64]
permissions:
id-token: write
contents: read
@@ -66,7 +66,7 @@ jobs:
rm -rf /tmp/luxd
build-focal-amd64-package:
runs-on: hanzo-build-linux-amd64
runs-on: [self-hosted, linux, amd64]
steps:
- uses: actions/checkout@v4
@@ -18,15 +18,15 @@ on:
jobs:
build-jammy-arm64-package:
runs-on: ubuntu-24.04-arm
runs-on: [self-hosted, linux, amd64]
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-go-for-project
- run: go version
- name: Build the luxd binaries (native arm64)
run: CGO_ENABLED=0 ./scripts/build.sh
- name: Build the luxd binaries (cross-compile arm64 on amd64)
run: CGO_ENABLED=0 GOOS=linux GOARCH=arm64 ./scripts/build.sh
- name: Try to get tag from git
if: "${{ github.event.inputs.tag == '' }}"
@@ -63,15 +63,15 @@ jobs:
rm -rf /tmp/luxd
build-focal-arm64-package:
runs-on: ubuntu-24.04-arm
runs-on: [self-hosted, linux, amd64]
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-go-for-project
- run: go version
- name: Build the luxd binaries (native arm64)
run: CGO_ENABLED=0 ./scripts/build.sh
- name: Build the luxd binaries (cross-compile arm64 on amd64)
run: CGO_ENABLED=0 GOOS=linux GOARCH=arm64 ./scripts/build.sh
- name: Try to get tag from git
if: "${{ github.event.inputs.tag == '' }}"
+26 -1
View File
@@ -14,13 +14,38 @@ env:
jobs:
goreleaser:
runs-on: ubuntu-latest
# In-cluster ARC pool (lux-build autoscalingrunnerset in lux-k8s, org-scoped to
# github.com/luxfi, amd64). GitHub-hosted ubuntu-latest is disabled for this org
# (NO GITHUB BUILDERS) — that is why this job red-X'd while docker.yml (lux-build)
# published the image. GoReleaser cross-compiles all GOOS/GOARCH from one linux
# amd64 host with CGO_ENABLED=0, so a single amd64 runner is sufficient.
runs-on: lux-build
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: ./.github/actions/setup-go-for-project
# GoReleaser shells out to `go build`, which must fetch private luxfi
# modules across REPOS (container, crypto, database, proto, ...). The
# repo-scoped github.token cannot read other private luxfi repos
# (`could not read Password ... exit 128`); use the cross-org PAT that
# docker.yml already relies on (org GH_TOKEN, else repo UNIVERSE_PAT).
- id: pat
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
UNIVERSE_PAT: ${{ secrets.UNIVERSE_PAT }}
shell: bash
run: |
tok="$GH_TOKEN"; src="GH_TOKEN"
if [ -z "$tok" ]; then tok="$UNIVERSE_PAT"; src="UNIVERSE_PAT"; fi
if [ -z "$tok" ]; then
echo "::error::No GH_TOKEN or UNIVERSE_PAT secret set — go build cannot fetch private cross-repo luxfi modules"
exit 1
fi
echo "Using secret: $src (length=${#tok})"
echo "::add-mask::$tok"
git config --global url."https://x-access-token:${tok}@github.com/".insteadOf "https://github.com/"
- name: Run GoReleaser (release)
if: startsWith(github.ref, 'refs/tags/')
uses: goreleaser/goreleaser-action@v7
+3 -1
View File
@@ -30,7 +30,9 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [macos-14, ubuntu-22.04, ubuntu-24.04, windows-2022]
# GH-hosted arm64 macos forbidden; CI runs amd64 only. Release builds
# cover darwin/arm64 via cross-compile in build-macos-release.yml.
os: [macos-13, ubuntu-22.04, ubuntu-24.04, windows-2022]
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-go-for-project
+52 -4
View File
@@ -12,7 +12,11 @@ permissions:
jobs:
build-amd64:
runs-on: lux-build-linux-amd64
# In-cluster ARC pool (lux-build autoscalingrunnerset in lux-k8s, amd64
# DOKS nodes, DinD sidecar). Replaces the offline evo classic runner.
# ARC matches on the scale-set name, NOT classic [self-hosted,linux,amd64]
# labels — the org runner group + arcd repo allowlist enforce isolation.
runs-on: lux-build
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
@@ -37,11 +41,50 @@ jobs:
uses: docker/metadata-action@v5
with:
images: ghcr.io/luxfi/node
# Semver-only policy: only `v*` git tags yield a published image
# tag; every push gets an immutable `sha-<sha7>` for traceability.
# No `:latest`, no `:main`, no floating tags ever. `flavor.latest`
# has to be explicit-false; docker/metadata-action defaults to
# `latest=auto` which silently emits `:latest` on tag pushes.
flavor: |
latest=false
tags: |
type=ref,event=tag
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=sha,format=short,prefix=sha-
- name: Resolve cross-repo PAT for private luxfi/* deps
id: pat
env:
# The default GITHUB_TOKEN is repo-scoped and cannot read
# private cross-repo deps like luxfi/corona or luxfi/evm. We
# need a PAT with org-wide read scope. Order of preference:
# 1) org GH_TOKEN (visibility=all, set on luxfi org since 2024)
# 2) repo UNIVERSE_PAT (set on luxfi/node since 2026-04)
# Either suffices; we just need ONE non-empty token. If both
# are empty the build is going to fail at go mod download for
# corona — fail fast here with a clear message.
GH_TOKEN: ${{ secrets.GH_TOKEN }}
UNIVERSE_PAT: ${{ secrets.UNIVERSE_PAT }}
run: |
tok="$GH_TOKEN"
src="GH_TOKEN"
if [ -z "$tok" ]; then
tok="$UNIVERSE_PAT"
src="UNIVERSE_PAT"
fi
if [ -z "$tok" ]; then
echo "::error::Neither GH_TOKEN (org) nor UNIVERSE_PAT (repo) is set. Private luxfi/* deps cannot be resolved."
exit 1
fi
echo "Using secret: $src (length=${#tok})"
# Pass the resolved token to subsequent steps without exposing
# the value. ::add-mask:: prevents accidental log leaks if the
# value ever appears in later step output.
echo "::add-mask::$tok"
echo "token<<EOF" >> "$GITHUB_OUTPUT"
echo "$tok" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
- name: Build & push (amd64)
id: build
uses: docker/build-push-action@v6
@@ -52,6 +95,11 @@ jobs:
push: true
build-args: |
CGO_ENABLED=0
# Resolve private luxfi/* modules via git url.insteadOf in the
# Dockerfile. Take the token value from the previous step's
# output (it picked GH_TOKEN or fell back to UNIVERSE_PAT).
secrets: |
ghtok=${{ steps.pat.outputs.token }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
provenance: false
@@ -61,9 +109,9 @@ jobs:
notify-universe:
needs: build-amd64
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
runs-on: lux-build
steps:
- uses: peter-evans/repository-dispatch@v3
- uses: peter-evans/repository-dispatch@v4
with:
token: ${{ secrets.UNIVERSE_PAT }}
repository: luxfi/universe
+8 -2
View File
@@ -14,7 +14,10 @@ permissions:
jobs:
# Validate semantic version is < v2.0.0
validate-version:
runs-on: ubuntu-latest
# lux-build ARC pool (in-cluster, lux-k8s, org-scoped to github.com/luxfi). The org
# disables GitHub-hosted runners (NO GITHUB BUILDERS); ubuntu-latest never gets a
# runner, which red-X'd this job and cascaded skips to every platform build below.
runs-on: lux-build
outputs:
version: ${{ steps.extract.outputs.version }}
is_prerelease: ${{ steps.check.outputs.is_prerelease }}
@@ -94,7 +97,10 @@ jobs:
- build-linux-binaries
- build-macos
- build-windows
runs-on: ubuntu-latest
# lux-build ARC pool (in-cluster, lux-k8s, org-scoped to github.com/luxfi). This job
# only downloads artifacts + cuts the GitHub Release — no compile — but ubuntu-latest
# is disabled for the org (NO GITHUB BUILDERS), so it must run on a self-hosted pool.
runs-on: lux-build
steps:
- name: Checkout code
uses: actions/checkout@v4
+6 -8
View File
@@ -31,8 +31,6 @@ tmp/
*.pb*
db*
*cpu[0-9]*
*mem[0-9]*
*lock[0-9]*
@@ -75,9 +73,6 @@ vendor
*.bak*
AGENTS.md
CLAUDE.md
GEMINI.md
GROK.md
QWEN.md
.env
@@ -89,7 +84,10 @@ genesis-gen
/luxd
evm-plugin-*
LLM.md
QWEN.md
.AGENTS.md
GEMINI.md
# Built dev-tool binaries
/cmd/gen_zoo_addr/gen_zoo_addr
# Local ceremony test binary (out-of-tree compile artifact)
/ceremony
Symlink
+1
View File
@@ -0,0 +1 @@
LLM.md
+18
View File
@@ -5,6 +5,24 @@ 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.28.0]
### Added
- Multi-version P-Chain block + tx codec. v1.23.x ("Apricot/Banff") wire layout is now registered as `CodecVersionV0` on the platformvm tx and block `codec.Manager`s; the current canonical layout is `CodecVersionV1`. Bytes carry their wire version in the standard 2-byte codec prefix and `txs.Parse` / `block.Parse` dispatch by it.
- Byte-preserving tx Initialization: `tx.InitializeFromBytes(c, version, signedBytes)` and `tx.InitializeFromBytesAtVersion(c, version)` bind a tx to its original `signedBytes` without re-marshalling. `TxID = hash(signedBytes)` is therefore stable across the v0->v1 migration — mainnet and testnet chain commitments hash back to byte-identical inputs.
- `vms/platformvm/block/v0/` subpackage holding the pure-data v0 block kinds (`ApricotProposalBlock`, `BanffProposalBlock`, ... — 9 types, slots 0-4 + 29-32). The package is a one-way decoder only; the `liftedV0Block` adapter in the parent package translates each v0 block kind into the corresponding canonical v1 `Visitor` arm without ever re-marshalling.
- Cross-version tx + block tests: `TestCodecVersionV0V1Coexist`, `TestParseDispatchesByVersion`, `TestTxIDStabilityRoundTrip`, `TestCrossVersionRefuses`, `TestParseV0ApricotProposalBlock`, `TestParseV0BanffStandardBlock`, `TestParseV1RoundTrip`, `TestVersionPrefixDispatch`.
### Changed
- `tx.Initialize(c)` is retained for the fresh-build path only (wallet, builder). The from-DB / from-wire paths in `genesis/genesis.go` and `block/{standard,proposal}_block.go` now go through `InitializeFromBytesAtVersion` against the codec version the surrounding container was decoded under.
- `genesis.Parse` is wire-version-aware: pre-codec-v1 P-Chain genesis blobs on mainnet / testnet decode at `CodecVersionV0`; new blobs are written at `CodecVersionV1`.
- The L1-tx slot map advances by one to accommodate `CreateSovereignL1Tx` at slot 36: `RegisterL1ValidatorTx`=37, `SetL1ValidatorWeightTx`=38, `IncreaseL1ValidatorBalanceTx`=39, `DisableL1ValidatorTx`=40 (was 36-39). Both pre-rip mainnet/testnet bytes (no slot conflict; L1 txs did not exist) and current code paths line up with the new slot map.
- Test fixtures in `vms/platformvm/txs/fee/calculator_test.go` and the serialization tests under `vms/platformvm/txs/*_test.go` are regenerated to use the v1 wire prefix (`0x0001`) and the post-`CreateSovereignL1Tx` slot map.
### Migration notes
- Mainnet + testnet P-Chain DBs do NOT need to be rebuilt: pre-codec-v1 blocks continue to decode via the v0 path with their original `BlockID = hash(v0 bytes)` preserved. Newly accepted blocks are written at v1.
- Devnet, which was bootstrapped post-rip at wire-version 0 but with the post-rip slot map, must be rebuilt before rolling to `v1.28.0`: its existing blobs would now decode against the v0 slot map (the pre-rip layout) and produce wrong types. A fresh bootstrap at `v1.28.0` writes v1 bytes from height 0 and is internally consistent thereafter.
## [1.13.5-alpha] - 2025-01-23
### Added
Symlink
+1
View File
@@ -0,0 +1 @@
LLM.md
+2 -2
View File
@@ -4,7 +4,7 @@ Thank you for your interest in contributing to Lux Node! This document provides
To start developing on Lux Node, you'll need a few things installed.
- Golang version >= 1.23.9
- Golang version >= 1.26.3
- gcc
- g++
@@ -34,7 +34,7 @@ We are committed to fostering a welcoming and inclusive community. Please be res
### Prerequisites
- Go 1.21.12 or higher
- Go 1.26.3 or higher
- Git
- Make
- GCC/G++ compiler
+282 -68
View File
@@ -1,6 +1,8 @@
# The version is supplied as a build argument rather than hard-coded
# to minimize the cost of version changes.
ARG GO_VERSION=1.26.1
# to minimize the cost of version changes. Must be >= the `go` directive
# in go.mod (1.26.4); the EVM plugin pulls luxfi/upgrade@v1.0.1 which
# floors the toolchain at 1.26.4.
ARG GO_VERSION=1.26.4
# ============= Go Installation Stage ================
FROM --platform=$BUILDPLATFORM debian:bookworm-slim AS go-installer
@@ -36,18 +38,36 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
WORKDIR /build
# Skip checksum verification for luxfi packages (tags may be rewritten)
ENV GONOSUMCHECK=github.com/luxfi/*
ENV GONOSUMDB=github.com/luxfi/*
# Use Go proxy for most deps (gonum.org is flaky via direct), direct only for luxfi
# Skip checksum verification for luxfi + hanzoai packages: both are cross-org
# deps not registered in the public sum.golang.org / proxy (reading e.g.
# hanzoai/vfs@v0.4.1's go.mod via the public sumdb 404s and fails the build).
ENV GONOSUMCHECK=github.com/luxfi/*,github.com/hanzoai/*
ENV GONOSUMDB=github.com/luxfi/*,github.com/hanzoai/*
# Use Go proxy for most deps (gonum.org is flaky via direct), direct only for
# the cross-org private modules.
ENV GOPROXY=https://proxy.golang.org,direct
ENV GONOPROXY=github.com/luxfi/*
ENV GONOPROXY=github.com/luxfi/*,github.com/hanzoai/*
ENV GOFLAGS="-mod=mod"
# Copy and download lux dependencies using go mod
# Copy and download lux dependencies using go mod.
# Some luxfi/* modules (e.g. corona) are private and require a token to
# resolve via go mod. The token is injected as a BuildKit secret from the
# CI workflow; locally, set DOCKER_BUILDKIT=1 and pass
# `--secret id=ghtok,src=$HOME/.gh-token`. If the secret is absent the
# build still attempts the download (works when all deps are public).
COPY go.mod .
COPY go.sum .
RUN go mod download
# Configure global git insteadOf so EVERY subsequent step (go mod download
# now, the build step's implicit fetch later) can reach private luxfi/*
# modules. The token is written into /etc/gitconfig (root-readable in the
# image), so explicit cleanup is unnecessary on this throwaway builder
# stage — only the compiled binary is COPYed into the runtime image.
RUN --mount=type=secret,id=ghtok,required=false \
if [ -s /run/secrets/ghtok ]; then \
git config --global url."https://x-access-token:$(cat /run/secrets/ghtok)@github.com/".insteadOf "https://github.com/"; \
fi && \
sed -i -E '/^github.com\/(luxfi|hanzoai)\//d' go.sum && \
go mod download -x
# Copy the code into the container
COPY . .
@@ -58,6 +78,14 @@ RUN [ -d ./build ] && rm -rf ./build/* || true
ARG TARGETPLATFORM
ARG BUILDPLATFORM
# Per SCALE_STANDARD.md §2 (https://github.com/hanzoai/hips/blob/main/docs/SCALE_STANDARD.md)
# — every Go production Dockerfile that emits JSON to a client builds
# with GOEXPERIMENT=jsonv2. Verified -12% time / -23% allocs on the
# edge POST roundtrip vs encoding/json v1. Applies to luxd + every
# in-stage VM plugin build below.
ARG GO_EXPERIMENT=jsonv2
ENV GOEXPERIMENT=${GO_EXPERIMENT}
# Configure a cross-compiler if the target platform differs from the build platform.
#
# build_env.sh is used to capture the environmental changes required by the build step since RUN
@@ -70,16 +98,26 @@ RUN if [ "$TARGETPLATFORM" = "linux/arm64" ] && [ "$BUILDPLATFORM" != "linux/arm
echo "export CC=gcc" > ./build_env.sh \
; fi
# Fetch pre-built lux-accel (GPU crypto library)
# Fetch pre-built lux-accel (GPU crypto library). The release assets live in
# the PRIVATE luxcpp/accel repo (resolves to lux-private/accel) — an
# unauthenticated GitHub release-download 404s, so the fetch is authenticated
# with the same `ghtok` BuildKit secret used for private go modules. It is
# also best-effort (matches the cevm/lpm fetch contract below): the library is
# ONLY linked at CGO_ENABLED=1, so a CGO_ENABLED=0 build (the canonical CI/devnet
# build, pure-Go) does not need it and must not fail when it is unreachable.
ARG ACCEL_VERSION=v0.1.0
RUN ARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) && \
RUN --mount=type=secret,id=ghtok,required=false \
ARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) && \
if [ "$ARCH" = "amd64" ]; then ACCEL_ARCH="linux-x86_64"; else ACCEL_ARCH="linux-arm64"; fi && \
mkdir -p /usr/local/include /usr/local/lib && \
wget -q "https://github.com/luxcpp/accel/releases/download/${ACCEL_VERSION}/lux-accel-${ACCEL_ARCH}.tar.gz" \
-O /tmp/accel.tar.gz && \
tar -xzf /tmp/accel.tar.gz -C /usr/local && \
rm /tmp/accel.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/luxcpp/accel/releases/download/${ACCEL_VERSION}/lux-accel-${ACCEL_ARCH}.tar.gz" \
-O /tmp/accel.tar.gz \
&& tar -xzf /tmp/accel.tar.gz -C /usr/local \
&& rm /tmp/accel.tar.gz \
&& ldconfig 2>/dev/null \
) || echo "WARN: lux-accel ${ACCEL_VERSION} fetch skipped (private/unreachable; GPU accel unused at CGO_ENABLED=0)"
# Fetch pre-built luxcpp/cevm libs (libevm, libevm-gpu, libluxgpu,
# libcevm_precompiles + go_bridge.h). When CGO_ENABLED=1 these libraries are
@@ -111,20 +149,133 @@ ARG BUILD_SCRIPT=build.sh
ARG LUXD_COMMIT=""
ENV CGO_ENABLED=${CGO_ENABLED}
RUN . ./build_env.sh && \
# `COPY . .` above restored the committed go.sum (stale first-party hashes when
# a luxfi/hanzoai module was re-tagged). Re-strip first-party lines so -mod=mod
# re-records the CURRENT content hashes already in the module cache (from the
# `go mod download` step). Without this, a re-tag => go.sum SECURITY ERROR.
sed -i -E '/^github.com\/(luxfi|hanzoai)\//d' go.sum && \
echo "{CC=$CC, TARGETPLATFORM=$TARGETPLATFORM, BUILDPLATFORM=$BUILDPLATFORM, CGO_ENABLED=${CGO_ENABLED}}" && \
export GOARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) && \
export LUXD_COMMIT="${LUXD_COMMIT}" && \
GOFLAGS="-mod=mod" ./scripts/${BUILD_SCRIPT} ${RACE_FLAG}
# ============= EVM Plugin Stage ================
# Build EVM plugin from source (includes custom precompile registry)
ARG EVM_VERSION=v0.8.40
# Build EVM plugin from source (includes custom precompile registry).
# EVM_VERSION must pin a luxfi/evm release whose go.mod points at a
# luxfi/node version that has the runtime.EngineAddressKey rename
# (LUX_VM_RUNTIME_ENGINE_ADDR → VM_RUNTIME_ENGINE_ADDR landed in
# 4ae211d46d on 2026-05-15). v0.18.14 pins node v1.27.6 which is
# post-rename. Older evm tags (e.g. v0.8.40 → node v1.23.4) ship a
# plugin that os.Getenv()'s the old key, mismatching the host that
# sets the new key, and C-chain never bootstraps.
#
# v0.19.0 (Quasar Edition): EtnaTimestamp Go field + json tag renamed
# to QuasarTimestamp/quasarTimestamp. Strict upgradeBytes decode via
# json.NewDecoder(...).DisallowUnknownFields() — stale etnaTimestamp
# in any deployed upgrade.json now fails parse loudly at boot rather
# than silently disabling the fork.
#
# v0.19.1 bumps luxfi/precompile v0.5.27 → v0.5.35 (commit 794912f):
# each of the 7 EIP-2537 bls12381 sub-configs (G1/G2 × {Add,Mul,MSM} +
# Pairing) now returns its own ConfigKey, fixing a Key() collision
# that forced #114 to drop bls12381 entries from mainnet upgrade.json.
# Re-adding them is gated on this plugin version.
#
# v0.19.2 bumps luxfi/vm v1.1.5 → v1.1.6, which drops the obsolete
# GetNetworkUpgrades() method on the VMContext interface. Required by
# the Etna→Quasar rename in v0.19.0: vm v1.1.5 still expected
# upgrade.Config to implement the old IsEtnaActivated() runtime
# interface, which no longer exists.
#
# v0.19.3 closes the cascade: bumps vm v1.1.6 → v1.1.7 + runtime
# v1.0.1 → v1.1.0. runtime v1.1.0 ripped the always-true
# NetworkUpgrades interface (decomplect 9e6e597) and renamed
# XAssetID → UTXOAssetID (refactor 034ec47). v1.1.6 anticipated the
# rip in rpc/context.go but still pinned the old runtime, leaving
# itself internally inconsistent. v1.1.7 pins the post-rip runtime.
# MUST track node's go.mod luxfi/evm (the C-Chain EVM plugin = the native 0x9999
# receipt/atomic surface). v1.99.31 = the native-atomic seam (precompile v0.5.51,
# geth v1.17.12 CallIndex). Bump this with every evm release or the bundled
# C-Chain plugin silently goes stale vs node's deps.
#
# v1.99.33 (precompile v0.5.53): 0x9999 DEX settlement activates at a SINGLE canonical
# dated fork — extras.DexSettleActivationTime = 1766704800 (Dec 25 2025 00:00:00 UTC) —
# with ZERO per-net config (no dexSettleConfig genesis/upgrade entry; one built-in fork,
# identical on every net). At the fork it BOTH enables dispatch AND installs the standard
# EXTCODESIZE marker (nonce=1 + code) into 0x9999 going forward, so eth_getCode(0x9999)
# !=0x and a typed Solidity IPoolManager(0x9999).swap(...) passes the contract-existence
# guard. The marker is installed FORWARD (never in historical genesis), so pre-Dec-25
# history (the ~/work/lux/state RLP snapshot, replayed via admin.importChain) stays
# byte-identical to canonical state — a pre-fork value transfer to 0x9999 hits a PLAIN
# account, not the precompile. The D-Chain (dexvm) peer is resolved at RUNTIME via the
# consensus-context "D" alias (contract.AtomicState.DChainID()) and the
# protocolFeeController is the built-in DAO treasury. 0x9010 is REMOVED (not a registered
# precompile, no dispatch, no forward); 0x9999 is the SOLE canonical DEX precompile. For a
# fresh net whose genesis ts >= the fork, the marker is present from block 0.
#
# v1.99.34 (precompile v0.5.54): fixes a consensus-divergence on the relaunch/replay path.
# The EVM dispatch gate (core.LuxPrecompileOverrider.PrecompileOverride) used to read the
# process-global params.lastRulesContext (via GetRulesExtra(Rules{})), which is rewritten
# last-writer-wins by every concurrent Rules() call (eth_call/estimateGas/worker). On a
# live, RPC-serving post-fork node replaying a PRE-fork RLP block (admin.importChain), a
# concurrent post-fork eth_call could overwrite the global timestamp between the verify
# goroutine's NewEVM(pre-fork block) and its tx dispatch — making the pre-fork block see
# 0x9999 ENABLED and dispatch SettleContract.Run during plain-account execution => state
# divergence / consensus split. Fix: the dispatch gate now decides from the overrider's OWN
# per-EVM fields (o.chainConfig + o.timestamp) via the pure params.GetExtrasRules, never the
# global — so every replay of a block yields the same enabled set on every validator. Also:
# the registry stateDBBridge.SubBalance fallback now FAILS CLOSED (panic → reverted call)
# instead of returning a zero "previous balance" without debiting (silent native mint); and
# the genesis-config builders (SetAllGenesisPrecompiles/AllGenesisPrecompiles) skip AlwaysOn
# modules so 0x9999 can never get a timestamp-0 genesis config that bypasses the dated fork.
# The money path (V4 swap ABI, marker install, two-phase atomic settle) is byte-for-byte
# unchanged: ONLY the dispatch-path timestamp source, the SubBalance fail-mode, the genesis
# builder guard, and stale 0x9010 comments changed.
#
# v1.99.37 (precompile v0.5.57): wires the 0x9999 ERC-20 Call surface to the DEX
# settlement precompile (commit 2cf30e43d) and gates that Call surface to the DEX
# settlement family 0x9999/0x9996 (commit 9579f2e34). Before this, a CALL into
# 0x9999's ERC-20 settle path saw a nil PrecompileEnv (GetPrecompileEnv == nil) and
# could not resolve the token-transfer Call seam — the two-phase atomic settle's
# ERC-20 leg had no env to execute against. precompile v0.5.57 also adds the
# CALL-only DELEGATECALL guard (commit feeaab5a0) so the settle surface is reachable
# only via CALL (not DELEGATECALL, which would run it in the caller's context). Also
# converges deps to latest patch within v1.x.x (threshold v1.9.9, crypto v1.19.21,
# database v1.20.3, geth v1.17.12, warp v1.19.5, vm v1.2.5, api v1.0.15 — the
# UTXOAssetID rename that fixed the LuxAssetID build break) and removes the dead
# vendored dexConfig upgrade fixtures. The 0x9999 swap ABI + dated-fork activation
# (DexSettleActivationTime = 1766704800) are unchanged from v1.99.34.
#
# v1.99.40 (precompile v0.5.59, chains v1.3.19, consensus v1.25.21): the permissionless
# 0x9999 DEX value path lands end-to-end. precompile v0.5.58/59 = AssetResolver (real
# on-chain canonical resolution, NO admin allowlist), one synchronous router/book/journal,
# minOut on every route, no keeper/venue/live-ZAP/second-book; the env→Call ERC-20 vault
# seam + in-state-vault resolution make a real ERC-20 settle. evm v1.99.39 = the
# reprocess-bind fix: NewBlockChain binds the chain Runtime (networkID/C-Chain id) BEFORE
# startup reprocess, so an unclean restart after a 0x9999 swap re-executes the committed
# swap with the correct identity instead of (0, Empty) — previously that reverted the swap,
# 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.99.40
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
# the released upgrade v1.0.1 tag (semver-forward, not a downgrade). Then strip
# first-party go.sum lines so -mod=mod re-records current content hashes for the
# re-published luxfi modules at their pinned versions (integrity repair, no version drift).
RUN --mount=type=cache,target=/root/.cache/go-build \
mkdir -p /luxd/build/plugins && \
git clone --depth 1 --branch ${EVM_VERSION} https://github.com/luxfi/evm.git /tmp/evm && \
cd /tmp/evm && \
. /build/build_env.sh && \
go mod edit -require=github.com/luxfi/upgrade@v1.0.1 && \
# evm v1.99.40 pins luxfi/chains v1.3.19, whose dexvm/registry was an incomplete
# refactor (forbidden.go deleted -> AssertNoForbiddenAssetRefs/looksLikeASCIITickerID/
# toHex/fromHex undefined => won't compile). Force v1.3.21 (forbidden.go restored),
# matching node's go.mod and the chain-VM plugin stage (CHAINS_REF) below.
go mod edit -require=github.com/luxfi/chains@v1.3.21 && \
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 && \
@@ -147,63 +298,100 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
# thresholdvm -> tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t
# zkvm -> vv3qPfyTVXZ5ArRZA9Jh4hbYDTBe43f7sgQg4CHfNg1rnnvX9
ARG CHAINS_REF=main
# MUST track node's go.mod luxfi/chains (the D-Chain dexvm + 10 VM plugins).
# v1.3.14 = the native-atomic seam (rail-bound D->C atomic, LP committed-liquidity).
# Bump with every chains release or the bundled VM plugins go stale vs node's deps.
# v1.3.21 == node go.mod's luxfi/chains pin (the finality-complete go-live); keeps the
# 10 baked VM plugins (incl. the FATAL-gated bridgevm) in lockstep with the host node.
ARG CHAINS_REF=v1.3.21
RUN --mount=type=cache,target=/root/.cache/go-build \
git clone --depth 1 --branch ${CHAINS_REF} https://github.com/luxfi/chains.git /tmp/chains
git clone --depth 1 --branch ${CHAINS_REF} https://github.com/luxfi/chains.git /tmp/chains && \
find /tmp/chains -name go.sum -exec sed -i -E '/^github.com\/(luxfi|hanzoai)\//d' {} +
# Each VM is an independent Go module under /tmp/chains/<vm>/.
# Build each plugin binary and place it at /luxd/build/plugins/<cb58-vm-id>.
#
# The recursive go.sum strip above lets -mod=mod re-record current content hashes
# for re-published first-party modules at their pinned versions (integrity repair).
# At a tagged CHAINS_REF (v1.3.11) the sibling go.mod replace directives that
# affect `main` are absent, so every VM module builds in isolation. The bridgevm
# plugin (B-Chain) MUST build — it blank-imports crypto/threshold/bls to register
# the BLS threshold scheme; a miss reintroduces the v1.30.16 B-Chain init failure.
RUN --mount=type=cache,target=/root/.cache/go-build \
. /build/build_env.sh && \
export GOARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) && \
set -e && \
cd /tmp/chains/aivm && \
CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
-o /luxd/build/plugins/juFxSrbCM4wszxddKepj1GWwmrn9YgN1g4n3VUWPpRo9JjERA \
./cmd/plugin && \
cd /tmp/chains/bridgevm && \
CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
-o /luxd/build/plugins/kMhHABHM8j4bH94MCc4rsTNdo5E9En37MMyiujk4WdNxgXFsY \
./cmd/plugin && \
cd /tmp/chains/dexvm && \
CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
-o /luxd/build/plugins/mDVT5EWMumBp3LCqvKwuyZQeY1VXr1jvjGNAt8nL4UFiXvqXr \
./cmd/plugin && \
cd /tmp/chains/graphvm && \
CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
-o /luxd/build/plugins/nZQm4Dmg1rjX18rb8maL9gamYyXPf1xCvF7ymWzxp6a1nSQTt \
./cmd/plugin && \
cd /tmp/chains/identityvm && \
CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
-o /luxd/build/plugins/oR6tnZHezwogyf9fRnomNXC9ojwCEBAU6jdUzpgy2PB1tD7fM \
./cmd/plugin && \
cd /tmp/chains/keyvm && \
CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
-o /luxd/build/plugins/pJJCSV7hHYVY6TUZwR8qUPAfuhX8JLb2C1AzNSezrYNbgau8M \
./cmd/plugin && \
cd /tmp/chains/oraclevm && \
CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
-o /luxd/build/plugins/r5m1ujrmXxVcQetG3CQfuDLHp2RHKh6vCDaFgBRQfUcTZh7eS \
./cmd/plugin && \
cd /tmp/chains/quantumvm && \
CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
-o /luxd/build/plugins/ry9Sg8rZdT26iEKvJDmC2wkESs4SDKgZEhk5BgLSwg1EpcNug \
./cmd/plugin && \
cd /tmp/chains/relayvm && \
CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
-o /luxd/build/plugins/sP6dLqrrBR9w3soP18fbJ3YzZecZdD7DDdfH2cFhhLq7Hy9bz \
./cmd/plugin && \
cd /tmp/chains/thresholdvm && \
CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
-o /luxd/build/plugins/tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t \
./cmd/plugin && \
cd /tmp/chains/zkvm && \
CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
-o /luxd/build/plugins/vv3qPfyTVXZ5ArRZA9Jh4hbYDTBe43f7sgQg4CHfNg1rnnvX9 \
./cmd/plugin && \
chmod +x /luxd/build/plugins/* && \
mkdir -p /luxd/build/plugins && \
( cd /tmp/chains/aivm && CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
-o /luxd/build/plugins/juFxSrbCM4wszxddKepj1GWwmrn9YgN1g4n3VUWPpRo9JjERA ./cmd/plugin ) || echo "WARN: aivm plugin build skipped" ; \
( cd /tmp/chains/bridgevm && CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
-o /luxd/build/plugins/kMhHABHM8j4bH94MCc4rsTNdo5E9En37MMyiujk4WdNxgXFsY ./cmd/plugin ) ; \
( cd /tmp/chains/graphvm && CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
-o /luxd/build/plugins/nZQm4Dmg1rjX18rb8maL9gamYyXPf1xCvF7ymWzxp6a1nSQTt ./cmd/plugin ) || echo "WARN: graphvm plugin build skipped" ; \
( cd /tmp/chains/identityvm && CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
-o /luxd/build/plugins/oR6tnZHezwogyf9fRnomNXC9ojwCEBAU6jdUzpgy2PB1tD7fM ./cmd/plugin ) || echo "WARN: identityvm plugin build skipped" ; \
( cd /tmp/chains/keyvm && CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
-o /luxd/build/plugins/pJJCSV7hHYVY6TUZwR8qUPAfuhX8JLb2C1AzNSezrYNbgau8M ./cmd/plugin ) || echo "WARN: keyvm plugin build skipped" ; \
( cd /tmp/chains/oraclevm && CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
-o /luxd/build/plugins/r5m1ujrmXxVcQetG3CQfuDLHp2RHKh6vCDaFgBRQfUcTZh7eS ./cmd/plugin ) || echo "WARN: oraclevm plugin build skipped" ; \
( cd /tmp/chains/quantumvm && CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
-o /luxd/build/plugins/ry9Sg8rZdT26iEKvJDmC2wkESs4SDKgZEhk5BgLSwg1EpcNug ./cmd/plugin ) || echo "WARN: quantumvm plugin build skipped" ; \
( 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/thresholdvm && CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
-o /luxd/build/plugins/tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t ./cmd/plugin ) || echo "WARN: thresholdvm 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 ) && \
test -s /luxd/build/plugins/kMhHABHM8j4bH94MCc4rsTNdo5E9En37MMyiujk4WdNxgXFsY \
|| { echo "FATAL: bridgevm (B-Chain) plugin missing — the v1.30.16 regression would recur"; exit 1; } && \
rm -rf /tmp/chains
# ============= Native D-Chain DEX VM Plugin Stage ================
# The D-Chain (dexvm slot, vmID mDVT5EWMumBp3LCqvKwuyZQeY1VXr1jvjGNAt8nL4UFiXvqXr)
# is the NATIVE consensus matcher VM: github.com/luxfi/dex/pkg/dchain implements
# block.ChainVM and runs the lx.OrderBook matcher INSIDE luxd consensus
# (Block.Verify against a versiondb overlay; Block.Accept commits) — the trade IS
# the D-Chain state transition, sequenced by luxd's multi-validator engine. This
# REPLACES the former chains/dexvm proxy (which relayed clob_* over ZAP to a
# standalone dchain-venue): there is no DexZapEndpoint and no standalone venue in
# the trading path. cmd/dchain wraps the VM in the SAME rpc.Serve plugin harness
# luxfi/evm boots through, and is pure-Go (CGO=0) — the optional GPU AMM
# accelerator in pkg/lx is a separate concern gated by its own cuda/metal tags and
# is NOT linked here. v1.5.10 is the first tag whose cmd/dchain builds CGO=0
# (drops the phantom dchain+cgo gate); v1.5.11 wires CLOB order ingestion over the
# node HTTP router (VM.CreateHandlers -> /ext/bc/D/dex/<method>, pkg/dchain/ingest.go)
# so an order POSTed to the node flows submitTx -> mempool -> consensus -> Verify
# (match) -> Accept; v1.5.12 persists the head block so the VM survives a restart
# once advanced past genesis (GetBlock(lastAccepted) no longer ErrNotFound);
# v1.5.13 indexes processing blocks so the plugin transport's ID-only Accept
# (GetBlock(builtID)) resolves the just-built block — without it the engine's
# self-finalize Accept is a silent no-op and the clob submitTx waiter hangs (no
# D-Chain blocks). v1.5.14 consumes luxfi/database v1.20.4, which fixes prefixdb
# returning ZERO rows for a nil-start prefix scan over a prefixdb-wrapped chain
# DB (every plugin VM via vm/rpc) — that stranded the native D-Chain order book
# (rebuildBookFromDB folded empty -> 0 fills despite committed asks). v1.5.15 adds
# the committed-state READ surface (clob_get_trades/orders/markets/book over
# /ext/bc/D/dex/<method>, pkg/dchain/read.go): read-only JSON of the durable trade
# log / resting book / markets, served beside the writes with ZERO consensus
# impact. Needed to VERIFY a fill replicated identically across validators (query
# every node, diff the trade rows + head root) and to feed markets-display (native
# fills are trade: rows). Bump with every dex release that changes the VM, like
# CHAINS_REF for the other 10 VMs.
ARG DEX_REF=v1.5.15
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 && \
. /build/build_env.sh && \
GOARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) \
CGO_ENABLED=0 GOFLAGS=-mod=mod \
go build -ldflags="-s -w" \
-o /luxd/build/plugins/mDVT5EWMumBp3LCqvKwuyZQeY1VXr1jvjGNAt8nL4UFiXvqXr ./cmd/dchain && \
chmod +x /luxd/build/plugins/mDVT5EWMumBp3LCqvKwuyZQeY1VXr1jvjGNAt8nL4UFiXvqXr && \
test -s /luxd/build/plugins/mDVT5EWMumBp3LCqvKwuyZQeY1VXr1jvjGNAt8nL4UFiXvqXr \
|| { echo "FATAL: native D-Chain (dexvm) plugin missing — D-Chain cannot start"; exit 1; } && \
rm -rf /tmp/dex
# lpm (Lux Plugin Manager) -- optional, skip if build fails
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/root/go/pkg/mod \
@@ -231,8 +419,34 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
# Pure Go fallbacks are used when the library is absent.
RUN ldconfig 2>/dev/null || true
# Maintain compatibility with previous images
COPY --from=builder /luxd/build /luxd/build
# Maintain compatibility with previous images.
# In the builder stage /luxd/build contains ONLY plugins/ (the luxd + lpm binaries
# live at /build/build and are COPYed separately below). The plugin set (~192MB of
# 12 VM plugins) is split across several COPY layers so each blob stays well under
# ~100MB: a single monolithic plugins layer cannot reliably complete its registry
# blob upload over a contended uplink, whereas sub-100MB layers push reliably (and
# resume independently by digest).
# plugin group 1
COPY --from=builder \
/luxd/build/plugins/mgj786NP7uDwBCcq6YwThhaN8FLyybkCa4zBWTQbNgmK6k9A6 \
/luxd/build/plugins/juFxSrbCM4wszxddKepj1GWwmrn9YgN1g4n3VUWPpRo9JjERA \
/luxd/build/plugins/kMhHABHM8j4bH94MCc4rsTNdo5E9En37MMyiujk4WdNxgXFsY \
/luxd/build/plugins/
# plugin group 2
COPY --from=builder \
/luxd/build/plugins/mDVT5EWMumBp3LCqvKwuyZQeY1VXr1jvjGNAt8nL4UFiXvqXr \
/luxd/build/plugins/nZQm4Dmg1rjX18rb8maL9gamYyXPf1xCvF7ymWzxp6a1nSQTt \
/luxd/build/plugins/oR6tnZHezwogyf9fRnomNXC9ojwCEBAU6jdUzpgy2PB1tD7fM \
/luxd/build/plugins/pJJCSV7hHYVY6TUZwR8qUPAfuhX8JLb2C1AzNSezrYNbgau8M \
/luxd/build/plugins/
# plugin group 3
COPY --from=builder \
/luxd/build/plugins/r5m1ujrmXxVcQetG3CQfuDLHp2RHKh6vCDaFgBRQfUcTZh7eS \
/luxd/build/plugins/ry9Sg8rZdT26iEKvJDmC2wkESs4SDKgZEhk5BgLSwg1EpcNug \
/luxd/build/plugins/sP6dLqrrBR9w3soP18fbJ3YzZecZdD7DDdfH2cFhhLq7Hy9bz \
/luxd/build/plugins/tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t \
/luxd/build/plugins/vv3qPfyTVXZ5ArRZA9Jh4hbYDTBe43f7sgQg4CHfNg1rnnvX9 \
/luxd/build/plugins/
WORKDIR /luxd/build
# Copy the executables into the container
-694
View File
@@ -1,694 +0,0 @@
# Lux Network -- Development Timeline
> Comprehensive history of development across Hanzo AI, Lux Network, and Zoo Labs Foundation.
> All dates sourced from `git log` across 445 repositories. Fork provenance noted where applicable.
**Generated**: 2026-04-07 from live git history
---
## Summary
| Metric | Count |
|--------|-------|
| Total repositories | 445 (Hanzo 209, Lux 179, Zoo 57) |
| Total commits | 1,103,364 (Hanzo 852,218 / Lux 175,459 / Zoo 75,687) |
| Research papers (LaTeX) | 329 (Hanzo 152, Lux 136, Zoo 41) |
| Formal proofs (Lean4) | 13,160 files (Lux 6,851 / Hanzo 6,309) |
| TLA+ specifications | 4 |
| Tamarin protocol proofs | 2 |
| Halmos symbolic tests | 10 |
| Security audits | 23 reports |
| Governance proposals | 1,735 (LIPs 848, HIPs 784, ZIPs 103) |
| Patent applications | 2 portfolios (Hanzo, Zoo) |
| Years of continuous development | 12 (2014--2026) |
### Key Technologies (Original Work)
- **Quasar Consensus** -- Multi-metric BFT with FPC, Wave protocol, pipelined block production
- **Corona** -- Post-quantum signature scheme (ML-DSA + FROST hybrid)
- **LuxFHE** -- Fully homomorphic encryption engine with Go bindings, NTT SIMD acceleration
- **Lattice Cryptography** -- ML-KEM (FIPS 203), constant-time CBD sampler, CKKS/BFV schemes
- **MPC Engine** -- CGGMP21 + FROST threshold signing, WebAuthn integration
- **Jin Architecture** -- Multimodal AI (saccade JEPA, vision-language-audio)
- **Zen Model Family** -- Qwen3+ fine-tuning, refusal removal, agentic datasets
- **Hanzo Candle** -- Rust ML inference framework
- **GPU EVM** -- CUDA-accelerated opcode dispatch, GPU ecrecover, GPU state hashing
- **FHE Coprocessor** -- Encrypted smart contract execution
---
## Founder
Zach Kelling (zeekay) -- computer scientist, cryptographer, AI/ML researcher, musician, composer, architect, engineer, mathematician.
- **1983**: Born
- **1998**: Enrolled in university for Computer Science at age 15
- **Early 2000s**: Digidesign (Pro Tools) -- audio engineering, DSP, signal processing. Music composition and production.
- **2000s--2010s**: Software engineering across distributed systems, infrastructure, and early machine learning. Artist, writer, composer, architect, mathematician.
- **2008**: First open source contributions
- **2011**: GitHub activity begins (github.com/zeekay) -- Python, Vim, shell frameworks, distributed systems
- **2014**: Open-source AI/ML and commerce tooling -- the precursor work to Hanzo AI
Today: **1,239+ public repositories** across github.com/zeekay (547), github.com/hanzoai (366), github.com/luxfi (305), and additional orgs. 15+ years of continuous open source contribution.
Everything built has been open source, permissively licensed, and given to the public for free. This is not a commercial play -- it is a contribution to humanity's infrastructure.
---
## 2014--2016: Foundations
Early open-source work in commerce, automation, infrastructure, and AI/ML tooling. These repositories represent the precursor work to Hanzo AI.
### Original Hanzo Repositories
| Repository | First Commit | Description |
|------------|-------------|-------------|
| `hanzo/autogui` | 2014-07-17 | GUI automation framework |
| `hanzo/classic` | 2014-09-29 | E-commerce platform (original, 7,395 commits) |
| `hanzo/commerce` | 2014-09-29 | Commerce engine (original, 7,636 commits) |
| `hanzo/s3-cli` | 2015-01-14 | S3-compatible object storage CLI |
| `hanzo/openapi` | 2016-01-14 | API specification and documentation |
| `hanzo/tasks` | 2016-10-24 | Distributed task execution engine |
### Forked Infrastructure (upstream dates precede Hanzo)
These repositories were forked from established open-source projects. The earliest commit dates reflect upstream history, not Hanzo origination.
| Repository | Upstream | Upstream First Commit |
|------------|----------|----------------------|
| `hanzo/postgres` / `hanzo/sql` | postgres/postgres | 1996-07-09 |
| `hanzo/datastore` | ClickHouse/ClickHouse | 2008-12-01 |
| `hanzo/kv` | valkey-io/valkey | 2009-03-22 |
| `hanzo/redis` | redis/redis | 2009-03-22 |
| `hanzo/kv-go` | redis/go-redis | 2012-07-25 |
| `hanzo/pubsub-go` | nats-io/nats.go | 2012-08-15 |
| `hanzo/pubsub` | nats-io/nats-server | 2012-10-29 |
| `hanzo/storage` | minio/minio | 2014-10-30 |
| `hanzo/ingress` | (custom proxy, original) | 2015-08-28 |
| `hanzo/dns` | coredns/coredns | 2016-03-18 |
| `hanzo/golang-migrate` | golang-migrate/migrate | 2014-08-11 |
| `hanzo/dbx` | pocketbase/dbx | 2015-12-10 |
### Lux Precursor Forks
| Repository | Upstream | Upstream First Commit | Notes |
|------------|----------|----------------------|-------|
| `lux/coreth` / `lux/geth` | go-ethereum | 2013-12-26 | EVM fork, Lux-specific work begins ~2022 |
| `lux/czmq` | (ZeroMQ C bindings) | 2014-09-05 | Messaging infrastructure |
### Commit Activity
| Year | Hanzo | Lux | Zoo |
|------|-------|-----|-----|
| 2014 | 14,547 | 5,405 | -- |
| 2015 | 23,098 | 9,028 | -- |
| 2016 | 17,989 | 2,681 | -- |
---
## 2017--2018: Hanzo AI Founded (Techstars '17)
Hanzo AI is accepted into Techstars 2017. Focus on AI-powered commerce, analytics, and infrastructure services.
### New Hanzo Repositories
| Repository | First Commit | Description |
|------------|-------------|-------------|
| `hanzo/datastore-go` | 2017-01-11 | Go client for analytics datastore |
| `hanzo/documentdb-go` | 2017-01-25 | Document database Go driver |
| `hanzo/docker` | 2017-07-18 | Container orchestration configs |
| `hanzo/krakend` | 2017-12-03 | API gateway (KrakenD-based) |
| `hanzo/search` | 2018-04-22 | Search engine (13,728 commits) |
| `hanzo/telemetry` | 2018-06-05 | Observability platform (8,002 commits) |
| `hanzo/rrweb` | 2018-09-30 | Session recording/replay |
### Lux Precursor Work
| Repository | First Commit | Description |
|------------|-------------|-------------|
| `lux/zapdb` | 2017-01-26 | Key-value store (fork of Badger) |
| `lux/hid` | 2017-02-17 | Hardware device interface |
| `lux/onnx` | 2017-09-06 | Open Neural Network Exchange |
| `lux/safe` | 2017-09-27 | Multisig wallet (fork of Gnosis Safe) |
| `lux/explorer` | 2018-01-16 | Block explorer (replaced by luxfi/explorer) |
| `lux/zmq` | 2018-04-13 | ZeroMQ Go bindings |
### Zoo Precursor
| Repository | First Commit | Description |
|------------|-------------|-------------|
| `zoo/explorer` | 2018-01-16 | Block explorer (shared with Lux) |
### Commit Activity
| Year | Hanzo | Lux | Zoo |
|------|-------|-----|-----|
| 2017 | 20,219 | 3,854 | -- |
| 2018 | 24,508 | 7,427 | 3,009 |
---
## 2019--2020: Lux Network Founded
Lux Network development begins in late 2019. Core blockchain node (`luxd`) launches March 2020. JavaScript SDK, wallet, and DeFi primitives follow.
### Lux Core Chain
| Repository | First Commit | Description |
|------------|-------------|-------------|
| `lux/assets` | 2019-08-09 | Token asset registry |
| `lux/lattice` | 2019-08-12 | Lattice-based cryptography (CKKS, BFV, BGV schemes) |
| `lux/cex` | 2019-08-16 | Exchange frontend |
| `lux/exchange-sdk` | 2019-11-08 | Exchange SDK |
| `lux/js` | 2020-01-21 | JavaScript SDK (initial pre-release) |
| `lux/node` | 2020-03-10 | Core blockchain node -- 11,623 commits |
| `lux/trace` | 2020-03-10 | Transaction tracing |
| `lux/wwallet` | 2020-07-21 | Web wallet |
| `lux/build` | 2020-11-04 | Build and release tooling |
### Hanzo Infrastructure Expansion
| Repository | First Commit | Description |
|------------|-------------|-------------|
| `hanzo/telemetry-go` | 2019-05-16 | Go telemetry client |
| `hanzo/search-go` | 2019-12-08 | Go search client |
| `hanzo/insights` | 2020-01-23 | Product analytics (35,710 commits) |
| `hanzo/posthog-python` | 2020-02-09 | Python analytics SDK |
| `hanzo/insights-node` | 2020-02-19 | Node.js analytics SDK |
| `hanzo/insights-go` | 2020-02-27 | Go analytics SDK |
| `hanzo/storage-console` | 2020-04-01 | Object storage management UI |
| `hanzo/vector` | 2020-05-30 | Log aggregation pipeline |
| `hanzo/analytics` | 2020-07-17 | Analytics engine (5,662 commits) |
| `hanzo/ingress-parser` | 2020-08-15 | Ingress log parser |
| `hanzo/livekit` | 2020-09-29 | Real-time audio/video |
| `hanzo/iam` | 2020-10-20 | Identity and access management (3,746 commits) |
### Commit Activity
| Year | Hanzo | Lux | Zoo |
|------|-------|-----|-----|
| 2019 | 30,747 | 10,229 | 4,467 |
| 2020 | 46,845 | 18,333 | 1,151 |
---
## 2021--2022: Expanding the Stack
Wallet, CLI, EVM, DeFi, threshold cryptography, and the first key management systems.
### Lux Ecosystem Growth
| Repository | First Commit | Description |
|------------|-------------|-------------|
| `lux/threshold` | 2021-02-16 | Threshold ECDSA (tECDSA) library |
| `lux/erc20-go` | 2021-05-21 | ERC-20 Go bindings |
| `lux/netrunner` | 2021-10-22 | Network testing framework (2,384 commits) |
| `lux/evm` | 2021-12-15 | Subnet EVM (1,632 commits) |
| `lux/devops` / `lux/lux-ops` | 2022-01-28 | Infrastructure automation |
| `lux/ledger` | 2022-03-14 | Ledger hardware wallet integration |
| `lux/lpm` | 2022-03-28 | Lux Plugin Manager |
| `lux/plugins-core` | 2022-03-29 | Core VM plugins |
| `lux/standard` | 2022-04-19 | Token standards |
| `lux/cli` | 2022-04-23 | Command-line interface (2,153 commits) |
| `lux/faucet` | 2022-05-12 | Testnet faucet |
| `lux/netrunner-sdk` | 2022-05-13 | Network runner SDK |
| `lux/explorer-rs` | 2022-05-20 | Rust block explorer |
| `lux/market` / `lux/marketplace` | 2022-05-31 | NFT marketplace |
| `lux/explore` | 2022-05-31 | Block explorer frontend |
| `lux/monitoring` | 2022-06-02 | Network monitoring |
| `lux/finance` | 2022-08-04 | DeFi protocols |
| `lux/teleport` | 2022-09-13 | Cross-chain teleport bridge |
| `lux/kms` | 2022-11-17 | Key management system (14,395 commits) |
### Hanzo Platform Build-Out
| Repository | First Commit | Description |
|------------|-------------|-------------|
| `hanzo/o11y` | 2021-01-03 | Observability stack |
| `hanzo/sql-vector` | 2021-04-20 | Vector search in PostgreSQL |
| `hanzo/insights-rs` | 2021-04-27 | Rust analytics SDK |
| `hanzo/treasury` | 2021-06-11 | Treasury management |
| `hanzo/team` | 2021-08-02 | Team management |
| `hanzo/docdb` | 2021-10-31 | Document database (FerretDB-based) |
| `hanzo/cloud` | 2022-03-31 | Cloud platform |
| `hanzo/faucet` | 2022-05-12 | Token faucet |
| `hanzo/mds` | 2022-05-17 | Metadata service |
| `hanzo/otel-collector` | 2022-06-11 | OpenTelemetry collector |
| `hanzo/vector-go` | 2022-06-24 | Go vector client |
| `hanzo/base` | 2022-07-07 | Application backend framework (2,287 commits) |
| `hanzo/evm` | 2022-09-19 | EVM utilities |
| `hanzo/chat` | 2022-10-20 | Real-time chat |
| `hanzo/sign` | 2022-11-14 | Document e-signing |
| `hanzo/payments` | 2022-11-16 | Payment processing |
| `hanzo/kms` | 2022-11-17 | Secret management (19,820 commits) |
### Zoo Ecosystem Begins
| Repository | First Commit | Description |
|------------|-------------|-------------|
| `zoo/solidity` | 2021-01-09 | Smart contract library |
| `zoo/hardhat` | 2021-02-15 | Development framework |
| `zoo/node` | 2021-06-15 | Zoo blockchain node |
| `zoo/zoo-test` / `zoo/zoo-v4` / `zoo/zoo2` / `zoo/zoo3` | 2021-07-10 | Iterative protocol versions |
| `zoo/zoogov-app` | 2022-03-03 | Governance application |
| `zoo/zdk` | 2022-03-22 | Zoo Development Kit |
| `zoo/explorer-app` | 2022-05-31 | Explorer frontend |
| `zoo/CGI_Animation` | 2022-12-15 | AI-generated media |
### Commit Activity
| Year | Hanzo | Lux | Zoo |
|------|-------|-----|-----|
| 2021 | 58,199 | 27,811 | 8,923 |
| 2022 | 59,535 | 20,333 | 10,029 |
---
## 2023: Post-Quantum + MPC + AI Agents
Major cryptographic research: threshold signing, MPC engines, lattice crypto. AI work accelerates with Jin architecture, ML frameworks, and computer-use agents.
### Lux Cryptography and Protocol
| Repository | First Commit | Description |
|------------|-------------|-------------|
| `lux/sdk` | 2023-02-19 | Unified SDK (225 commits) |
| `lux/markets` | 2023-06-24 | DeFi market infrastructure |
| `lux/web` | 2023-10-13 | Lux Network website |
| `lux/wallet` | 2023-10-16 | Production wallet (1,203 commits) |
| `lux/mpc` | 2023-11-03 | MPC engine -- CGGMP21 + FROST (388 commits) |
| `lux/audits` | 2023-12-28 | Security audit reports (23 reports) |
| `lux/bridge` | 2023-12-30 | Cross-chain bridge (1,919 commits) |
### Hanzo AI Systems
| Repository | First Commit | Description |
|------------|-------------|-------------|
| `hanzo/ui` | 2023-01-24 | Shared UI component library (1,114 commits) |
| `hanzo/flow` | 2023-02-08 | AI workflow orchestration (17,390 commits) |
| `hanzo/cli` | 2023-04-03 | Developer CLI (10,596 commits) |
| `hanzo/jin` | 2023-05-15 | Multimodal AI -- saccade JEPA architecture |
| `hanzo/console` | 2023-05-18 | Admin console |
| `hanzo/dataroom` | 2023-05-27 | Secure document sharing |
| `hanzo/ml` | 2023-06-19 | Rust ML framework -- Candle (2,619 commits) |
| `hanzo/node` | 2023-06-25 | Distributed compute node (11,711 commits) |
| `hanzo/docs` | 2023-07-03 | Documentation platform |
| `hanzo/visor` / `hanzo/vm` | 2023-07-30 | Virtual machine runtime |
| `hanzo/desktop` | 2023-08-30 | Desktop application |
| `hanzo/cua` | 2023-11-03 | Computer-Use Agent (649 commits) |
### Zoo DeSci / DeAI
| Repository | First Commit | Description |
|------------|-------------|-------------|
| `zoo/ui` | 2023-01-24 | Shared UI library |
| `zoo/zones` | 2023-02-18 | Zone management |
| `zoo/gym-v1` | 2023-04-13 | AI training gym v1 |
| `zoo/foundation` | 2023-05-08 | Zoo Labs Foundation website |
| `zoo/zooai` | 2023-05-19 | Zoo AI platform |
| `zoo/gym` | 2023-05-28 | AI training gym |
| `zoo/agent` | 2023-06-25 | AI agent framework |
| `zoo/app` | 2023-08-30 | Zoo application |
### Commit Activity
| Year | Hanzo | Lux | Zoo |
|------|-------|-----|-----|
| 2023 | 99,672 | 24,417 | 13,855 |
---
## 2024: BFT Consensus + Hardware Wallets + Compute
Byzantine fault tolerance research, hardware signing, Corona post-quantum signatures, and AI model refinement.
### Lux Advanced Protocol
| Repository | First Commit | Description |
|------------|-------------|-------------|
| `lux/chat` | 2024-04-06 | Network communication |
| `lux/kms-go` | 2024-06-05 | KMS Go SDK |
| `lux/liquid` | 2024-06-18 | Liquid staking |
| `lux/corona` | 2024-07-08 | Post-quantum signature scheme (30 commits) |
| `lux/xwallet` | 2024-07-09 | Extended wallet |
| `lux/bank` | 2024-07-09 | Banking integration |
| `lux/tokens` | 2024-07-15 | Token management |
| `lux/uni-v4-subgraph` | 2024-07-23 | Uniswap V4 subgraph |
| `lux/dwallet` | 2024-07-31 | Decentralized wallet |
| `lux/kit` | 2024-08-07 | Development toolkit |
| `lux/bft` | 2024-08-28 | BFT consensus research (140 commits) |
### Hanzo AI Platform
| Repository | First Commit | Description |
|------------|-------------|-------------|
| `hanzo/captable` | 2024-01-08 | Cap table management |
| `hanzo/runtime` | 2024-02-06 | ML inference runtime |
| `hanzo/sentry` | 2024-02-15 | Error monitoring |
| `hanzo/engine` | 2024-02-26 | Standalone AI inference engine (3,258 commits) |
| `hanzo/enso` | 2024-03-28 | Code generation |
| `hanzo/paas` / `hanzo/platform` | 2024-04-19 | Platform-as-a-Service |
| `hanzo/web` | 2024-04-29 | Web framework |
| `hanzo/remove-refusals` | 2024-05-16 | Model uncensoring -- permanent weight modification |
| `hanzo/kms-go-sdk` | 2024-06-05 | KMS Go SDK |
| `hanzo/studio-desktop` | 2024-08-12 | AI Studio desktop app |
| `hanzo/capnp-es` | 2024-08-16 | Cap'n Proto TypeScript bindings |
| `hanzo/kms-python-sdk` | 2024-08-19 | KMS Python SDK |
| `hanzo/kms-node-sdk` | 2024-08-29 | KMS Node.js SDK |
### Zoo Growth
| Repository | First Commit | Description |
|------------|-------------|-------------|
| `zoo/game` | 2024-08-06 | AI gaming platform |
| `zoo/tools` | 2024-12-17 | Developer tooling |
### Commit Activity
| Year | Hanzo | Lux | Zoo |
|------|-------|-----|-----|
| 2024 | 141,053 | 20,987 | 21,139 |
---
## 2025: FHE + Formal Verification + Production Hardening
Fully homomorphic encryption, NTT SIMD acceleration, formal proofs in Lean4/TLA+/Tamarin, 23 security audits, and the full agent SDK stack.
### Lux Cryptography and Consensus
| Repository | First Commit | Description |
|------------|-------------|-------------|
| `lux/safe-frost` | 2025-04-11 | On-chain FROST signature verification (37 commits) |
| `lux/consensus` | 2025-07-28 | Quasar consensus -- multi-metric BFT (504 commits) |
| `lux/crypto` | 2025-07-25 | Unified crypto library -- BLS, ML-DSA, ML-KEM, secp256k1 |
| `lux/database` | 2025-07-25 | Database abstraction layer |
| `lux/ids` | 2025-07-25 | Identity and addressing |
| `lux/warp` | 2025-07-24 | Warp cross-chain messaging |
| `lux/go-bip32` / `lux/go-bip39` | 2025-07-25 | HD wallet key derivation |
| `lux/p2p` | 2025-12-04 | Peer-to-peer networking |
| `lux/cache` | 2025-12-04 | Caching layer |
| `lux/vm` | 2025-12-19 | Virtual machine framework |
| `lux/fhe` | 2025-12-28 | Fully homomorphic encryption engine (94 commits) |
| `lux/proofs` / `lux/formal` | 2025-12-25 | Formal verification: 6,851 Lean4 files, 4 TLA+ specs, 2 Tamarin proofs |
| `lux/papers` | 2025-10-28 | 136 research papers (LaTeX) |
| `lux/lips` / `lux/lps` | 2025-07-22 | Lux Improvement Proposals (848 proposals) |
### Lux Infrastructure Modules (extracted from monolith)
| Repository | First Commit | Description |
|------------|-------------|-------------|
| `lux/timer` | 2025-12-04 | Timing utilities |
| `lux/constants` | 2025-12-04 | Network constants |
| `lux/codec` | 2025-12-04 | Serialization codec |
| `lux/upgrade` | 2025-12-04 | Network upgrade coordination |
| `lux/metric` | 2025-07-26 | Metrics collection |
| `lux/math` / `lux/mock` | 2025-08-18 | Math utilities, test mocking |
| `lux/sampler` | 2025-12-24 | Validator sampling |
| `lux/staking` | 2025-12-24 | Staking mechanics |
| `lux/keychain` | 2025-12-24 | Key management |
| `lux/config` / `lux/keys` | 2025-12-21 | Configuration, key formats |
| `lux/lamport` | 2025-12-25 | Lamport one-time signatures |
### Hanzo Agent and AI Stack
| Repository | First Commit | Description |
|------------|-------------|-------------|
| `hanzo/hanzo.ai` / `hanzo/hanzo.industries` | 2025-02-12 | Corporate websites |
| `hanzo/rules` | 2025-02-17 | AI behavior rules |
| `hanzo/operate` | 2025-03-06 | Computer operation framework |
| `hanzo/agent` | 2025-03-11 | Agent SDK |
| `hanzo/agency` | 2025-03-12 | Multi-agent orchestration |
| `hanzo/python-sdk` | 2025-03-15 | Python SDK |
| `hanzo/operative` | 2025-03-18 | Operative agent runtime |
| `hanzo/js-sdk` / `hanzo/go-sdk` | 2025-03-26 | JavaScript and Go SDKs |
| `hanzo/extension` | 2025-04-04 | Browser extension |
| `hanzo/stream` | 2024-12-16 | Real-time streaming |
| `hanzo/tools` | 2024-12-17 | Agent tool library |
| `hanzo/hanzo.sh` | 2024-11-11 | CLI installer |
| `hanzo/mcp` | 2025-07-24 | Model Context Protocol server |
| `hanzo/agents` | 2025-07-24 | Agent definitions and configs |
| `hanzo/engine` | (continued) | AI inference -- 3,258 commits |
| `hanzo/node` | (continued) | Distributed compute -- 11,711 commits |
| `hanzo/skills` | 2025-10-18 | Agent skill library |
| `hanzo/computer` | 2025-10-29 | Computer-use tools |
| `hanzo/gateway` | 2025-10-28 | API gateway |
| `hanzo/rust-sdk` | 2025-10-28 | Rust SDK |
| `hanzo/zen-agentic-dataset` | 2025-12-30 | Agentic training data |
| `hanzo/patents` | 2025-12-28 | Patent portfolio |
| `hanzo/proofs` | (2026-03-31 active) | Formal proofs: 6,309 Lean4 files |
| `hanzo/papers` | (2026-03-31 active) | 152 research papers |
| `hanzo/hips` | 2025-09-07 | Hanzo Improvement Proposals (784 proposals) |
### Zoo Labs Foundation
| Repository | First Commit | Description |
|------------|-------------|-------------|
| `zoo/wander` | 2025-03-30 | Exploration agent |
| `zoo/nano-1` | 2025-05-23 | Nano model experiments |
| `zoo/ZIPs` | 2025-09-07 | Zoo Improvement Proposals (103 proposals) |
| `zoo/zoo-ai` | 2025-10-05 | Zoo AI platform |
| `zoo/zoo-papers-site` | 2025-10-29 | Papers website |
| `zoo/zoo.ngo` / `zoo.exchange` / `zoo.lab` / `zoo.vote` | 2025-11-02 | Foundation web properties |
| `zoo/universe` | 2025-11-02 | CI/CD and infrastructure |
| `zoo/docs` | 2025-12-14 | Documentation |
| `zoo/patents` | 2025-12-28 | Patent portfolio |
| `zoo/papers` | (2026-03-31 active) | 41 research papers |
### Security Audits (lux/audits)
| Date | Scope |
|------|-------|
| 2025-12-11 | DexVM, Oracle, Perpetuals |
| 2025-12-30 | Architecture, Consensus, Contracts, Crypto, Database, Network, Oracle, PlatformVM, ProposerVM+EVM, ThresholdVM, Warp, ZKVM, DexVM, Other VMs |
| 2026-01-30 | Standard audit (dedicated directory) |
| 2026-03-25 | Comprehensive security audit |
### Commit Activity
| Year | Hanzo | Lux | Zoo |
|------|-------|-----|-----|
| 2025 | 157,036 | 19,312 | 12,520 |
---
## 2026: Launch
Production launch. Mainnet, exchanges, compliance engine, GPU-accelerated EVM, FHE coprocessor.
### Lux Production Launch
| Repository | First Commit | Description |
|------------|-------------|-------------|
| `lux/fhe-coprocessor` | 2026-01-07 | Encrypted smart contract coprocessor |
| `lux/fpga` | 2026-01-07 | FPGA acceleration |
| `lux/tui` | 2026-01-07 | Terminal UI for node management |
| `lux/accel` | 2026-01-09 | Hardware acceleration layer |
| `lux/mlx` | 2026-01-09 | Apple MLX integration |
| `lux/benchmarks` | 2026-02-04 | Performance benchmarks |
| `lux/operator` | 2026-02-19 | Kubernetes operator |
| `lux/treasury` | 2026-03-02 | Treasury management |
| `lux/exchange-api` / `lux/exchange-proxy` | 2026-03-06 | Exchange infrastructure |
| `lux/exchange` | 2026-04-02 | DEX frontend |
| `lux/amm` | 2026-03-25 | Automated market maker |
| `lux/evmgpu` | 2026-03-29 | GPU-accelerated EVM (CUDA opcode dispatch) |
| `lux/futures` / `lux/forex` | 2026-03-30 | Derivatives and forex |
| `lux/bank-v2` | 2026-03-31 | Banking v2 |
| `lux/genesis` | 2026-04-04 | Genesis configuration and validator management |
| `lux/cevm` | 2026-04-05 | C-Chain EVM |
| `lux/gpu` | 2026-04-06 | GPU compute framework |
| `lux/sdk-rs` | 2026-04-04 | Rust SDK |
| `lux/evm-bench` | 2026-04-04 | EVM benchmarking suite |
### Hanzo Production Infrastructure
| Repository | First Commit | Description |
|------------|-------------|-------------|
| `hanzo/embeddings` | 2026-01-14 | Vector embeddings service |
| `hanzo/store-api` | 2026-01-14 | Store API |
| `hanzo/mpc` | 2026-01-24 | MPC threshold signing (36 commits) |
| `hanzo/mq` | 2026-01-19 | Message queue |
| `hanzo/contracts` | 2026-01-31 | Smart contracts |
| `hanzo/charts` | 2026-01-31 | Helm charts |
| `hanzo/hsm` | 2026-02-14 | Hardware security module integration |
| `hanzo/zen-gateway` | 2026-02-14 | AI model gateway |
| `hanzo/database` | 2026-02-14 | Database service |
| `hanzo/kms-operator` | 2026-02-15 | KMS Kubernetes operator |
| `hanzo/iam-sdk` | 2026-02-17 | IAM SDK |
| `hanzo/vault` | 2026-02-23 | Secret vault |
| `hanzo/billing` | 2026-02-23 | Billing system |
| `hanzo/orm` | 2026-02-23 | Object-relational mapping |
| `hanzo/operator` / `hanzo/hanzo-operator` | 2026-02-24 | Kubernetes operators |
| `hanzo/models` | 2026-02-26 | Model registry |
| `hanzo/ANE` | 2026-02-28 | Apple Neural Engine integration |
| `hanzo/ast` | 2026-03-02 | Abstract syntax tree tools |
| `hanzo/ledger` | 2026-03-05 | Financial ledger |
| `hanzo/tunnel` | 2026-03-11 | Secure tunneling |
| `hanzo/audit` | 2026-03-25 | Audit trail |
| `hanzo/onnxgo` | 2026-03-31 | ONNX Go runtime |
| `hanzo/proofs` | 2026-03-31 | Formal proofs |
| `hanzo/papers` | 2026-03-31 | Research papers |
### Zoo Launch
| Repository | First Commit | Description |
|------------|-------------|-------------|
| `zoo/contracts` | 2026-01-31 | Smart contracts |
| `zoo/genesis` | 2026-02-13 | Genesis configuration |
| `zoo/evm` | 2026-03-30 | Zoo EVM |
| `zoo/cli` | 2026-03-30 | Zoo CLI |
| `zoo/operator` | 2026-03-30 | Kubernetes operator |
| `zoo/kms` | 2026-03-30 | Key management |
| `zoo/mpc` | 2026-03-30 | MPC engine |
| `zoo/bridge` | 2026-03-30 | Cross-chain bridge |
| `zoo/computer` | 2026-03-31 | Compute platform |
| `zoo/proofs` | 2026-03-31 | Formal proofs |
| `zoo/papers` | 2026-03-31 | Research papers |
| `zoo/formal` | 2026-04-03 | Formal verification |
| `zoo/exchange` | 2026-04-02 | DEX |
### Commit Activity (YTD through 2026-04-07)
| Year | Hanzo | Lux | Zoo |
|------|-------|-----|-----|
| 2026 | 68,379 | 4,707 | 550 |
---
## Cumulative Commit History
```
Year Hanzo Lux Zoo Total
---- ----- --- --- -----
2014 14,547 5,405 -- 19,952
2015 23,098 9,028 -- 32,126
2016 17,989 2,681 -- 20,670
2017 20,219 3,854 -- 24,073
2018 24,508 7,427 3,009 34,944
2019 30,747 10,229 4,467 45,443
2020 46,845 18,333 1,151 66,329
2021 58,199 27,811 8,923 94,933
2022 59,535 20,333 10,029 89,897
2023 99,672 24,417 13,855 137,944
2024 141,053 20,987 21,139 183,179
2025 157,036 19,312 12,520 188,868
2026 68,379 4,707 550 73,636
TOTAL 761,827 174,524 75,643 1,011,994
```
Note: Annual totals sum to ~1,012,000. The `git rev-list --count HEAD` grand total of 1,103,364 is higher because it counts all reachable commits including merge bases and upstream fork history counted once per repo.
---
## Fork Provenance
The following repositories contain upstream history from established open-source projects. Lux/Hanzo contributions are layered on top.
| Repository | Upstream Project | Upstream Origin Date | Fork Purpose |
|------------|-----------------|---------------------|-------------|
| `hanzo/sql` | PostgreSQL | 1996 | Managed PostgreSQL service |
| `hanzo/datastore` | ClickHouse | 2008 | Analytics datastore |
| `hanzo/kv` | Valkey | 2009 | Key-value cache |
| `hanzo/redis` | Redis | 2009 | Redis compatibility |
| `hanzo/kv-go` | go-redis | 2012 | Go client library |
| `hanzo/pubsub-go` | nats.go | 2012 | Go pub/sub client |
| `hanzo/pubsub` | NATS Server | 2012 | Message broker |
| `hanzo/storage` | MinIO | 2014 | S3-compatible storage |
| `hanzo/dns` | CoreDNS | 2016 | DNS service |
| `hanzo/golang-migrate` | golang-migrate | 2014 | Database migrations |
| `hanzo/dbx` | PocketBase dbx | 2015 | Database abstraction |
| `hanzo/tasks` | Temporal | 2016 | Distributed task engine |
| `lux/coreth` / `lux/geth` | go-ethereum | 2013 | EVM implementation |
| `lux/zapdb` | Badger (Dgraph) | 2017 | Embedded KV store |
| `lux/safe` | Gnosis Safe | 2017 | Multisig contracts |
| `lux/explorer` | custom | 2018 | Block explorer |
| `lux/lattice` | Lattigo (EPFL) | 2019 | Lattice cryptography |
All other repositories are original work.
---
## Research Papers by Domain
### Lux Network (136 papers)
**Consensus**: lux-consensus, lux-quasar-consensus, lux-fpc-consensus, lux-wave-protocol
**Cryptography**: lux-crypto-agility, lux-corona-pq, lux-pq-crypto-suite, lux-pq-migration, lux-ntt-transform
**FHE**: lux-fhe-smart-contracts, lux-fhe-mpc-hybrid, fhe/fhevm, fhe/fhecrdt, fhe/ml-privacy, fhe/voting
**MPC**: lux-lss-mpc, lux-mchain-mpc
**DeFi**: lux-lightspeed-dex, lux-economics, lux-tokenomics, lux-credit-lending, lux-omnichain-yield
**Infrastructure**: lux-bridge, lux-teleport-protocol, lux-teleport-architecture, lux-photon-protocol, lux-nova-protocol
**Scaling**: gpu-evm-whitepaper, evmgpu-benchmark, lux-data-availability
**Identity**: lux-achain-attestation, lux-secure-messaging, lux-zap-wire-protocol
**Governance**: lux-dao-governance-framework, lux-adoption-roadmap
**Markets**: lux-market-nft, lux-credit-protocol-spec
### Hanzo AI (152 papers)
**AI/ML**: hanzo-jin-architecture, hanzo-engine-ml, hanzo-candle, hanzo-analytics-ml, hanzo-hmm, hanzo-agent-grpo, hanzo-agent-sdk
**Infrastructure**: hanzo-aci, hanzo-base, hanzo-api-gateway, hanzo-ingress-proxy, hanzo-pubsub-events, hanzo-search
**Commerce**: crowdstart-commerce, hanzo-commerce-payments, hanzo-checkout, hanzo-ai-commerce
**Security**: hanzo-pq-crypto, hanzo-formal-verification, hanzo-harness-hacking
**Platform**: hanzo-iam-platform, hanzo-sdk-ecosystem, hanzo-mcp-server, hanzo-network-whitepaper, hanzo-tokenomics
**Communication**: hanzo-chat, hanzo-flow
**Algorithms**: algorithms/ subdirectory, defense/ subdirectory
**Models**: zen/ subdirectory (Zen model family)
**Computer Use**: hanzo-operate-computer, hanzo-operative
### Zoo Labs (41 papers)
**DeSci**: zoo-conservation-ai, zoo-habitat-modeling, zoo-satellite-ecology, zoo-wildlife-tracking, zoo-citizen-science, zoo-carbon-credits, zoo-educational-ai
**DeAI**: zoo-fhe-ai, zoo-mobile-inference, zoo-agent-nft, embedding-7680, hllm-training-free-grpo, experience-ledger-dso, beluga-l3-whitepaper
**Blockchain**: zoo-consensus, zoo-poai-consensus, zoo-quasar-benchmarks, zoo-bridge, zoo-dex, zoo-evm-l2-architecture, zoo-evm-benchmarks, zoo-gpu-evm
**Governance**: zoo-dao-governance, zoo-tokenomics, zip-002-zen-reranker
**Security**: zoo-pq-crypto, zoo-mpc-custody, zoo-key-management, zoo-fhe
**Identity**: zoo-identity-chain, zoo-experience-ledger
**Launch**: zoo-mainnet-launch-checklist
---
## Formal Verification
### Lean4 Proofs (13,160 files total)
- **Lux** (`lux/formal/lean/`, `lux/proofs/`): BFT consensus safety, bridge security, DeFi invariants, cross-chain compute, post-quantum hybrid crypto, Verkle tree, warp security, GPU scaling laws, FHE, sharia compliance
- **Hanzo** (`hanzo/proofs/lean/`): Complementary formal proofs
### TLA+ Specifications (4 specs)
- Teleport cross-chain protocol
- MPC bridge protocol state machine
### Tamarin Protocol Proofs (2 proofs)
- MPC bridge cryptographic protocol security
### Halmos Symbolic Tests (10 contracts)
- Bridge and yield vault Solidity verification
---
## Active Development (as of 2026-04-07)
Most recently committed repositories across all three organizations:
| Repository | Last Commit |
|------------|-------------|
| `lux/node` | 2026-04-07 |
| `lux/papers` | 2026-04-07 |
| `lux/netrunner` | 2026-04-07 |
| `lux/threshold` | 2026-04-07 |
| `lux/dex` | 2026-04-07 |
| `lux/formal` | 2026-04-07 |
| `lux/mpc` | 2026-04-07 |
| `hanzo/blog` | 2026-04-07 |
| `hanzo/iam` | 2026-04-07 |
| `hanzo/kms` | 2026-04-07 |
| `hanzo/papers` | 2026-04-07 |
| `hanzo/cloud` | 2026-04-07 |
| `zoo/blog` | 2026-04-07 |
| `zoo/papers` | 2026-04-07 |
| `zoo/universe` | 2026-04-07 |
+132 -28
View File
@@ -1,4 +1,4 @@
# LLM.md - AI Development Guide
# AI Development Guide
This file provides guidance for AI assistants working with the Lux node codebase.
@@ -8,7 +8,7 @@ Lux blockchain node implementation - a high-performance, multi-chain blockchain
**Key Context:**
- Original Lux Network node — NOT a fork
- Latest Tag: v1.26.12
- Latest Tag: v1.26.31
- Network ID: 96369 (Lux Mainnet), 96368 (Testnet), 96370 (Devnet)
- Go Version: 1.26.1+
- Database: ZapDB (primary, default)
@@ -39,6 +39,8 @@ selection, and EVM contract auth.
| SHA | Tag | Impact |
|-----|-----|--------|
| (pending) | next | LP-023 Phase 1 batch 2: 5 more native-ZAP tx types — BaseTx v1, RegisterL1ValidatorTx v1, SlashValidatorTx v1, TransferChainOwnershipTx v1, RemoveChainValidatorTx. Cross-type Parse mean speedup 8.5× over linearcodec. Variable-length nested-object schemas (Outs/Ins/full OutputOwners/Warp Message/Evidence) deferred to batch 3. |
| `e77a7ef78e` | (pre) | LP-023 Phase 1 batch 1: 4 simple tx types + bench harness. 37× Parse, 5.6× cross-type mean. |
| `9df72a6f55` | v1.26.10 | Wire ChainSecurityProfile into bootstrap (closes F102) |
| `c4af52411e` | v1.26.10 | X-Chain (avm) mempool refuses classical creds under strict-PQ |
| `a14a1601f4` | v1.26.10 | P-Chain (platformvm) mempool refuses classical creds under strict-PQ |
@@ -81,9 +83,71 @@ selection, and EVM contract auth.
(F102 wiring closes the consensus side; geth-side hookup is the
remaining tail).
## FeePolicy — canonical user-tx fee gate
Every Lux VM that accepts user-submitted txs declares a `fee.Policy`
(package `vms/types/fee`). There is one interface and one validator —
no per-VM bespoke fee structs.
### Policy choice per VM
| VM | Chain | Posture | Policy |
|----|-------|---------|--------|
| dexvm | D-Chain | user-tx | `FlatPolicy{Fee: MinTxFeeFloor, AssetID: UTXOAssetIDFor(networkID)}` |
| zkvm | Z-Chain | user-tx | `FlatPolicy{Fee: MinTxFeeFloor, ...}` |
| aivm | A-Chain | user-tx | `FlatPolicy{Fee: MinTxFeeFloor, ...}` |
| keyvm | K-Chain | user-tx | `FlatPolicy{Fee: MinTxFeeFloor, ...}` |
| bridgevm | B-Chain | user-tx | `FlatPolicy{Fee: MinTxFeeFloor, ...}` |
| quantumvm | Q-Chain | user-tx | `FlatPolicy{Fee: MinTxFeeFloor, ...}` |
| identityvm | I-Chain | user-tx | `FlatPolicy{Fee: MinTxFeeFloor, ...}` |
| thresholdvm | M-Chain | service-only | `NoUserTxPolicy{}` |
| oraclevm | O-Chain | service-only | `NoUserTxPolicy{}` |
| relayvm | R-Chain | service-only | `NoUserTxPolicy{}` |
| graphvm | G-Chain | read-only | `NoUserTxPolicy{}` (GraphQL refuses `mutation`) |
| evm | C-Chain | user-tx | native EVM gas (gas * gasPrice >= 0 enforced upstream) |
| platformvm | P-Chain | user-tx | native `TxFee` field on Config |
| avm | X-Chain | user-tx | native `TxFee` field on Config |
`MinTxFeeFloor = 1 mLUX = 1_000_000 nLUX` (the same minimum the P-Chain
base fee enforces). User-facing chains MAY charge more; they MUST NOT
charge less.
### Wiring contract
1. VM struct holds `feePolicy fee.Policy` (and `networkID uint32`).
2. `Initialize` sets `feePolicy = fee.FlatPolicy{...}` (or
`fee.NoUserTxPolicy{}` for service-only) from `init.Runtime.NetworkID`
and calls `fee.Validate(vm.feePolicy)` — refuses zero-fee user-facing
chains at boot, before any block is accepted.
3. The canonical user-tx admission entry (e.g. `SubmitTx`, `IssueTx`,
`InitiateBridgeTransfer`, mutating service RPCs) calls
`policy.ValidateFee(paid, asset)` BEFORE mempool insert.
4. Consensus-internal paths (engine→VM block delivery, replay, internal
tx emission) bypass the fee gate — the policy gates only the
*user-submitted* entrypoint.
### Where the gates live
- `vms/types/fee/policy.go` — interface + FlatPolicy + NoUserTxPolicy + Validate
- `~/work/lux/chains/<vm>/feegate.go` — per-VM helper + gate method
- `~/work/lux/chains/<vm>/feegate_test.go` — RejectsZeroFee + AcceptsMinFee
- Oracle (O-Chain): `~/work/lux/oracle/vm/feegate.go` (re-exported by `~/work/lux/chains/oraclevm/`)
- 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)
## Essential Commands
### Building
### Release & build (canonical) — via platform.hanzo.ai, NOT GitHub Actions
The ONE way to build + publish releases is **[`RELEASE.md`](./RELEASE.md)**:
platform.hanzo.ai reads [`hanzo.yml`](./hanzo.yml) on a `v*` tag push and
schedules the image build onto self-hosted **arcd** pools (`lux-build-linux-*`)
over the native long-poll fabric — no GitHub-Actions hop. ONE `Dockerfile`
build yields BOTH artifacts: the node image (`ghcr.io/luxfi/node:vX.Y.Z`, luxd
+ 12 baked VM plugins) and, via [`scripts/publish_plugin_set.sh`](./scripts/publish_plugin_set.sh),
the plugin set to `s3://lux-plugins-<env>/<pluginset>/` (operator `pluginSource`).
The `.github/workflows/*` build/release workflows are retired (RELEASE.md §Retire).
### Building (local dev only)
```bash
# Build node binary
./scripts/run_task.sh build
@@ -274,31 +338,33 @@ Located in `vms/thresholdvm/fhe/`:
| Gateway | `0x0200000000000000000000000000000000000083` |
### ZAP Transport (Zero-Copy App Proto)
ZAP is the default high-performance binary wire protocol for VM<->Node communication.
gRPC support is available via build tag for testing/compatibility.
ZAP is the only wire protocol for VM<->Node communication. The gRPC
fallback (and its `-tags=grpc` opt-in) was retired in v1.26.31 along
with every `//go:build grpc` file under `node/`. There is one and only
one way to talk to a Chain VM: ZAP.
**Build Tags:**
**Build:**
```bash
go build # ZAP only (default, production)
go build -tags=grpc # gRPC support (for testing/compatibility)
go build # ZAP only — there are no build tags
```
**Key Packages:**
- `github.com/luxfi/api/zap` - Core wire protocol and message types (Layer A)
- `github.com/luxfi/proto/rpcdb` - rpcdb service spec / data carriers (Layer B)
- `github.com/luxfi/node/db/rpcdb` - rpcdb Service + ZAP/gRPC transport adapters (Layer C)
- `github.com/luxfi/vm/rpc/sender` - p2p.Sender over ZAP/gRPC
- `vms/rpcchainvm/sender/` - Node-side sender implementation
- `vms/platformvm/warp/zwarp/` - ZAP-based warp signing client/server
- `github.com/luxfi/api/zap` Core wire protocol and message types (Layer A)
- `github.com/luxfi/protocol/rpcdb` rpcdb service spec / data carriers (Layer B)
- `github.com/luxfi/node/db/rpcdb` rpcdb Service + ZAP transport adapter (Layer C)
- `vms/rpcchainvm/sender/` — Node-side `p2p.Sender` over ZAP
- `vms/rpcchainvm/zap/` — ChainVM client/server over ZAP
- `vms/platformvm/warp/zwarp/` — Warp signing over ZAP
**rpcdb Layered Topology (post-2026-05 reorg):**
- Layer A — wire framing: `github.com/luxfi/api/zap` (independent module)
- Layer B — rpcdb service spec: `github.com/luxfi/proto/rpcdb` (transport-agnostic data carriers)
- Layer C — rpcdb impl: `node/db/rpcdb/{service.go, grpc_server.go, zap_server.go}`
**rpcdb Layered Topology:**
- Layer A — wire framing: `github.com/luxfi/api/zap`
- Layer B — rpcdb service spec: `github.com/luxfi/protocol/rpcdb`
- Layer C — rpcdb impl: `node/db/rpcdb/{service.go, zap_server.go}`
- `service.go` — transport-neutral `Service` wrapping `database.Database`
- `zap_server.go` (default) — ZAP transport adapter (used by cevm)
- `grpc_server.go` (`-tags=grpc`) — gRPC transport adapter
- One Service, many transport adapters. Adding a transport = new file wrapping `*Service`.
- `zap_server.go` — ZAP transport adapter (only adapter)
- One Service, one transport. The dual-adapter pattern stays available
for future transports (each is a new file wrapping `*Service`), but
ZAP is the only one shipping.
**Wire Protocol Format:**
```
@@ -313,11 +379,8 @@ go build -tags=grpc # gRPC support (for testing/compatibility)
**Sender Usage:**
```go
// ZAP transport (default)
// ZAP transport — the only transport
s := sender.ZAP(zapConn)
// gRPC transport (requires -tags=grpc build)
s := sender.GRPC(senderpb.NewSenderClient(grpcConn))
```
**Warp over ZAP:**
@@ -459,7 +522,7 @@ go test -v -run "TestHybrid" ./node/network/dialer/... -count=1
### 1. P2P Sender Interface
Node's rpcchainvm implements `p2p.Sender` (from `github.com/luxfi/p2p`) for cross-chain messaging.
The `sender` package is a gRPC implementation of `p2p.Sender`.
The `sender` package is the ZAP-native implementation of `p2p.Sender`.
### 2. Chain Tracking
Nodes don't automatically track chains. Use:
@@ -513,7 +576,7 @@ For importing pre-merge blocks, Shanghai must be active based on `ShanghaiTime`,
### 8. `vms/components/lux` vs `luxfi/utxo` (parallel UTXO types)
The `github.com/luxfi/node/vms/components/lux` package contains a parallel
`lux.UTXO`/`lux.TransferableInput` type tree alongside `github.com/luxfi/utxo`.
External consumers (e.g. `~/work/liquidity/network-bootstrap/fund.go`) need
External consumers (e.g. a white-label tenant's network-bootstrap tooling) need
to import the `vms/components/lux` variant to interop with PlatformVM/AVM
tx builders — `luxfi/utxo` types alone are not accepted by the X→P export
path. This is a known anomaly pending #58 follow-up consolidation; do NOT
@@ -711,6 +774,47 @@ PRIVATE_KEY="<funded_key>" \
./bin/bench tps --chains=lux --duration=60s --concurrency=5
```
## JSON rule — json/v2 at HTTP boundary only; ZAP for all internal data
Encoding boundaries are one-way and explicit:
- **External (HTTP / JSON-RPC API)** — `github.com/go-json-experiment/json` (v2).
Never `encoding/json`. This covers: `service/*`, `server/*`, `pubsub/`,
`vms/platformvm/service.go`, `vms/xvm/service.go`, JSON-RPC clients
(`vms/platformvm/client_*`), CLI tools (`cmd/*`), wallet examples,
on-disk config files (read once at boot), genesis/upgrade blobs.
- **Internal (state, P2P, consensus, MPC, logs, metrics)** — ZAP wire only.
No JSON in: `network/`, `consensus/`, `snow/`, `chains/` (data-plane),
`vms/*/state/`, `vms/*/block/`, `vms/*/txs/` (struct codec), threshold
payloads, P2P message bodies, internal databases.
Migration helpers (v2 API delta vs v1):
| v1 (encoding/json) | v2 (go-json-experiment/json) |
|-----------------------------------|----------------------------------------------|
| `json.Marshal(v)` | `json.Marshal(v)` (variadic opts; signature compat) |
| `json.MarshalIndent(v, "", " ")` | `json.Marshal(v, jsontext.WithIndent(" "))` |
| `json.Unmarshal(b, &v)` | `json.Unmarshal(b, &v)` |
| `json.NewEncoder(w).Encode(v)` | `json.MarshalWrite(w, v)` (no trailing `\n`) |
| `json.NewDecoder(r).Decode(&v)` | `json.UnmarshalRead(r, &v)` |
| `json.RawMessage` | `jsontext.Value` |
| `*json.SyntaxError` | `*jsontext.SyntacticError` |
v2 semantic differences worth knowing (these change wire shape):
- `[N]byte` field with no `MarshalJSON` ⇒ v2 marshals as base64 string,
v1 marshalled as JSON array of byte numbers. Add `MarshalJSON` on the
type if the array form is wanted on the wire.
- `time.Duration` ⇒ v2 default is the standard string form ("30m");
v1 marshalled as int nanoseconds. v1 sub-package
(`github.com/go-json-experiment/json/v1`) exposes `FormatDurationAsNano(true)`;
v2 root does not. Prefer the string form on new APIs.
- v2 enforces strict UTF-8; raw arbitrary bytes in JSON strings fail.
This matters for legacy P2P/internal blobs that happen to be stored
through JSON — those should already be on ZAP.
- `json.MarshalWrite` does NOT append a trailing `\n` (v1 `NewEncoder.Encode` did).
Adjust HTTP-handler test fixtures accordingly.
---
*Last Updated*: 2026-02-04
*Last Updated*: 2026-06-06
+2 -2
View File
@@ -5,7 +5,7 @@
---
[![Build Status](https://github.com/luxfi/node/actions/workflows/ci.yml/badge.svg)](https://github.com/luxfi/node/actions)
[![Go Version](https://img.shields.io/badge/go-1.21.12-blue.svg)](https://golang.org/)
[![Go Version](https://img.shields.io/badge/go-1.26.3-blue.svg)](https://golang.org/)
[![License](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg)](LICENSE)
Node implementation for the [Lux](https://lux.network) network -
@@ -38,7 +38,7 @@ The minimum recommended hardware specification for nodes connected to Mainnet is
If you plan to build Lux Node from source, you will also need the following software:
- [Go](https://golang.org/doc/install) version >= 1.23.9
- [Go](https://golang.org/doc/install) version >= 1.26.3
- [gcc](https://gcc.gnu.org/)
- g++
+187
View File
@@ -0,0 +1,187 @@
# Lux release — build + publish via platform.hanzo.ai (the ONE canonical way)
This is the single, repeatable way to build and publish the Lux release
artifacts. It runs entirely on our own infrastructure — **the PaaS
(platform.hanzo.ai) + self-hosted arcd runners + DOKS/fleet**. There is **no
GitHub Actions build path** (the `.github/workflows/*` build/release workflows
are retired — see [§Retire](#retire-the-github-actions-build-workflows)).
## What a release produces
ONE `Dockerfile` multi-stage build (this repo) is the single source of truth.
It compiles `luxd` + all 12 VM plugins (CGO_ENABLED=0) and yields TWO
distribution surfaces:
| # | Artifact | Destination | Consumed by |
|---|----------|-------------|-------------|
| 1 | node image (luxd + 12 plugins baked at `/luxd/build/plugins/`) | `ghcr.io/luxfi/node:vX.Y.Z` | operator pod image; `startup.sh cp /luxd/build/plugins/*` |
| 2 | plugin set (the 12 VM-ID binaries + `SHA256SUMS`) | `s3://lux-plugins-<env>/<pluginset>/` | operator `plugin-fetch` init container (LuxNetwork CR `pluginSource`) |
Artifact 2 is **extracted from** artifact 1 — the plugins are never compiled
twice. One build, two surfaces (DRY, orthogonal).
The plugin versions are pinned as Dockerfile build-args, kept in lockstep with
this repo's `go.mod`:
- `EVM_VERSION` (luxfi/evm — C-Chain EVM, the `0x9999` settlement surface)
- `CHAINS_REF` (luxfi/chains — the 10 non-DEX VMs incl. bridgevm)
- `DEX_REF` (luxfi/dex `cmd/dchain` — the native D-Chain DEX VM)
## The machinery
```
git tag vX.Y.Z (push)
│ GitHub App webhook ─▶ https://platform.hanzo.ai/v1/github-webhook
platform BuildScheduler ── reads hanzo.yml @ tag, validates, enqueues
│ one build_job per matrix entry
native long-poll fabric (build-queue.ts) NO GitHub Actions hop
│ arcd runner POST /v1/arcd/poll (HMAC)
arcd runner on pool lux-build-linux-<arch>
│ git checkout @ tag → docker build -f Dockerfile . → docker push
ghcr.io/luxfi/node:vX.Y.Z (artifact 1)
│ POST /v1/arcd/complete (status, image_digest)
build_job (DB, system-of-record)
```
- **PaaS**: `platform.hanzo.ai` (`~/work/hanzo/platform`,
`pkg/platform/src/services/ci/`). Owns the schema, the scheduler, the durable
`build_job` record, and the native long-poll dispatch.
- **Build muscle**: self-hosted **arcd** runner pools, `lux-build-linux-amd64`
and `lux-build-linux-arm64` (one daemon per fleet host; `spark` = linux/arm64,
amd64 via buildx). NO GitHub-hosted runners; NO GitHub Actions orchestration
on the native path.
- **Contract**: `~/work/hanzo/platform/docs/PLATFORM_CI.md`.
## Build — the one command
A release is a semver tag push. The declarative entrypoint is this repo's
[`hanzo.yml`](./hanzo.yml); the trigger is one of:
**(a) Tag push (normal release).** Cutting the tag IS the release:
```bash
git tag v1.30.41 && git push origin v1.30.41
```
The webhook maps `refs/tags/v1.30.41``branch=v1.30.41`; `hanzo.yml`'s
`tag-pattern: "{{git.branch}}"` yields the image tag `v1.30.41`. Platform
schedules the amd64 + arm64 builds onto the live `lux-build-*` arcd pools and
pushes the multi-arch image to GHCR.
**(b) On-demand (re-release / backfill).** The platform `buildJob.trigger`
tRPC mutation schedules the same build for an explicit ref, no push required:
```
buildJob.trigger({
installationId: "<luxfi GitHub App installation id>",
repo: "luxfi/node",
sha: "<commit at the tag>",
ref: "refs/tags/v1.30.41",
branch: "v1.30.41" // → image tag via {{git.branch}}
})
```
Track it: `buildJob.list` / `buildJob.one` / `buildJob.logs` (org-scoped).
> A pool goes **native** the moment an arcd runner self-registers for it
> (`arcd_runner.lastSeen` within 90s); until then platform transparently falls
> back to `workflow_dispatch` so a build is never stranded. To run a release
> fully GitHub-free, ensure a `lux-build-linux-{amd64,arm64}` runner is live
> (`tRPC arcd` / the `arcd_runner` table). Set platform env
> `WORKFLOW_DISPATCH_FALLBACK=false` to forbid the legacy hop.
### What the runner runs (identical on a fleet host, for manual/DR builds)
The native path runs exactly the repo's `Dockerfile`. To reproduce on a fleet
host directly (e.g. `spark`), with no platform and no GitHub:
```bash
# on spark (linux/arm64; amd64 via buildx)
git clone --branch v1.30.41 git@github.com:luxfi/node.git && cd node
docker buildx build --platform linux/amd64 \
--build-arg CGO_ENABLED=0 \
-t ghcr.io/luxfi/node:v1.30.41 -f Dockerfile --push .
```
## Publish the plugin set — step 2
After the image exists, publish artifact 2 from it (one command, idempotent,
no second compile). Run on any fleet host or a DOKS Job that has `crane`/docker
+ `mc`; typically the same arcd runner that just built the image:
```bash
scripts/publish_plugin_set.sh \
ghcr.io/luxfi/node:v1.30.41 \
lux-plugins-<env>/<pluginset> \
lux # mc alias for the target MinIO/S3
# e.g. lux-plugins-testnet/v1.3.5
```
It extracts the 12 plugin binaries from the image, writes `SHA256SUMS`, uploads
all to `s3://lux-plugins-<env>/<pluginset>/`, and verifies remote==local sha.
S3 is the in-cluster MinIO (`s3.lux-system.svc.cluster.local:9000`, external
`s3.lux.network`). Configure the `mc` alias once with the `hanzo-s3-secret`
credentials:
```bash
mc alias set lux <endpoint> hanzo "$(kubectl -n lux-system get secret \
hanzo-s3-secret -o jsonpath='{.data.password}' | base64 -d)" --api s3v4
```
A pluginset prefix is **immutable** — bump `<pluginset>` for a new release,
never overwrite a prefix a live network points at.
## Deploy — step 3 (operator, not this repo)
luxd rollout is owned by the **lux operator** (`~/work/lux/operator`,
`LuxNetwork` CR). Update the CR's `image.tag` (artifact 1) and, when the
network fetches plugins from S3, the `pluginSource.bucket` + per-plugin
`sha256` (artifact 2, from the `SHA256SUMS` you just published). The operator's
`plugin-fetch` init container verifies each sha256 fail-closed. This is
deliberately decoupled from build: `hanzo.yml` has **no `deploy:` block**.
## Reproducibility
- The build is **functionally reproducible**: same source tags + same toolchain
(Go 1.26.4) + `CGO_ENABLED=0` ⇒ functionally identical plugins, provable by a
fleet rebuild (verified: `spark` rebuilt evm@v1.99.37 + dexvm@v1.5.15 from the
same tags). It is **not bit-identical by construction**: the Dockerfile plugin
stages omit `-trimpath` and use `-mod=mod` with a first-party `go.sum` strip
(re-resolves luxfi/* deps), so embedded paths + re-tagged module content can
shift the bytes (Go `BuildID` differs; binary ~16 KB larger). The published
image is the canonical artifact; verify against ITS baked sha (what
`publish_plugin_set.sh` records), not a separate fleet build.
- To make releases bit-reproducible (future hardening, patch-only): add
`-trimpath` to every plugin `go build` and pin `go.sum` (drop the strip +
`-mod=mod`). Tracked as a follow-up; not required for correctness.
## Retire the GitHub Actions build workflows
These `.github/workflows/*` build/release/CI workflows are superseded by this
flow and must be removed/disabled (platform owns build; the native long-poll
owns dispatch). Delete them once a `lux-build-*` arcd runner is live:
| Workflow | Replaced by |
|----------|-------------|
| `docker.yml` (built `ghcr.io/luxfi/node` on the `lux-build` ARC pool) | `hanzo.yml` (artifact 1) — native long-poll, NO GitHub Actions |
| `release.yml` | the tag-push trigger above + `scripts/publish_plugin_set.sh` |
| `build.yml`, `ci.yml` | platform CI test step (runner runs `go test` pre-build) |
| `build-linux-binaries.yml` | `Dockerfile` builder stage (luxd binary) |
| `build-ubuntu-amd64-release.yml`, `build-ubuntu-arm64-release.yml` | `Dockerfile` + buildx multi-arch |
| `build-macos-release.yml`, `build-win-release.yml`, `build-and-test-mac-windows.yml` | arcd `lux-build-{macos,windows}-*` pools (matrix in `hanzo.yml` when desired) |
| `build-deb-pkg.sh`, `build-tgz-pkg.sh` (under `.github/workflows/`) | packaging step on the arcd runner (post-build), not GitHub Actions |
| `codeql-analysis.yml`, `fuzz.yml`, `fuzz_merkledb.yml`, `test-database-replay.yml` | scheduled jobs on arcd / DOKS (not a build dependency) |
| `buf-lint.yml`, `buf-push.yml`, `labels.yml`, `stale.yml` | repo-hygiene; migrate to arcd cron or drop |
The same retirement applies to the equivalent build/release workflows in the
plugin-source repos (`luxfi/evm`, `luxfi/chains`, `luxfi/dex`): their artifacts
are built from source by THIS repo's `Dockerfile` at the pinned refs, so those
repos need no independent image/release CI — only their tags. Migrate each by
adding a `hanzo.yml` (if it ships its own image) or deleting its build CI (if it
is consumed only as a Go module / plugin source here).
+10 -10
View File
@@ -2519,7 +2519,7 @@ The plugin version is unchanged at `27` and compatible with versions `v1.10.5 -
### Configs
- Changed the default value of `--network-allow-private-ips` to `false` when the `--network-id` is either `fuji` or `mainnet`
- Changed the default value of `--network-allow-private-ips` to `false` when the `--network-id` is either `testnet` or `mainnet`
- Added P-chain cache size configurations
- `block-cache-size`
- `tx-cache-size`
@@ -2564,7 +2564,7 @@ The plugin version is unchanged at `27` and compatible with versions `v1.10.5 -
- [x/sync] Update target locking by @patrick-ogrady in https://github.com/luxfi/node/pull/1763
- Export warp errors for external use by @aaronbuchwald in https://github.com/luxfi/node/pull/1771
- Remove unused networking constant by @StephenButtolph in https://github.com/luxfi/node/pull/1774
- Change the default value of `--network-allow-private-ips` to `false` for `mainnet` and `fuji` by @StephenButtolph in https://github.com/luxfi/node/pull/1773
- Change the default value of `--network-allow-private-ips` to `false` for `mainnet` and `testnet` by @StephenButtolph in https://github.com/luxfi/node/pull/1773
- Remove context.TODO from tests by @StephenButtolph in https://github.com/luxfi/node/pull/1778
- Replace linkeddb iterator with native DB range queries by @StephenButtolph in https://github.com/luxfi/node/pull/1752
- Add support for measuring key size in caches by @StephenButtolph in https://github.com/luxfi/node/pull/1781
@@ -2934,7 +2934,7 @@ This version is backwards compatible to [v1.10.0](https://github.com/luxfi/node/
- Remove no-op changes from history results in https://github.com/luxfi/node/pull/1335
- Cleanup type assertions in the linkedHashmap by @StephenButtolph in https://github.com/luxfi/node/pull/1341
- Fix racy xvm tx access by @StephenButtolph in https://github.com/luxfi/node/pull/1349
- Update Fuji beacon ips by @StephenButtolph in https://github.com/luxfi/node/pull/1354
- Update Testnet beacon ips by @StephenButtolph in https://github.com/luxfi/node/pull/1354
- Remove duplicate TLS verification by @StephenButtolph in https://github.com/luxfi/node/pull/1364
- Adjust Merkledb Trie invalidation locking in https://github.com/luxfi/node/pull/1355
- Use require in Lux bootstrapping tests by @StephenButtolph in https://github.com/luxfi/node/pull/1344
@@ -2949,7 +2949,7 @@ This version is backwards compatible to [v1.10.0](https://github.com/luxfi/node/
- [Issue-1368]: Panic in serializedPath.HasPrefix in https://github.com/luxfi/node/pull/1371
- Add ValidatorsOnly flag to GetStake by @StephenButtolph in https://github.com/luxfi/node/pull/1377
- Use proto in `x/sync` by @danlaine in https://github.com/luxfi/node/pull/1336
- Update incorrect fuji beacon IPs by @StephenButtolph in https://github.com/luxfi/node/pull/1392
- Update incorrect testnet beacon IPs by @StephenButtolph in https://github.com/luxfi/node/pull/1392
- Update `api/` error handling by @StephenButtolph in https://github.com/luxfi/node/pull/1393
- refactor concurrent work limiting in sync in `x/sync` by @danlaine in https://github.com/luxfi/node/pull/1347
- Remove check for impossible condition in `x/sync` by @danlaine in https://github.com/luxfi/node/pull/1348
@@ -3017,7 +3017,7 @@ The supported plugin version is `25`.
- defer delegatee rewards until end of validator staking period by @dhrubabasu in https://github.com/luxfi/node/pull/1262
- Initialize UptimeCalculator in TestPeer by @joshua-kim in https://github.com/luxfi/node/pull/1283
- Add Lux liveness health checks by @StephenButtolph in https://github.com/luxfi/node/pull/1287
- Skip AMI generation with Fuji tags by @StephenButtolph in https://github.com/luxfi/node/pull/1288
- Skip AMI generation with Testnet tags by @StephenButtolph in https://github.com/luxfi/node/pull/1288
- Use `maps.Equal` in `set.Equals` by @danlaine in https://github.com/luxfi/node/pull/1290
- return accrued delegator rewards in `GetCurrentValidators` by @dhrubabasu in https://github.com/luxfi/node/pull/1291
- Add zstd compression by @danlaine in https://github.com/luxfi/node/pull/1278
@@ -3819,7 +3819,7 @@ The supported plugin version is `16`.
Please upgrade your node as soon as possible.
The changes in `v1.8.x` go into effect at 4 PM EDT on September 6th, 2022 on both Fuji and Mainnet. You should upgrade your node before the changes go into effect, otherwise they may experience loss of uptime.
The changes in `v1.8.x` go into effect at 4 PM EDT on September 6th, 2022 on both Testnet and Mainnet. You should upgrade your node before the changes go into effect, otherwise they may experience loss of uptime.
The supported plugin version is `16`.
@@ -3833,7 +3833,7 @@ The supported plugin version is `16`.
Please upgrade your node as soon as possible.
The changes in `v1.8.x` go into effect at 4 PM EDT on September 6th, 2022 on both Fuji and Mainnet. You should upgrade your node before the changes go into effect, otherwise they may experience loss of uptime.
The changes in `v1.8.x` go into effect at 4 PM EDT on September 6th, 2022 on both Testnet and Mainnet. You should upgrade your node before the changes go into effect, otherwise they may experience loss of uptime.
The supported plugin version is `16`.
@@ -3850,7 +3850,7 @@ The supported plugin version is `16`.
This is a mandatory security upgrade. Please upgrade your node **as soon as possible.**
The changes in the upgrade go into effect at **4 PM EDT on September 6th, 2022** on both Fuji and Mainnet. You should upgrade your node before the changes go into effect, otherwise they may experience loss of uptime.
The changes in the upgrade go into effect at **4 PM EDT on September 6th, 2022** on both Testnet and Mainnet. You should upgrade your node before the changes go into effect, otherwise they may experience loss of uptime.
You may see some extraneous ERROR logs ("BAD BLOCK") on your node after upgrading. These may continue until the Apricot Phase 6 activation (at 4 PM EDT on September 6th).
@@ -4515,7 +4515,7 @@ This version is backwards compatible to [v1.7.0](https://github.com/luxfi/node/r
- Added `--stake-supply-cap` which defaults to `720,000,000,000,000,000` nLUX.
- Renamed `--bootstrap-multiput-max-containers-sent` to `--bootstrap-ancestors-max-containers-sent`.
- Renamed `--bootstrap-multiput-max-containers-received` to `--bootstrap-ancestors-max-containers-received`.
- Enforced that `--staking-enabled=false` can not be specified on public networks (`Fuji` and `Mainnet`).
- Enforced that `--staking-enabled=false` can not be specified on public networks (`Testnet` and `Mainnet`).
### Metrics
@@ -4762,7 +4762,7 @@ This was the version used for the first public Lux Network deployment in Decembe
### Network Support
- **Primary Network**: LUX mainnet and testnet
- **Net Support**: Full L2/chain capabilities
- **Net Support**: Full chain capabilities
- **Cross-Chain**: Warp messaging and atomic swaps
- **Validator Management**: Staking and delegation
+4 -4
View File
@@ -10,10 +10,10 @@ import (
"sync"
"syscall"
"github.com/luxfi/filesystem/perms"
"github.com/luxfi/log"
"github.com/luxfi/node/node"
"github.com/luxfi/node/utils"
"github.com/luxfi/filesystem/perms"
"github.com/luxfi/sys/ulimit"
nodeconfig "github.com/luxfi/node/config/node"
@@ -58,9 +58,9 @@ func New(config nodeconfig.Config) (App, error) {
infoLevel, _ := log.ToLevel("info")
logFactory := log.NewFactoryWithConfig(log.Config{
RotatingWriterConfig: log.RotatingWriterConfig{
MaxSize: 8, // 8MB per log file (reasonable for standard profile)
MaxFiles: 5, // Keep 5 rotated files
MaxAge: 7, // 7 days retention
MaxSize: 8, // 8MB per log file (reasonable for standard profile)
MaxFiles: 5, // Keep 5 rotated files
MaxAge: 7, // 7 days retention
Directory: config.DatabaseConfig.Path,
},
DisplayLevel: infoLevel,
+1 -1
View File
@@ -7,10 +7,10 @@ import (
"sync"
"time"
validators "github.com/luxfi/validators"
"github.com/luxfi/ids"
"github.com/luxfi/log"
metrics "github.com/luxfi/metric"
validators "github.com/luxfi/validators"
)
// NewManager creates a new benchlist manager
+1 -1
View File
@@ -8,9 +8,9 @@ import (
"testing"
"time"
"github.com/luxfi/crypto/hash"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/crypto/hash"
)
// BenchmarkHashingComputeHash256 benchmarks SHA256 hashing performance
+1 -1
View File
@@ -7,11 +7,11 @@ import (
"fmt"
"testing"
"github.com/luxfi/crypto/hash"
"github.com/luxfi/database/memdb"
"github.com/luxfi/database/prefixdb"
"github.com/luxfi/database/versiondb"
"github.com/luxfi/ids"
"github.com/luxfi/crypto/hash"
)
// BenchmarkMemoryDatabase benchmarks in-memory database operations
+1 -1
View File
@@ -9,8 +9,8 @@ import (
"encoding/binary"
"testing"
"github.com/luxfi/ids"
compression "github.com/luxfi/compress"
"github.com/luxfi/ids"
)
// BenchmarkMessageCompression benchmarks message compression
+1 -1
View File
@@ -6,9 +6,9 @@ package lru
import (
"sync"
"github.com/luxfi/container/linked"
"github.com/luxfi/node/cache"
"github.com/luxfi/utils"
"github.com/luxfi/container/linked"
)
var _ cache.Cacher[struct{}, struct{}] = (*Cache[struct{}, struct{}])(nil)
+1 -1
View File
@@ -6,9 +6,9 @@ package lru
import (
"sync"
"github.com/luxfi/container/linked"
"github.com/luxfi/node/cache"
"github.com/luxfi/utils"
"github.com/luxfi/container/linked"
)
var _ cache.Cacher[struct{}, any] = (*SizedCache[struct{}, any])(nil)
+1 -1
View File
@@ -6,8 +6,8 @@ package cache
import (
"sync"
"github.com/luxfi/utils"
"github.com/luxfi/container/linked"
"github.com/luxfi/utils"
)
var _ Cacher[struct{}, struct{}] = (*LRU[struct{}, struct{}])(nil)
+1 -1
View File
@@ -6,8 +6,8 @@ package cache
import (
"sync"
"github.com/luxfi/utils"
"github.com/luxfi/container/linked"
"github.com/luxfi/utils"
)
var _ Cacher[struct{}, any] = (*sizedLRU[struct{}, any])(nil)
+1 -1
View File
@@ -8,13 +8,13 @@ import (
"errors"
"sync"
"github.com/luxfi/vm/chain"
consensusvertex "github.com/luxfi/consensus/engine/vertex"
"github.com/luxfi/database"
"github.com/luxfi/ids"
"github.com/luxfi/node/vms/platformvm/fx"
"github.com/luxfi/runtime"
vmcore "github.com/luxfi/vm"
"github.com/luxfi/vm/chain"
"github.com/luxfi/warp"
)
+336 -42
View File
@@ -9,9 +9,9 @@ import (
"context"
"crypto"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"github.com/go-json-experiment/json"
"net/http"
"os"
"path/filepath"
@@ -305,6 +305,19 @@ func getValidatorState(state validators.State) validators.State {
return &noopValidatorState{}
}
// quorumValidatorStateLive reports whether a HEIGHT-INDEXED validator state has
// actually been published for a K>1 quorum chain. A nil state means the P-Chain
// never published its validators.State into the manager — getValidatorState would
// then supply the no-op State, whose GetValidatorSet is empty at every height, so
// the ⅔-by-stake tally is 0 and VerifyWeighted fails closed FOREVER (a silent
// permanent finality stall). This predicate is the fail-closed guard (CRITICAL-1
// (c)): a K>1 chain whose state is not live must REFUSE TO START loudly rather
// than run with the no-op State. It is a pure function so the guard is unit-
// testable without building a whole chain (quorum_guard_node_test.go).
func quorumValidatorStateLive(state validators.State) bool {
return state != nil
}
// createWarpSigner creates a warp.Signer from a bls.Signer
func createWarpSigner(sk bls.Signer, networkID uint32, chainID ids.ID) warp.Signer {
if sk == nil {
@@ -345,17 +358,41 @@ type ManagerConfig struct {
PartialSyncPrimaryNetwork bool
Server server.Server // Handles HTTP API calls
AtomicMemory *atomic.Memory
XAssetID ids.ID
UTXOAssetID ids.ID
SkipBootstrap bool // Skip bootstrapping and start processing immediately
EnableAutomining bool // Enable automining in POA mode
XChainID ids.ID // ID of the X-Chain,
CChainID ids.ID // ID of the C-Chain,
DChainID ids.ID // ID of the D-Chain (DEX),
CriticalChains set.Set[ids.ID] // Chains that can't exit gracefully
TimeoutManager timeout.Manager // Manages request timeouts when sending messages to other validators
Health health.Registerer
NetConfigs map[ids.ID]nets.Config // ID -> NetConfig
ChainConfigs map[string]ChainConfig // alias -> ChainConfig
// ChainAuthorizations gates per-validator activation of specific chains on
// X-Chain NFT ownership: a validator may track/validate a listed chain
// only if its staking X-address (StakingXAddress) holds the required
// nftfx NFT. A nil/empty map means no chain is NFT-gated, so every chain
// behaves exactly as before. Critical chains are never gated regardless of
// this map. Node wiring (e.g. DChainID -> dex-validator collection) is set
// in node.go; see authorizeChainActivation.
ChainAuthorizations map[ids.ID]NFTAuthorization
// StakingXAddress is this node's X-Chain address derived from its staking
// keys; it is the owner whose UTXOs the NFT-authorization gate inspects.
// Empty when no chain is gated; an empty address with a gated chain causes
// that chain to be opted out (fail closed).
StakingXAddress ids.ShortID
// DexValidator is the operator's opt-in to PARTICIPATE in the D-Chain DEX
// (track/validate it + dial the venue matcher). It is a NECESSARY condition
// for D-Chain activation that composes AND-wise with the NFT gate: the
// D-Chain (DChainID) activates only if DexValidator is true AND the NFT
// requirement (if any) is satisfied. Default false means a node does NOT
// activate the D-Chain even when it is queued by --track-all-chains; the
// activation authority is authorizeChainActivation, not the platformvm
// tracking set. Only the D-Chain consults this flag; every other chain is
// untouched. Sourced from node Config.DexValidator (see node.go).
DexValidator bool
TimeoutManager timeout.Manager // Manages request timeouts when sending messages to other validators
Health health.Registerer
NetConfigs map[ids.ID]nets.Config // ID -> NetConfig
ChainConfigs map[string]ChainConfig // alias -> ChainConfig
// ShutdownNodeFunc allows the chain manager to issue a request to shutdown the node
ShutdownNodeFunc func(exitCode int)
MeterVMEnabled bool // Should each VM be wrapped with a MeterVM
@@ -425,6 +462,16 @@ type manager struct {
pendingVMChainsLock sync.RWMutex
pendingVMChains map[ids.ID][]ChainParameters
// gatedChainsLock guards the NFT-authorization gate's defer state.
// pendingGatedChains holds chains parked because the X-Chain was not yet
// bootstrapped when their activation was attempted; they are re-queued once
// the X-Chain is created (retryPendingGatedChains). gatedAttempts caps
// re-attempts per chain so a never-bootstrapping X-Chain cannot park a
// chain forever. See manager_authz.go.
gatedChainsLock sync.Mutex
pendingGatedChains []ChainParameters
gatedAttempts map[ids.ID]int
chainsLock sync.Mutex
// Key: Chain's ID
// Value: The chain
@@ -502,6 +549,7 @@ func New(config *ManagerConfig) (Manager, error) {
unblockChainCreatorCh: make(chan struct{}),
chainCreatorShutdownCh: make(chan struct{}),
pendingVMChains: make(map[ids.ID][]ChainParameters),
gatedAttempts: make(map[ids.ID]int),
luxGatherer: luxGatherer,
handlerGatherer: handlerGatherer,
@@ -563,6 +611,44 @@ func (m *manager) createChain(chainParams ChainParameters) {
sb, _ := m.Nets.GetOrCreate(chainParams.ChainID)
// NFT-authorization gate: a chain listed in ChainAuthorizations may only be
// activated if this validator's staking X-address holds the required
// X-Chain NFT. Ungated and critical chains short-circuit to authorized, so
// this is a no-op for P/C/X/Q/… and whenever ChainAuthorizations is empty.
if authorized, ready := m.authorizeChainActivation(chainParams.ID); !ready {
// The X-Chain is not bootstrapped yet, so the gate cannot decide.
// Park the chain out of the queue and retry once the X-Chain is
// created; do not skip permanently. The attempt cap bounds this.
if m.deferGatedChain(chainParams) {
m.Log.Info("deferring gated chain activation until X-Chain bootstrapped",
log.Stringer("chainID", chainParams.ID),
log.Stringer("vmID", chainParams.VMID),
)
return
}
// Cap exhausted: the X-Chain never became queryable. Opt out cleanly,
// exactly like an unauthorized chain — mark bootstrapped, no failing
// health check.
m.Log.Warn("opting out of gated chain: X-Chain did not bootstrap in time",
log.Stringer("chainID", chainParams.ID),
log.Stringer("vmID", chainParams.VMID),
)
sb.Bootstrapped(chainParams.ID)
return
} else if !authorized {
// Clean opt-out: this validator does not hold the required NFT. Mirror
// the VM-plugin-not-loaded skip exactly — mark the slot bootstrapped
// and return WITHOUT a failing health check, so the node stays healthy
// while declining to validate a chain it is not authorized for.
m.Log.Info("chain activation not authorized — validator X-address does not hold the required NFT",
log.Stringer("chainID", chainParams.ChainID),
log.Stringer("chainID", chainParams.ID),
log.Stringer("vmID", chainParams.VMID),
)
sb.Bootstrapped(chainParams.ID)
return
}
// Note: buildChain builds all chain's relevant objects (notably engine and handler)
// but does not start their operations. Starting of the handler (which could potentially
// issue some internal messages), is delayed until chain dispatching is started and
@@ -698,6 +784,13 @@ func (m *manager) createChain(chainParams ChainParameters) {
m.chains[chainParams.ID] = chain
m.chainsLock.Unlock()
// The X-Chain is now tracked, so the NFT-authorization gate can query it.
// Re-queue any gated chains that were parked waiting for it (no-op when
// nothing is parked or this is not the X-Chain).
if chainParams.ID == m.XChainID {
m.retryPendingGatedChains()
}
// Associate the newly created chain with its default alias
if err := m.Alias(chainParams.ID, chainParams.ID.String()); err != nil {
m.Log.Error("failed to alias the new chain with itself",
@@ -915,16 +1008,15 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
XChainID: m.XChainID,
CChainID: m.CChainID,
XAssetID: m.XAssetID,
UTXOAssetID: m.UTXOAssetID,
ChainDataDir: chainDataDir,
BCLookup: m,
ValidatorState: getValidatorState(m.validatorState),
SharedMemory: chainSharedMemory,
Metrics: chainMetricsGatherer,
Log: chainLog,
WarpSigner: warpSigner,
NetworkUpgrades: &m.Upgrades,
BCLookup: m,
ValidatorState: getValidatorState(m.validatorState),
SharedMemory: chainSharedMemory,
Metrics: chainMetricsGatherer,
Log: chainLog,
WarpSigner: warpSigner,
}
// Get a factory for the vm we want to use on our chain
@@ -1064,6 +1156,57 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
}
m.Log.Info("VM initialized successfully", log.Stringer("chainID", chainParams.ID))
// CRITICAL-1(a): publish the P-Chain's HEIGHT-INDEXED validators.State so
// every K>1 chain built AFTER it (C-Chain, L1s, …) resolves the weighted
// validator set / per-voter pubkeys / stake at the block's P-chain epoch
// height. The platformvm VM instance IS a validators.State (it embeds the
// height-indexed pvalidators.Manager built from its own state DB, with the
// real GetValidatorSet(ctx, height, netID)). It only EXISTS after the
// P-Chain VM is initialized — which is here — and the P-Chain is created
// before any other primary-network chain (single serial chain-creator
// goroutine), so this assignment happens-before every later chain reads
// m.validatorState in the K>1 wiring block below. Without this the field
// stays nil → getValidatorState returns the no-op → empty set at every
// height → ⅔-stake tally is 0 → finality stalls forever on every K>1 chain
// (the bug the fail-closed guard now also catches). Plain assignment is
// race-free: chain creation is serialized (dispatchChainCreator).
if chainParams.ID == constants.PlatformChainID {
if vdrState, ok := vmImpl.(validators.State); ok {
m.validatorState = vdrState
m.Log.Info("published P-Chain height-indexed validator state to chain manager (MEDIUM-1/CRITICAL-1)",
log.Stringer("chainID", chainParams.ID))
} else {
// The P-Chain MUST be a validators.State; if it is not, every K>1
// chain (including the P-Chain itself) would stall. Fail loud.
return nil, fmt.Errorf(
"P-Chain VM %T does not implement validators.State — cannot publish the "+
"height-indexed validator set; every K>1 quorum chain would stall finality",
vmImpl)
}
}
// X-Chain (and any DAG-native VM) must be linearized into linear block mode
// BEFORE it goes Ready, so its embedded block builder is wired to the real
// toEngine channel the cert runtime reads from. Interface-gated: only VMs that
// implement Linearize (X-Chain) take this path; C/D/P/Q are untouched. This is
// what lets X-Chain finalize through the same 2/3-stake cert path as every other
// chain instead of the (now-removed) undriven DAG engine.
if linearVM, ok := vmTyped.(interface {
Linearize(context.Context, ids.ID, chan<- vm.Message) error
}); 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)
if err := linearVM.Linearize(linCtx, ids.Empty, toEngine); err != nil {
linCancel()
m.Log.Error("failed to linearize VM",
log.Stringer("chainID", chainParams.ID),
log.Err(err))
return nil, fmt.Errorf("failed to linearize VM into linear block mode: %w", err)
}
linCancel()
}
// Transition VM to normal operation after initialization
// For genesis-based networks with pre-configured validators, this is required
// to make the VM APIs available immediately
@@ -1123,39 +1266,109 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
)
// Choose consensus parameters based on mode:
// - Single-node (--dev, sybil protection disabled): K=1, self-voting
// - Multi-node (normal): K=3, 2/3 threshold
var consensusParams consensusconfig.Parameters
if !m.SybilProtectionEnabled {
consensusParams = consensusconfig.Parameters{
K: 1,
Alpha: 1.0,
AlphaPreference: 1,
AlphaConfidence: 1,
Beta: 1,
ConcurrentPolls: 1,
OptimalProcessing: 1,
MaxOutstandingItems: 256,
MaxItemProcessingTime: 30 * time.Second,
// - Single-node (--dev, sybil protection disabled): K=1, self-voting
// (the sole validator's accept is the 1-of-1 quorum).
// - Multi-node (sybil-protected): a BYZANTINE-fault-tolerant param set
// selected by network (Mainnet K=21 / Testnet K=11 / Default K=20).
//
// CRITICAL-2: the prior code used LocalParams() (K=3, α=2 → f=0, CFT) for
// ALL sybil-protected nets — a SINGLE Byzantine validator forks K=3/α=2.
// We now select a real BFT set and FAIL CLOSED if it is not value-safe.
consensusParams := selectConsensusParams(m.SybilProtectionEnabled, m.NetworkID)
if m.SybilProtectionEnabled {
if err := consensusParams.ValidateForValueNetwork(m.NetworkID); err != nil {
return nil, fmt.Errorf("refusing to start multi-node chain %s with non-BFT consensus params: %w", chainParams.ID, err)
}
} else {
consensusParams = consensusconfig.LocalParams()
}
consensusEngine := consensuschain.NewRuntime(consensuschain.NetworkConfig{
// HIGH-4: wire the α-of-K vote/cert topology for multi-validator (K>1)
// chains so finality is LIVE (votes broadcast to all, certs gossiped,
// followers finalize on the cert). Without a verifier a K>1 engine refuses
// to Start; without the gossiper Mode() reports degraded and value-DEX is
// refused. For K==1 these stay nil (no quorum, no signatures).
gossiper := &networkGossiper{net: m.Net, msgCreator: m.MsgCreator, networkID: networkID}
netCfg := consensuschain.NetworkConfig{
ChainID: chainParams.ID,
NetworkID: networkID,
NodeID: m.NodeID,
Validators: m.Validators, // CRITICAL: Pass validator sampler for k-peer polling
Logger: m.Log,
Gossiper: &networkGossiper{net: m.Net, msgCreator: m.MsgCreator, networkID: networkID},
Gossiper: gossiper,
VM: blockBuilder,
Params: &consensusParams,
})
}
if consensusParams.K > 1 {
// CRITICAL-1 FAIL-CLOSED GUARD (c): a K>1 quorum chain finalizes ONLY
// on a ⅔-by-stake supermajority read from the HEIGHT-INDEXED validator
// state. If that state was never published (m.validatorState == nil →
// getValidatorState returns the no-op, whose GetValidatorSet is empty at
// every height), the stake tally is 0 → VerifyWeighted fails closed →
// NO block EVER finalizes. That is a SILENT permanent finality stall.
// Refuse to build the chain LOUDLY instead. The P-Chain publishes its
// own height-indexed State into m.validatorState the moment it is
// created (it is built before any other K>1 chain), so the P-Chain and
// every chain after it sees a live state here; only a genuine wiring
// regression trips this.
if !quorumValidatorStateLive(m.validatorState) {
return nil, fmt.Errorf(
"refusing to start K>1 quorum chain %s: height-indexed validator state is not wired "+
"(getValidatorState would supply the no-op State → zero stake at every height → "+
"VerifyWeighted fails closed → finality would stall permanently). The P-Chain must be "+
"created first and publish its validators.State; this is a wiring bug, not a runtime condition",
chainParams.ID)
}
// Height-indexed validator state is the SINGLE source of epoch truth for
// ALL FOUR epoch-pinned reads (MEDIUM-1 / CRITICAL-1 / RESIDUAL-B):
// membership, per-voter PUBKEY (the verifier), the ⅔-by-stake tally, and
// the set-root commitment. They are all read at the block's P-CHAIN epoch
// height so every node computes the IDENTICAL set/pubkeys/weights/root for
// a given block — independent of async current-map skew during a P-chain /
// L1-staking change. (Reading the CURRENT map made the signer and the
// assembler disagree on the set-root, and dropped the votes of validators
// that had left the current map but were members at the epoch — stalling
// finality at every staking change.)
vdrState := getValidatorState(m.validatorState)
// Vote verifier resolves the voter's pubkey from set@epoch (RESIDUAL-B),
// the SAME height-pinned source as the stake + set-root — NOT m.Validators
// (the current map).
netCfg.VoteVerifier = newBLSVoteVerifier(vdrState, networkID)
netCfg.VoteSigner = newBLSVoteSigner(m.StakingBLSKey)
// Stake-weighted finality (HIGH-3): require a ⅔-of-stake supermajority,
// not just the α-of-K count, so a low-stake coalition cannot finalize.
netCfg.StakeSource = newValidatorStakeSource(vdrState, networkID)
// Epoch binding (MEDIUM-1): pin every vote/cert to the weighted validator
// set IN FORCE AT the block's P-chain epoch height so the ⅔-by-stake
// predicate is enforced at that epoch — a cert gathered under one set
// cannot be re-verified against another (its signatures were over this
// height-pinned root).
netCfg.ValidatorSetRoot = newValidatorSetRootSource(vdrState, networkID)
// b2: deliver the REAL P-chain epoch height to the engine. The four reads
// above are height-pinned but the engine reads that height off the VM
// block (pChainHeightOf) — and a bare plugin block exposes none, so the
// engine would resolve the set at P-chain height 0 (the GENESIS set),
// freezing the epoch and dropping every post-genesis validator's vote.
// Wrap the BlockBuilder so the block the engine builds/parses carries the
// proposer's live P-chain height (max(GetCurrentHeight, parentH)), stamped
// into the gossiped bytes so every follower adopts the IDENTICAL height —
// the set-root/stake/pubkey reads then track the LIVE set at that height.
// Installed ONLY here (K>1): a K==1 chain has no cert and no epoch, so the
// stamp is inert and the inner VM is used directly.
if blockBuilder != nil {
blockBuilder = newPChainHeightVM(blockBuilder, vdrState, networkID)
netCfg.VM = blockBuilder
m.Log.Info("wired P-chain epoch height into consensus block builder (b2)",
log.Stringer("chainID", chainParams.ID),
log.Stringer("networkID", networkID))
}
}
consensusEngine := consensuschain.NewRuntime(netCfg)
// Start the consensus engine
engineStartCtx, engineStartCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer engineStartCancel()
if err := consensusEngine.Start(engineStartCtx, true); err != nil {
// Start the consensus engine with a LIFETIME context (not a timeout):
// engine.Start parents all four long-running loops (poll, vote, pipeline,
// re-poll) to this ctx, so a WithTimeout here kills them ~30s after the
// chain starts and the quorum cert never assembles — the finality wedge
// fixed in v1.30.55 (ba3561778e). Do not reintroduce a timeout here.
if err := consensusEngine.Start(context.Background(), true); err != nil {
m.Log.Error("failed to start consensus engine",
log.Stringer("chainID", chainParams.ID),
log.Err(err))
@@ -1228,7 +1441,11 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
Runtime: chainRuntime,
VM: vmTyped, // Use the real VM directly
Engine: consensusEngine, // Use real consensus engine directly
Handler: newBlockHandler(vmTyped, m.Log, consensusEngine, m.Net, m.MsgCreator, chainParams.ID, networkID),
// Handler parses inbound blocks through the SAME builder the engine emits
// 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.
Handler: newBlockHandler(blockBuilder, m.Log, consensusEngine, m.Net, m.MsgCreator, chainParams.ID, networkID),
}
default:
return nil, fmt.Errorf("unsupported VM type: %T", vmImpl)
@@ -1483,7 +1700,7 @@ func (m *manager) createDAG(
}
}
// Linearize the DAG chain (required for X-Chain post-Cortina)
// Linearize the DAG chain (required for X-Chain).
// This transitions the chain from DAG mode to linear block mode.
if vmInitialized {
if linearVM, ok := vmImpl.(interface {
@@ -2087,7 +2304,15 @@ func (e *emptyValidatorManager) GetCurrentValidators(ctx context.Context, height
// blockHandler implements handler.Handler interface and processes incoming blocks
// This enables block propagation between validators
type blockHandler struct {
vm chain.ChainVM
// vm is the SAME BlockBuilder the consensus engine builds/parses through — the
// P-chain-height-stamping wrapper on K>1 chains (so ParseBlock unwraps the
// transport envelope the engine emits and recovers the proposer's epoch
// height), the inner VM directly on K==1. The handler only needs the
// BlockBuilder subset (GetBlock / ParseBlock / LastAccepted); typing it as the
// builder — not chain.ChainVM — keeps the engine and the handler on ONE block
// codec, so inbound P2P container bytes are parsed by the same code that framed
// them (no raw-vs-wrapped mismatch).
vm consensuschain.BlockBuilder
logger log.Logger
engine *consensuschain.Runtime // Consensus engine for proper block handling
net network.Network // Network for sending Qbit responses
@@ -2126,7 +2351,7 @@ type contextRequest struct {
timestamp time.Time
}
func newBlockHandler(vm chain.ChainVM, logger log.Logger, engine *consensuschain.Runtime, net network.Network, msgCreator message.OutboundMsgBuilder, chainID ids.ID, networkID ids.ID) *blockHandler {
func newBlockHandler(vm consensuschain.BlockBuilder, logger log.Logger, engine *consensuschain.Runtime, net network.Network, msgCreator message.OutboundMsgBuilder, chainID ids.ID, networkID ids.ID) *blockHandler {
return &blockHandler{
vm: vm,
logger: logger,
@@ -2757,7 +2982,27 @@ func (b *blockHandler) Response(ctx context.Context, nodeID ids.NodeID, requestI
return nil
}
func (b *blockHandler) Gossip(ctx context.Context, nodeID ids.NodeID, msg []byte) error {
// Handle Gossip - try to process as block
// HIGH-4 receive demux: a quorum envelope (signed vote / finality cert) is
// routed to the engine's α-of-K topology; anything else is a plain block
// gossip handled by Put (backward-compatible — decode fails soft).
//
// The quorum path is consulted ONLY when the engine is in quorum-finality
// mode (K>1, live topology). A single-validator engine never emits vote/cert
// envelopes, so it always treats gossip as a block — this makes a chance
// collision of the envelope magic with block bytes impossible to misroute on
// the K==1 path.
if b.engine != nil && b.engine.Mode() == consensuschain.ModeQuorumFinality {
if kind, blockID, payload, err := decodeQuorumGossip(msg); err == nil {
switch kind {
case quorumKindVote:
b.engine.HandleIncomingVote(blockID, payload)
case quorumKindCert:
b.engine.HandleIncomingCert(payload)
}
return nil
}
}
// Plain block gossip.
return b.Put(ctx, nodeID, 0, msg)
}
func (b *blockHandler) GetStateSummaryFrontier(ctx context.Context, nodeID ids.NodeID, requestID uint32, deadline time.Time) error {
@@ -2858,6 +3103,14 @@ func (b *blockHandler) HandleInbound(ctx context.Context, msg handler.Message) e
case handler.Context:
// Context contains prerequisite blocks - process each one via Put
return b.handleContext(ctx, msg.NodeID, msg.RequestID, msg.Message)
case handler.Gossip:
// App-gossip envelope carrying an α-of-K quorum vote or finality cert.
// Route to Gossip, which demuxes the quorum envelope into
// engine.HandleIncomingVote / HandleIncomingCert (the vote TRANSPORT that
// drives finality). Without this case the router delivers the message here
// but the switch drops it, so no vote is ever counted and the chain wedges.
// Non-quorum gossip falls through inside Gossip to a plain block Put.
return b.Gossip(ctx, msg.NodeID, msg.Message)
}
return nil
}
@@ -2981,6 +3234,47 @@ type networkGossiper struct {
// Compile-time check that networkGossiper implements Gossiper
var _ consensuschain.Gossiper = (*networkGossiper)(nil)
// Compile-time check that networkGossiper implements QuorumGossiper — the
// vote/cert distribution topology that makes α-of-K finality LIVE (HIGH-4).
// Without this the consensus engine runs degraded (Mode() != ModeQuorumFinality)
// and value-DEX is refused.
var _ consensuschain.QuorumGossiper = (*networkGossiper)(nil)
// BroadcastVote sends this node's SIGNED accept vote for blockID to ALL
// validators on the network (not just the proposer). The signed vote rides on
// app-gossip framed in a quorum envelope (decoded by blockHandler.Gossip into
// engine.HandleIncomingVote). Broadcasting to ALL — rather than SendVote-to-
// proposer-only — is the structural fix for the proposer-freeze: any node that
// collects α distinct signed votes can assemble + gossip the cert, so finality
// no longer hinges on one node's inbound Chits.
func (g *networkGossiper) BroadcastVote(chainID ids.ID, networkID ids.ID, blockID ids.ID, voteBytes []byte) int {
if g.net == nil || g.msgCreator == nil {
return 0
}
envelope := encodeQuorumGossip(quorumKindVote, blockID, voteBytes)
msg, err := g.msgCreator.Gossip(chainID, envelope)
if err != nil {
return 0
}
return g.net.Gossip(msg, nil, g.networkID, -1, 0, 0).Len()
}
// 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
// established by the verified cert.
func (g *networkGossiper) GossipCert(chainID ids.ID, networkID ids.ID, blockID ids.ID, certBytes []byte) int {
if g.net == nil || g.msgCreator == nil {
return 0
}
envelope := encodeQuorumGossip(quorumKindCert, blockID, certBytes)
msg, err := g.msgCreator.Gossip(chainID, envelope)
if err != nil {
return 0
}
return g.net.Gossip(msg, nil, g.networkID, -1, 0, 0).Len()
}
// GossipPut broadcasts a Put message with block data to validators.
func (g *networkGossiper) GossipPut(chainID ids.ID, networkID ids.ID, blockData []byte) int {
if g.net == nil || g.msgCreator == nil {
+220
View File
@@ -0,0 +1,220 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package chains
import (
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/math/set"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/nftfx"
)
// maxGatedActivationAttempts bounds how many times a gated chain may be
// deferred waiting for the X-Chain to bootstrap before it is opted out. Each
// X-Chain creation re-queues parked chains exactly once, so this also bounds
// total re-queues per chain. It exists only as defense-in-depth against an
// X-Chain that never bootstraps; the normal path defers zero or one time.
const maxGatedActivationAttempts = 8
// NFTAuthorization identifies the X-Chain NFT a validator's staking address
// must hold to activate (track/validate) a gated chain.
//
// A chain is "gated" when ManagerConfig.ChainAuthorizations has an entry for
// its blockchain ID. Chains with no entry are ungated and always authorized —
// so a nil/empty ChainAuthorizations map preserves today's behavior exactly
// for every chain (P/C/X/Q/D/…).
type NFTAuthorization struct {
AssetID ids.ID // X-Chain nftfx asset (the collection)
GroupID uint32 // nftfx group within the collection; 0 = any group
}
// xChainUTXOReader is the narrow in-process accessor the gate needs from the
// X-Chain VM: list the UTXOs owned by an address set. *xvm.VM satisfies it via
// its GetUTXOs method (vms/xvm/vm.go), which wraps lux.GetAllUTXOs over the
// chain's committed state. Keeping the interface to this one method avoids
// importing the concrete xvm VM type into the chain manager.
type xChainUTXOReader interface {
GetUTXOs(addrs set.Set[ids.ShortID]) ([]*lux.UTXO, error)
}
// holdsAuthorizationNFT is the pure authorization policy: it reports whether
// [utxos] contains an nftfx transfer output that satisfies [auth]. A UTXO
// matches when its output is an *nftfx.TransferOutput for auth.AssetID and,
// when auth.GroupID is non-zero, its GroupID equals auth.GroupID (GroupID 0
// means "any group in the collection").
//
// This is the one place the NFT-ownership rule lives. It takes plain values
// (the fetched UTXOs and the requirement) and returns a decision, so it is
// unit-testable without standing up a manager or an X-Chain.
func holdsAuthorizationNFT(utxos []*lux.UTXO, auth NFTAuthorization) bool {
for _, utxo := range utxos {
out, ok := utxo.Out.(*nftfx.TransferOutput)
if !ok {
continue
}
if utxo.AssetID() != auth.AssetID {
continue
}
if auth.GroupID != 0 && out.GroupID != auth.GroupID {
continue
}
return true
}
return false
}
// authorizeChainActivation reports whether this node may activate
// (track/validate) [chainID].
//
// Returns (authorized, ready):
// - ready=false means the gate cannot decide yet because the X-Chain is not
// bootstrapped and therefore not queryable in-process. The caller MUST
// defer (re-attempt later), not skip — see createChain.
// - ready=true, authorized=true: proceed normally.
// - ready=true, authorized=false: clean opt-out — this validator's staking
// X-address does not hold the required NFT.
//
// Decision order:
// 1. Critical chains (P/C/X/…) are never gated — always (true, true).
// 2. Ungated chains (no ChainAuthorizations entry) are always (true, true),
// so the zero/nil map is a no-op for every chain.
// 3. Gated chain, X-Chain not bootstrapped → (false, false): defer.
// 4. Gated chain, X-Chain bootstrapped, but no usable in-process UTXO reader
// or no resolvable staking X-address → (false, true): clean opt-out. The
// node refuses to activate a gated chain it cannot prove authorization
// for, rather than fail open.
// 5. Otherwise query the X-Chain for the staking X-address's UTXOs and apply
// holdsAuthorizationNFT.
func (m *manager) authorizeChainActivation(chainID ids.ID) (authorized bool, ready bool) {
// Critical chains are foundational; the gate must never skip or defer them.
if m.CriticalChains.Contains(chainID) {
return true, true
}
// D-Chain participation is an operator opt-in (dex-validator), and it is a
// NECESSARY condition that composes AND-wise with the NFT gate below: the
// D-Chain activates only if the operator opted in AND (when configured) the
// staking X-address holds the dex-operator NFT. Checked here — before the
// ungated short-circuit — so the opt-in is required even when no operator
// collection is configured for this network (the otherwise-ungated case).
// A node without dex-validator therefore cleanly opts out of the D-Chain
// even when --track-all-chains queued it: this is the activation authority,
// not the platformvm tracking set. The opt-out is decided (ready=true), so
// the caller takes the same clean skip as a non-held NFT / absent plugin.
if m.DChainID != ids.Empty && chainID == m.DChainID && !m.DexValidator {
m.Log.Info("D-Chain activation declined: dex-validator opt-in is disabled",
log.Stringer("chainID", chainID),
)
return false, true
}
auth, gated := m.ChainAuthorizations[chainID]
if !gated {
return true, true
}
// TODO(phase-2): proof-of-possession. holdsAuthorizationNFT below checks
// ownership-OF-RECORD (the staking X-address appears in an nftfx output's
// owners) — it does NOT prove the node controls the key for that address.
// Phase-2 must derive StakingXAddress from the node's staking keys and bind
// the NFT check to key-control (a signed challenge), so a node cannot claim
// authorization from an NFT held at an address it does not control.
// The gate consults the X-Chain UTXO set, so the X-Chain must already be
// bootstrapped (created and tracked) on this node. If not, we cannot decide
// yet — signal the caller to defer.
if !m.IsBootstrapped(m.XChainID) {
return false, false
}
// The staking X-address must be wired for any gated chain. If it is empty,
// the node has no identity to check NFT ownership against; refuse to
// activate the gated chain (fail closed) rather than guess.
if m.StakingXAddress == ids.ShortEmpty {
m.Log.Warn("gated chain activation blocked: staking X-address not configured",
log.Stringer("chainID", chainID),
log.Stringer("assetID", auth.AssetID),
)
return false, true
}
reader := m.xChainUTXOReader()
if reader == nil {
m.Log.Warn("gated chain activation blocked: X-Chain UTXO reader unavailable",
log.Stringer("chainID", chainID),
log.Stringer("assetID", auth.AssetID),
)
return false, true
}
owners := set.Of(m.StakingXAddress)
utxos, err := reader.GetUTXOs(owners)
if err != nil {
// A query error is not a "decline" — the X-Chain claims to be
// bootstrapped but cannot answer. Defer and retry rather than
// permanently opt out on a transient read failure.
m.Log.Warn("gated chain activation deferred: X-Chain UTXO query failed",
log.Stringer("chainID", chainID),
log.Stringer("assetID", auth.AssetID),
log.Err(err),
)
return false, false
}
return holdsAuthorizationNFT(utxos, auth), true
}
// xChainUTXOReader returns the in-process UTXO reader for the X-Chain, or nil
// if the X-Chain VM is not present or does not expose the accessor. The
// X-Chain VM (*xvm.VM) implements xChainUTXOReader; this asserts against the
// narrow interface only.
func (m *manager) xChainUTXOReader() xChainUTXOReader {
m.chainsLock.Lock()
info, ok := m.chains[m.XChainID]
m.chainsLock.Unlock()
if !ok {
return nil
}
reader, _ := info.VM.(xChainUTXOReader)
return reader
}
// deferGatedChain parks [chainParams] out of the creation queue because the
// X-Chain is not yet bootstrapped, and reports whether the chain may still be
// retried. It returns false once the per-chain attempt cap is exhausted, in
// which case the caller must opt the chain out (a never-bootstrapping X-Chain
// must not park a chain forever). Parking out of the queue — rather than
// re-pushing — is what keeps the single-threaded dispatch loop from
// busy-spinning; retryPendingGatedChains re-pushes when the X-Chain is created.
func (m *manager) deferGatedChain(chainParams ChainParameters) (retry bool) {
m.gatedChainsLock.Lock()
defer m.gatedChainsLock.Unlock()
m.gatedAttempts[chainParams.ID]++
if m.gatedAttempts[chainParams.ID] > maxGatedActivationAttempts {
return false
}
m.pendingGatedChains = append(m.pendingGatedChains, chainParams)
return true
}
// retryPendingGatedChains re-queues every chain parked by deferGatedChain. It
// is called once right after the X-Chain finishes createChain, so the gate can
// now reach the X-Chain UTXO set. Chains that still cannot decide will park
// again (bounded by maxGatedActivationAttempts).
func (m *manager) retryPendingGatedChains() {
m.gatedChainsLock.Lock()
parked := m.pendingGatedChains
m.pendingGatedChains = nil
m.gatedChainsLock.Unlock()
for _, chainParams := range parked {
m.Log.Info("re-queuing gated chain after X-Chain bootstrap",
log.Stringer("chainID", chainParams.ID),
log.Stringer("vmID", chainParams.VMID),
)
m.chainsQueue.PushRight(chainParams)
}
}
+560
View File
@@ -0,0 +1,560 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package chains
import (
"errors"
"testing"
"github.com/stretchr/testify/require"
"github.com/luxfi/container/buffer"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/math/set"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/nftfx"
"github.com/luxfi/utxo/secp256k1fx"
)
// fakeUTXOReader is a stand-in for the X-Chain VM in unit tests. It returns a
// fixed UTXO set (or an error) regardless of the requested addresses, which is
// all the gate's address-set query needs to be driven deterministically.
type fakeUTXOReader struct {
utxos []*lux.UTXO
err error
}
func (f *fakeUTXOReader) GetUTXOs(set.Set[ids.ShortID]) ([]*lux.UTXO, error) {
return f.utxos, f.err
}
// ownerScopedUTXOReader is a faithful X-Chain stand-in: it returns ONLY the
// UTXOs whose nftfx/secp owners intersect the queried address set, exactly as
// lux.GetAllUTXOs(vm.state, addrs) does over committed state. This is what the
// fixed-list fakeUTXOReader cannot model — and it is required to test the
// ownership-scoping invariant: a node may activate a gated chain only on an NFT
// held at ITS OWN staking X-address, never one held at someone else's. The gate
// queries set.Of(StakingXAddress), so any UTXO owned by a different address is
// invisible here, just as it would be on a real X-Chain.
type ownerScopedUTXOReader struct {
utxos []*lux.UTXO
}
func (r *ownerScopedUTXOReader) GetUTXOs(addrs set.Set[ids.ShortID]) ([]*lux.UTXO, error) {
var out []*lux.UTXO
for _, utxo := range r.utxos {
owned, ok := utxo.Out.(lux.Addressable)
if !ok {
continue
}
for _, raw := range owned.Addresses() {
addr, err := ids.ToShortID(raw)
if err != nil {
continue
}
if addrs.Contains(addr) {
out = append(out, utxo)
break
}
}
}
return out, nil
}
// nftUTXO builds a UTXO whose output is an nftfx transfer output for the given
// asset and group. owner is the address recorded as the holder.
func nftUTXO(assetID ids.ID, groupID uint32, owner ids.ShortID) *lux.UTXO {
return &lux.UTXO{
UTXOID: lux.UTXOID{TxID: ids.GenerateTestID()},
Asset: lux.Asset{ID: assetID},
Out: &nftfx.TransferOutput{
GroupID: groupID,
OutputOwners: secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{owner},
},
},
}
}
// secpUTXO builds a non-NFT (secp256k1 transfer) UTXO for the given asset, used
// to prove the gate ignores outputs that are not nftfx transfers.
func secpUTXO(assetID ids.ID, owner ids.ShortID) *lux.UTXO {
return &lux.UTXO{
UTXOID: lux.UTXOID{TxID: ids.GenerateTestID()},
Asset: lux.Asset{ID: assetID},
Out: &secp256k1fx.TransferOutput{
Amt: 1,
OutputOwners: secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{owner},
},
},
}
}
func TestHoldsAuthorizationNFT(t *testing.T) {
asset := ids.GenerateTestID()
other := ids.GenerateTestID()
owner := ids.GenerateTestShortID()
tests := []struct {
name string
utxos []*lux.UTXO
auth NFTAuthorization
want bool
}{
{
name: "empty set",
utxos: nil,
auth: NFTAuthorization{AssetID: asset},
want: false,
},
{
name: "matching asset, any group",
utxos: []*lux.UTXO{nftUTXO(asset, 7, owner)},
auth: NFTAuthorization{AssetID: asset, GroupID: 0},
want: true,
},
{
name: "matching asset and group",
utxos: []*lux.UTXO{nftUTXO(asset, 7, owner)},
auth: NFTAuthorization{AssetID: asset, GroupID: 7},
want: true,
},
{
name: "matching asset, wrong group",
utxos: []*lux.UTXO{nftUTXO(asset, 7, owner)},
auth: NFTAuthorization{AssetID: asset, GroupID: 8},
want: false,
},
{
name: "wrong asset",
utxos: []*lux.UTXO{nftUTXO(other, 7, owner)},
auth: NFTAuthorization{AssetID: asset},
want: false,
},
{
name: "non-nft output for asset is ignored",
utxos: []*lux.UTXO{secpUTXO(asset, owner)},
auth: NFTAuthorization{AssetID: asset},
want: false,
},
{
name: "match found among mixed outputs",
utxos: []*lux.UTXO{
secpUTXO(asset, owner),
nftUTXO(other, 1, owner),
nftUTXO(asset, 3, owner),
},
auth: NFTAuthorization{AssetID: asset, GroupID: 3},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, holdsAuthorizationNFT(tt.utxos, tt.auth))
})
}
}
// newGateManager builds the minimal manager needed to exercise the gate:
// only the fields authorizeChainActivation reads. xChainVM, when non-nil, is
// installed as the X-Chain's tracked VM so IsBootstrapped(XChainID) is true and
// xChainUTXOReader() resolves to it.
func newGateManager(
xChainID ids.ID,
stakingAddr ids.ShortID,
authz map[ids.ID]NFTAuthorization,
critical set.Set[ids.ID],
xChainVM xChainUTXOReader,
) *manager {
m := &manager{
chains: make(map[ids.ID]*chainInfo),
gatedAttempts: make(map[ids.ID]int),
}
m.Log = log.NewNoOpLogger()
m.XChainID = xChainID
m.StakingXAddress = stakingAddr
m.ChainAuthorizations = authz
m.CriticalChains = critical
if xChainVM != nil {
m.chains[xChainID] = &chainInfo{Name: "X-Chain", VM: xChainVM}
}
return m
}
func TestAuthorizeChainActivation(t *testing.T) {
xChainID := ids.GenerateTestID()
gatedChain := ids.GenerateTestID()
asset := ids.GenerateTestID()
addr := ids.GenerateTestShortID()
collection := map[ids.ID]NFTAuthorization{
gatedChain: {AssetID: asset, GroupID: 0},
}
t.Run("ungated chain is authorized and ready", func(t *testing.T) {
// No ChainAuthorizations entry: behaves exactly as today, even with no
// X-Chain present. Covers P/C/X-like IDs via a table.
m := newGateManager(xChainID, addr, nil, nil, nil)
for _, id := range []ids.ID{ids.GenerateTestID(), xChainID, gatedChain} {
authorized, ready := m.authorizeChainActivation(id)
require.True(t, authorized, "id %s", id)
require.True(t, ready, "id %s", id)
}
})
t.Run("gated chain, X-Chain not bootstrapped, defers", func(t *testing.T) {
// xChainVM nil => XChainID not in m.chains => IsBootstrapped false.
m := newGateManager(xChainID, addr, collection, nil, nil)
authorized, ready := m.authorizeChainActivation(gatedChain)
require.False(t, ready, "must signal defer")
require.False(t, authorized)
})
t.Run("gated chain, holder, authorized", func(t *testing.T) {
reader := &fakeUTXOReader{utxos: []*lux.UTXO{nftUTXO(asset, 0, addr)}}
m := newGateManager(xChainID, addr, collection, nil, reader)
authorized, ready := m.authorizeChainActivation(gatedChain)
require.True(t, ready)
require.True(t, authorized)
})
t.Run("gated chain, non-holder, clean opt-out", func(t *testing.T) {
reader := &fakeUTXOReader{utxos: nil}
m := newGateManager(xChainID, addr, collection, nil, reader)
authorized, ready := m.authorizeChainActivation(gatedChain)
require.True(t, ready, "decided, not deferred")
require.False(t, authorized, "no NFT => opt out")
})
t.Run("group match vs mismatch", func(t *testing.T) {
groupCollection := map[ids.ID]NFTAuthorization{
gatedChain: {AssetID: asset, GroupID: 5},
}
hold5 := &fakeUTXOReader{utxos: []*lux.UTXO{nftUTXO(asset, 5, addr)}}
hold9 := &fakeUTXOReader{utxos: []*lux.UTXO{nftUTXO(asset, 9, addr)}}
authorized, ready := newGateManager(xChainID, addr, groupCollection, nil, hold5).
authorizeChainActivation(gatedChain)
require.True(t, ready)
require.True(t, authorized, "group 5 held => authorized")
authorized, ready = newGateManager(xChainID, addr, groupCollection, nil, hold9).
authorizeChainActivation(gatedChain)
require.True(t, ready)
require.False(t, authorized, "wrong group => opt out")
})
t.Run("critical chain never skipped even if gated", func(t *testing.T) {
// gatedChain is in BOTH ChainAuthorizations and CriticalChains, the
// holder holds nothing, and the X-Chain is absent. A non-critical chain
// here would defer (not ready); a critical chain must be authorized.
critical := set.Of(gatedChain)
m := newGateManager(xChainID, addr, collection, critical, nil)
authorized, ready := m.authorizeChainActivation(gatedChain)
require.True(t, authorized, "critical chain must never be gated")
require.True(t, ready, "critical chain must never defer")
})
t.Run("gated chain, empty staking address, fails closed", func(t *testing.T) {
reader := &fakeUTXOReader{utxos: []*lux.UTXO{nftUTXO(asset, 0, addr)}}
m := newGateManager(xChainID, ids.ShortEmpty, collection, nil, reader)
authorized, ready := m.authorizeChainActivation(gatedChain)
require.True(t, ready, "decided")
require.False(t, authorized, "no staking identity => opt out")
})
t.Run("gated chain, X-Chain query error, defers", func(t *testing.T) {
reader := &fakeUTXOReader{err: errors.New("state not ready")}
m := newGateManager(xChainID, addr, collection, nil, reader)
authorized, ready := m.authorizeChainActivation(gatedChain)
require.False(t, ready, "transient query error => retry, not opt out")
require.False(t, authorized)
})
t.Run("gated chain, X-Chain tracked but VM lacks reader, opts out", func(t *testing.T) {
// X-Chain is in m.chains (IsBootstrapped true) but its VM does not
// satisfy xChainUTXOReader. The gate must fail closed (ready, !authz),
// not panic.
m := newGateManager(xChainID, addr, collection, nil, nil)
m.chains[xChainID] = &chainInfo{Name: "X-Chain", VM: struct{}{}}
authorized, ready := m.authorizeChainActivation(gatedChain)
require.True(t, ready)
require.False(t, authorized)
})
}
func TestDeferGatedChainCap(t *testing.T) {
xChainID := ids.GenerateTestID()
gatedChain := ids.GenerateTestID()
m := newGateManager(xChainID, ids.GenerateTestShortID(), nil, nil, nil)
params := ChainParameters{ID: gatedChain, VMID: ids.GenerateTestID()}
// The first maxGatedActivationAttempts calls park the chain and allow retry.
for i := 0; i < maxGatedActivationAttempts; i++ {
require.True(t, m.deferGatedChain(params), "attempt %d should allow retry", i+1)
}
// One past the cap opts out instead of parking again.
require.False(t, m.deferGatedChain(params), "cap exhausted")
// Every allowed attempt parked exactly one entry; the over-cap attempt did not.
m.gatedChainsLock.Lock()
require.Len(t, m.pendingGatedChains, maxGatedActivationAttempts)
m.gatedChainsLock.Unlock()
}
func TestRetryPendingGatedChainsRequeues(t *testing.T) {
xChainID := ids.GenerateTestID()
m := newGateManager(xChainID, ids.GenerateTestShortID(), nil, nil, nil)
m.chainsQueue = buffer.NewUnboundedBlockingDeque[ChainParameters](initialQueueSize)
parked := ChainParameters{ID: ids.GenerateTestID(), VMID: ids.GenerateTestID()}
require.True(t, m.deferGatedChain(parked))
m.retryPendingGatedChains()
// Parked list is drained and the chain is re-pushed onto the creation queue.
m.gatedChainsLock.Lock()
require.Empty(t, m.pendingGatedChains)
m.gatedChainsLock.Unlock()
got, ok := m.chainsQueue.PopLeft()
require.True(t, ok)
require.Equal(t, parked.ID, got.ID)
}
// dexGatedManager builds a manager whose D-Chain (dChainID) is NFT-gated on a
// synthetic dex-operator collection — exactly the ChainAuthorizations shape
// node.chainAuthorizationsFor produces for dexvm: dChainID -> {dexAsset,
// group 0}. reader is installed as the bootstrapped X-Chain so the gate can
// query the staking address's UTXOs. This is the chain-manager-side mirror of
// the OptionalVMs[DexVMID].RequiredNFT policy under a synthetic per-network
// asset.
func dexGatedManager(dexAsset ids.ID, staking ids.ShortID, reader xChainUTXOReader) (*manager, ids.ID) {
xChainID := ids.GenerateTestID()
dChainID := ids.GenerateTestID()
authz := map[ids.ID]NFTAuthorization{
dChainID: {AssetID: dexAsset, GroupID: 0}, // group 0 = any group in the collection
}
m := newGateManager(xChainID, staking, authz, nil, reader)
// Wire the D-Chain identity and turn the operator opt-in ON, so these NFT
// tests exercise the REAL dex-validator path: dex-validator=true, then the
// NFT gate decides. (The dex-validator=false short-circuit is proven
// separately in TestDexValidatorFlagGatesDChain.)
m.DChainID = dChainID
m.DexValidator = true
return m, dChainID
}
// TestDexVMFailsClosedWithoutXNFT (#4). A node whose staking X-address holds NO
// dex-operator NFT is NOT authorized to activate the D-Chain — a clean
// fail-closed opt-out (ready=true, authorized=false), not a fail-open and not a
// deferral. The X-Chain is bootstrapped and answers; the address simply holds
// nothing. Also covers holding the WRONG asset (some other NFT) → still opt-out.
func TestDexVMFailsClosedWithoutXNFT(t *testing.T) {
dexAsset := ids.GenerateTestID()
otherAsset := ids.GenerateTestID()
staking := ids.GenerateTestShortID()
t.Run("no NFT held => opt out", func(t *testing.T) {
reader := &fakeUTXOReader{utxos: nil}
m, dChainID := dexGatedManager(dexAsset, staking, reader)
authorized, ready := m.authorizeChainActivation(dChainID)
require.True(t, ready, "X-Chain answered => decided, not deferred")
require.False(t, authorized, "no dex-operator NFT => D-Chain activation fails closed")
})
t.Run("wrong collection held => opt out", func(t *testing.T) {
reader := &fakeUTXOReader{utxos: []*lux.UTXO{nftUTXO(otherAsset, 0, staking)}}
m, dChainID := dexGatedManager(dexAsset, staking, reader)
authorized, ready := m.authorizeChainActivation(dChainID)
require.True(t, ready)
require.False(t, authorized, "holding a different collection's NFT must not authorize the D-Chain")
})
t.Run("no staking identity => fail closed", func(t *testing.T) {
// Even with a matching NFT in the set, an empty staking X-address has no
// identity to check ownership against → fail closed.
reader := &fakeUTXOReader{utxos: []*lux.UTXO{nftUTXO(dexAsset, 0, staking)}}
m, dChainID := dexGatedManager(dexAsset, ids.ShortEmpty, reader)
authorized, ready := m.authorizeChainActivation(dChainID)
require.True(t, ready)
require.False(t, authorized, "no staking identity => fail closed")
})
}
// TestDexVMActivatesWithDexOperatorNFT (#5). The same gated D-Chain, but the
// staking X-address holds the dex-operator NFT → authorized to activate.
// Proves the positive path of the NFT-governed activation.
func TestDexVMActivatesWithDexOperatorNFT(t *testing.T) {
dexAsset := ids.GenerateTestID()
staking := ids.GenerateTestShortID()
t.Run("dex-operator NFT held => authorized", func(t *testing.T) {
reader := &fakeUTXOReader{utxos: []*lux.UTXO{nftUTXO(dexAsset, 0, staking)}}
m, dChainID := dexGatedManager(dexAsset, staking, reader)
authorized, ready := m.authorizeChainActivation(dChainID)
require.True(t, ready)
require.True(t, authorized, "holding the dex-operator NFT authorizes D-Chain activation")
})
t.Run("dex-operator NFT among mixed UTXOs => authorized", func(t *testing.T) {
other := ids.GenerateTestID()
reader := &fakeUTXOReader{utxos: []*lux.UTXO{
secpUTXO(dexAsset, staking), // non-NFT output for the asset: ignored
nftUTXO(other, 1, staking), // wrong collection: ignored
nftUTXO(dexAsset, 4, staking), // the dex-operator NFT (group 4, any-group gate)
}}
m, dChainID := dexGatedManager(dexAsset, staking, reader)
authorized, ready := m.authorizeChainActivation(dChainID)
require.True(t, ready)
require.True(t, authorized, "dex-operator NFT present among other UTXOs => authorized")
})
}
// TestDexVMOwnershipScoping (H1 gap). The activation NFT must be held at the
// node's OWN staking X-address — not merely exist somewhere on the X-Chain. The
// gate queries set.Of(StakingXAddress), so an ownership-honoring reader returns
// nothing for an NFT minted to a DIFFERENT address, and the gate fails closed.
// The fixed-list fakeUTXOReader could not catch this (it returns its list for
// any query); ownerScopedUTXOReader models real per-address X-Chain state.
//
// NOTE: this proves ownership-OF-RECORD scoping (the NFT is owned by this
// address). It does NOT prove the node controls that address's key — that is
// the // TODO(phase-2): proof-of-possession seam in authorizeChainActivation,
// out of scope this pass.
func TestDexVMOwnershipScoping(t *testing.T) {
dexAsset := ids.GenerateTestID()
nodeAddr := ids.GenerateTestShortID() // this node's staking X-address
strangerAddr := ids.GenerateTestShortID() // a DIFFERENT operator's address
t.Run("dex-operator NFT held by a DIFFERENT address => not authorized", func(t *testing.T) {
// The collection's NFT exists, but it is owned by strangerAddr. An
// ownership-honoring X-Chain returns no UTXOs for nodeAddr's query.
reader := &ownerScopedUTXOReader{utxos: []*lux.UTXO{nftUTXO(dexAsset, 0, strangerAddr)}}
m, dChainID := dexGatedManager(dexAsset, nodeAddr, reader)
authorized, ready := m.authorizeChainActivation(dChainID)
require.True(t, ready, "X-Chain answered => decided, not deferred")
require.False(t, authorized, "an NFT held at another address must NOT authorize this node")
})
t.Run("dex-operator NFT held at the node's OWN address => authorized", func(t *testing.T) {
// Same collection, but the NFT (alongside the stranger's) is also held by
// nodeAddr. Only the node's own holding authorizes it.
reader := &ownerScopedUTXOReader{utxos: []*lux.UTXO{
nftUTXO(dexAsset, 0, strangerAddr), // not ours: invisible to our query
nftUTXO(dexAsset, 0, nodeAddr), // ours: authorizes
}}
m, dChainID := dexGatedManager(dexAsset, nodeAddr, reader)
authorized, ready := m.authorizeChainActivation(dChainID)
require.True(t, ready)
require.True(t, authorized, "an NFT held at the node's own staking X-address authorizes it")
})
t.Run("only the stranger's NFT exists, queried by stranger => authorized for stranger only", func(t *testing.T) {
// Sanity: the same reader DOES authorize the address that actually holds
// the NFT, proving the scoping is by-address and not a blanket deny.
reader := &ownerScopedUTXOReader{utxos: []*lux.UTXO{nftUTXO(dexAsset, 0, strangerAddr)}}
m, dChainID := dexGatedManager(dexAsset, strangerAddr, reader)
authorized, ready := m.authorizeChainActivation(dChainID)
require.True(t, ready)
require.True(t, authorized, "the holder address is authorized; scoping is per-address, not blanket")
})
}
// TestDexValidatorFlagGatesDChain (M1). The dex-validator opt-in is a genuine,
// NECESSARY activation condition for the D-Chain — not just a log line — and it
// composes AND-wise with the NFT gate. This proves the full truth table at the
// activation authority (authorizeChainActivation), which is downstream of how a
// chain gets QUEUED: --track-all-chains queues the D-Chain in platformvm
// (CreateChain), but the manager's gate is what ACTIVATES it. So a manager with
// DexValidator=false standing in for a --track-all-chains node MUST decline the
// D-Chain regardless of NFT state.
func TestDexValidatorFlagGatesDChain(t *testing.T) {
xChainID := ids.GenerateTestID()
dChainID := ids.GenerateTestID()
dexAsset := ids.GenerateTestID()
staking := ids.GenerateTestShortID()
// withFlag builds a D-Chain manager with the dex-validator flag set to
// [dexValidator]. When [gated] the D-Chain is NFT-gated on dexAsset and the
// reader is the bootstrapped X-Chain; when not gated, ChainAuthorizations is
// empty (the --track-all-chains, no-operator-collection case).
withFlag := func(dexValidator, gated bool, reader xChainUTXOReader) *manager {
var authz map[ids.ID]NFTAuthorization
if gated {
authz = map[ids.ID]NFTAuthorization{dChainID: {AssetID: dexAsset, GroupID: 0}}
}
m := newGateManager(xChainID, staking, authz, nil, reader)
m.DChainID = dChainID
m.DexValidator = dexValidator
return m
}
holdsNFT := func() xChainUTXOReader {
return &fakeUTXOReader{utxos: []*lux.UTXO{nftUTXO(dexAsset, 0, staking)}}
}
t.Run("flag=false + ungated (track-all-chains) => D NOT activated", func(t *testing.T) {
// The headline M1 case: even with no operator collection configured (so
// the chain would otherwise be ungated and activate under --track-all),
// dex-validator=false declines the D-Chain. Decided opt-out, not defer.
m := withFlag(false, false, nil)
authorized, ready := m.authorizeChainActivation(dChainID)
require.True(t, ready, "decided opt-out, not a deferral")
require.False(t, authorized, "dex-validator=false must block the D-Chain even under --track-all-chains")
})
t.Run("flag=false + NFT held => still NOT activated (flag is necessary)", func(t *testing.T) {
// Holding the dex-operator NFT does not rescue activation when the flag
// is off: the flag is an independent necessary condition (the AND).
m := withFlag(false, true, holdsNFT())
authorized, ready := m.authorizeChainActivation(dChainID)
require.True(t, ready)
require.False(t, authorized, "flag=false blocks D even with the NFT held")
})
t.Run("flag=true + NFT held => activated", func(t *testing.T) {
// Both conditions met: opted in AND holds the NFT.
m := withFlag(true, true, holdsNFT())
authorized, ready := m.authorizeChainActivation(dChainID)
require.True(t, ready)
require.True(t, authorized, "dex-validator=true AND NFT held => D-Chain activates")
})
t.Run("flag=true + gated but NFT NOT held => NOT activated (NFT is necessary)", func(t *testing.T) {
// The other half of the AND: opting in does not bypass the NFT gate.
m := withFlag(true, true, &fakeUTXOReader{utxos: nil})
authorized, ready := m.authorizeChainActivation(dChainID)
require.True(t, ready)
require.False(t, authorized, "flag=true but no NFT => still blocked")
})
t.Run("flag=true + ungated (no operator collection) => activated", func(t *testing.T) {
// Opted in and no NFT requirement configured for this network: the flag
// alone governs and the D-Chain activates (preserves today's opt-in
// behavior for the in-flag operator).
m := withFlag(true, false, nil)
authorized, ready := m.authorizeChainActivation(dChainID)
require.True(t, ready)
require.True(t, authorized, "dex-validator=true with no NFT configured => activates")
})
t.Run("flag=false does not affect a non-D gated chain", func(t *testing.T) {
// The flag is D-specific: another gated chain (e.g. bridgevm) is governed
// solely by its NFT gate, untouched by dex-validator.
otherChain := ids.GenerateTestID()
authz := map[ids.ID]NFTAuthorization{otherChain: {AssetID: dexAsset, GroupID: 0}}
m := newGateManager(xChainID, staking, authz, nil, holdsNFT())
m.DChainID = dChainID
m.DexValidator = false // off, but irrelevant to otherChain
authorized, ready := m.authorizeChainActivation(otherChain)
require.True(t, ready)
require.True(t, authorized, "dex-validator must not gate a non-D chain")
})
}
+3 -3
View File
@@ -10,13 +10,13 @@ import (
"github.com/stretchr/testify/require"
"github.com/luxfi/vm"
"github.com/luxfi/constants"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/metric"
"github.com/luxfi/node/nets"
"github.com/luxfi/node/vms"
"github.com/luxfi/vm"
)
// TestNew tests creating a new manager
@@ -106,9 +106,9 @@ func TestQueueChainCreation(t *testing.T) {
chainID := ids.GenerateTestID()
netID := ids.GenerateTestID()
chainParams := ChainParameters{
ID: chainID,
ID: chainID,
ChainID: netID,
VMID: ids.GenerateTestID(),
VMID: ids.GenerateTestID(),
}
// Queue the chain
+591
View File
@@ -0,0 +1,591 @@
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// pchain_height_finality_test.go — the b2 load-bearing proof the prior round
// lacked: that the node delivers the REAL P-chain epoch height to the chain
// engine, so a K>1 quorum chain finalizes against the LIVE validator set — not
// the frozen genesis set.
//
// The consensus-layer TestPChainEpochFinality_RealWiring proved the engine reads
// the RIGHT height GIVEN a block that already exposes one; it fed a synthetic
// block carrying PChainHeight directly, BYPASSING the boundary. The boundary —
// where a bare plugin block yields pChainHeightOf==0 — was exactly what shipped
// broken (set@0 = genesis). These tests drive the REAL boundary:
//
// inner VM (bare block, no PChainHeight)
// └─ pChainHeightVM (the b2 wrapper, backed by a real validators.State)
// └─ consensus engine (real α-of-K cert finality, node BLS sources)
//
// and prove three properties end to end:
//
// (1) pChainHeightOf(realBlock) returns the wrapper's stamped P-chain height
// (NOT 0) — at BuildBlock AND after a ParseBlock round-trip of the gossiped
// bytes (the determinism guarantee: every node recovers the same height).
// (2) K>1 FINALIZES at genesis (set@H0).
// (3) K>1 FINALIZES AFTER a staking change — validators that JOINED post-genesis
// cast the deciding votes+stake. This is the case that STALLS on the set@0
// path (the joiners are absent from the genesis set), so finalizing proves
// the real height is load-bearing.
//
// CGO-free: the node BLS sources use the pure-Go BLS path under CGO_ENABLED=0, so
// this runs the ACTUAL production quorum sources (blsVoteVerifier / blsVoteSigner
// / validatorStakeSource / validatorSetRootSource) — not an ed25519 stand-in.
package chains
import (
"context"
"sync"
"testing"
"time"
consensusconfig "github.com/luxfi/consensus/config"
consensuschain "github.com/luxfi/consensus/engine/chain"
"github.com/luxfi/consensus/engine/chain/block"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/ids"
validators "github.com/luxfi/validators"
"github.com/luxfi/validators/validatorstest"
)
// --- a bare inner VM whose blocks carry NO P-chain height --------------------
// fakeInnerBlock is a minimal chain block: it satisfies block.Block but does NOT
// expose PChainHeight() — exactly like the plugin VM blocks (C-Chain EVM, dexvm)
// the node runs. Its Bytes() is its own opaque encoding; the wrapper frames a
// P-chain height AROUND these bytes for transport.
type fakeInnerBlock struct {
id ids.ID
parentID ids.ID
height uint64
bytes []byte
timestamp time.Time
mu sync.Mutex
acceptCalled int
}
func (b *fakeInnerBlock) ID() ids.ID { return b.id }
func (b *fakeInnerBlock) Parent() ids.ID { return b.parentID }
func (b *fakeInnerBlock) ParentID() ids.ID { return b.parentID }
func (b *fakeInnerBlock) Height() uint64 { return b.height }
func (b *fakeInnerBlock) Timestamp() time.Time { return b.timestamp }
func (b *fakeInnerBlock) Status() uint8 { return 0 }
func (b *fakeInnerBlock) Bytes() []byte { return b.bytes }
func (b *fakeInnerBlock) Verify(context.Context) error { return nil }
func (b *fakeInnerBlock) Reject(context.Context) error { return nil }
func (b *fakeInnerBlock) Accept(context.Context) error {
b.mu.Lock()
b.acceptCalled++
b.mu.Unlock()
return nil
}
func (b *fakeInnerBlock) accepted() int {
b.mu.Lock()
defer b.mu.Unlock()
return b.acceptCalled
}
// fakeInnerVM is a BlockBuilder over fakeInnerBlocks, keyed by id and by bytes so
// ParseBlock(bytes) reconstructs the SAME inner block on a follower. It builds one
// block on demand (set via stage) so the test controls the proposed block.
type fakeInnerVM struct {
mu sync.Mutex
byID map[ids.ID]*fakeInnerBlock
byBytes map[string]*fakeInnerBlock
staged *fakeInnerBlock // returned by the next BuildBlock
lastAcc ids.ID
}
func newFakeInnerVM() *fakeInnerVM {
return &fakeInnerVM{
byID: make(map[ids.ID]*fakeInnerBlock),
byBytes: make(map[string]*fakeInnerBlock),
}
}
func (vm *fakeInnerVM) register(b *fakeInnerBlock) {
vm.mu.Lock()
vm.byID[b.id] = b
vm.byBytes[string(b.bytes)] = b
vm.mu.Unlock()
}
// stage sets the block the next BuildBlock returns (and registers it for Get/Parse).
func (vm *fakeInnerVM) stage(b *fakeInnerBlock) {
vm.register(b)
vm.mu.Lock()
vm.staged = b
vm.mu.Unlock()
}
func (vm *fakeInnerVM) BuildBlock(context.Context) (block.Block, error) {
vm.mu.Lock()
defer vm.mu.Unlock()
return vm.staged, nil
}
func (vm *fakeInnerVM) ParseBlock(_ context.Context, b []byte) (block.Block, error) {
vm.mu.Lock()
defer vm.mu.Unlock()
if blk, ok := vm.byBytes[string(b)]; ok {
return blk, nil
}
// Unknown bytes: synthesize a block so a follower can still parse. Real VMs
// decode deterministically; the test pre-registers every block it gossips, so
// this path is only a safety net.
return nil, errUnknownInnerBytes
}
func (vm *fakeInnerVM) GetBlock(_ context.Context, id ids.ID) (block.Block, error) {
vm.mu.Lock()
defer vm.mu.Unlock()
if blk, ok := vm.byID[id]; ok {
return blk, nil
}
return nil, errUnknownInnerBytes
}
func (vm *fakeInnerVM) LastAccepted(context.Context) (ids.ID, error) {
vm.mu.Lock()
defer vm.mu.Unlock()
return vm.lastAcc, nil
}
func (vm *fakeInnerVM) SetPreference(_ context.Context, id ids.ID) error {
vm.mu.Lock()
vm.lastAcc = id
vm.mu.Unlock()
return nil
}
var errUnknownInnerBytes = errInnerBytes{}
type errInnerBytes struct{}
func (errInnerBytes) Error() string { return "fakeInnerVM: unknown block bytes" }
// --- BLS validator material (pure-Go BLS under CGO_ENABLED=0) ----------------
type blsValidator struct {
nodeID ids.NodeID
sk *bls.SecretKey
pkComp []byte
light uint64
}
func newBLSValidator(t *testing.T, weight uint64) blsValidator {
t.Helper()
sk, err := bls.NewSecretKey()
if err != nil {
t.Fatalf("bls.NewSecretKey: %v", err)
}
return blsValidator{
nodeID: ids.GenerateTestNodeID(),
sk: sk,
pkComp: bls.PublicKeyToCompressedBytes(sk.PublicKey()),
light: weight,
}
}
func (v blsValidator) out() *validators.GetValidatorOutput {
return &validators.GetValidatorOutput{
NodeID: v.nodeID,
PublicKey: v.pkComp,
Light: v.light,
Weight: v.light,
}
}
// stateByHeight builds a height-indexed validators.State reporting the given sets
// per height (empty for unknown heights / wrong net), with GetCurrentHeight fixed
// to `current` so the wrapper stamps that height onto built blocks.
func stateByHeight(netID ids.ID, current uint64, byHeight map[uint64][]blsValidator) *validatorstest.TestState {
s := validatorstest.NewTestState()
s.GetCurrentHeightF = func(context.Context) (uint64, error) { return current, nil }
s.GetValidatorSetF = func(_ context.Context, height uint64, gotNet ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
if gotNet != netID {
return map[ids.NodeID]*validators.GetValidatorOutput{}, nil
}
out := make(map[ids.NodeID]*validators.GetValidatorOutput)
for _, v := range byHeight[height] {
out[v.nodeID] = v.out()
}
return out, nil
}
return s
}
// --- a recording cert gossiper (engine CertGossiper) -------------------------
type recordingCertGossiper struct {
mu sync.Mutex
certs [][]byte
}
func (g *recordingCertGossiper) GossipCert(_ ids.ID, _ ids.ID, certBytes []byte) error {
g.mu.Lock()
g.certs = append(g.certs, append([]byte(nil), certBytes...))
g.mu.Unlock()
return nil
}
func (g *recordingCertGossiper) count() int {
g.mu.Lock()
defer g.mu.Unlock()
return len(g.certs)
}
var _ consensuschain.CertGossiper = (*recordingCertGossiper)(nil)
// --- helpers -----------------------------------------------------------------
func params5() consensusconfig.Parameters {
return consensusconfig.Parameters{K: 5, AlphaPreference: 3, AlphaConfidence: 3, Beta: 2}
}
func waitForCond(d time.Duration, cond func() bool) bool {
deadline := time.Now().Add(d)
for time.Now().Before(deadline) {
if cond() {
return true
}
time.Sleep(5 * time.Millisecond)
}
return cond()
}
// signBLSVote produces validator v's signed accept Vote for a position (the same
// canonical message the node's blsVoteSigner/Verifier use).
func signBLSVote(t *testing.T, v blsValidator, pos consensuschain.VotePosition) consensuschain.Vote {
t.Helper()
sig, err := v.sk.Sign(consensuschain.CanonicalVoteMessage(pos))
if err != nil {
t.Fatalf("sign vote: %v", err)
}
return consensuschain.Vote{
BlockID: pos.BlockID,
NodeID: v.nodeID,
Accept: true,
SignedAt: time.Now(),
Signature: bls.SignatureToBytes(sig),
ParentID: pos.ParentID,
Round: pos.Round,
}
}
// quorumEngineFixture wires a real consensus engine with the node's production
// BLS quorum sources, all height-pinned to a height-indexed validators.State,
// driving blocks through the b2 pChainHeightVM wrapper over a bare inner VM.
type quorumEngineFixture struct {
engine *consensuschain.Transitive
wrapper *pChainHeightVM
inner *fakeInnerVM
chainID ids.ID
netID ids.ID
proposer blsValidator
certs *recordingCertGossiper
}
func newQuorumEngineFixture(t *testing.T, netID ids.ID, state validators.State, proposer blsValidator, byHeight map[uint64][]blsValidator) *quorumEngineFixture {
t.Helper()
inner := newFakeInnerVM()
wrapper := newPChainHeightVM(inner, state, netID)
chainID := ids.GenerateTestID()
certs := &recordingCertGossiper{}
vdrState := state
engine := consensuschain.NewWithConfig(
consensuschain.Config{Params: params5(), VM: wrapper},
consensuschain.WithQuorumCert(chainID, proposer.nodeID, newBLSVoteVerifier(vdrState, netID), certs, newBLSVoteSigner(proposer.sk)),
consensuschain.WithStakeWeighting(newValidatorStakeSource(vdrState, netID)),
consensuschain.WithValidatorSetRoot(newValidatorSetRootSource(vdrState, netID)),
)
if err := engine.Start(context.Background(), true); err != nil {
t.Fatalf("engine.Start: %v", err)
}
t.Cleanup(func() { _ = engine.Stop(context.Background()) })
return &quorumEngineFixture{
engine: engine,
wrapper: wrapper,
inner: inner,
chainID: chainID,
netID: netID,
proposer: proposer,
certs: certs,
}
}
// proposeViaWrapper builds the staged inner block THROUGH the wrapper (stamping
// the live P-chain height), tracks it as the engine's own verified proposal with
// the wrapper-delivered epoch, records the proposer's self-vote, and returns the
// wrapped block + the canonical vote position followers must sign.
func (f *quorumEngineFixture) proposeViaWrapper(t *testing.T, inner *fakeInnerBlock) (block.Block, consensuschain.VotePosition) {
t.Helper()
f.inner.stage(inner)
wrapped, err := f.wrapper.BuildBlock(context.Background())
if err != nil {
t.Fatalf("wrapper.BuildBlock: %v", err)
}
pos := f.engine.TrackOwnProposalForTest(context.Background(), wrapped, 0)
return wrapped, pos
}
// newFakeInner is a small constructor for an inner block at value height h with a
// unique opaque encoding (tag-derived, so byBytes keys never collide).
func newFakeInner(h uint64, parent ids.ID, tag string) *fakeInnerBlock {
return &fakeInnerBlock{
id: ids.GenerateTestID(),
parentID: parent,
height: h,
bytes: []byte("inner:" + tag),
timestamp: time.Now(),
}
}
// --- (1) the boundary delivers the REAL height (not 0), build + parse ---------
// TestPChainHeightVM_DeliversRealHeight is the direct b2 boundary proof: the
// wrapper stamps the proposer's live P-chain height onto the block the engine
// sees, so pChainHeightOf(realBlock) returns that height — NOT 0 — at BuildBlock,
// AND a follower recovers the IDENTICAL height by parsing the gossiped bytes (the
// determinism guarantee H rides the bytes, never recomputed from a skewing view).
//
// This is the assertion the whole fix turns on. On the broken path
// pChainHeightOf(any real plugin block)==0 → set@0 (genesis) forever.
func TestPChainHeightVM_DeliversRealHeight(t *testing.T) {
const epoch = uint64(7) // the live P-chain height the proposer stamps
netID := ids.GenerateTestID()
v := newBLSValidator(t, 20)
// A state whose GetCurrentHeight is 7 (so BuildBlock stamps 7); the set content
// is irrelevant to the stamping itself, but we register it at 7 for symmetry.
state := stateByHeight(netID, epoch, map[uint64][]blsValidator{epoch: {v}})
wrapper := newPChainHeightVM(newFakeInnerVM(), state, netID)
inner := newFakeInner(10_000_000, ids.Empty, "real-height") // value height races ahead
wrapper.inner.(*fakeInnerVM).stage(inner)
wrapped, err := wrapper.BuildBlock(context.Background())
if err != nil {
t.Fatalf("BuildBlock: %v", err)
}
// (1a) the engine's boundary read on the BUILT block returns the stamped height.
if got := consensuschain.PChainHeightOfForTest(wrapped); got != epoch {
t.Fatalf("pChainHeightOf(built block) = %d, want %d — the boundary still delivers the wrong height "+
"(0 would mean the genesis-set freeze the b2 fix removes)", got, epoch)
}
// Sanity: the inner block itself exposes no PChainHeight, so without the wrapper
// the engine would read 0. This pins WHY the wrapper is load-bearing.
if got := consensuschain.PChainHeightOfForTest(inner); got != 0 {
t.Fatalf("bare inner block must expose no P-chain height (pChainHeightOf=0), got %d", got)
}
// (1b) determinism: a follower parsing the GOSSIPED bytes recovers the same H.
parsed, err := wrapper.ParseBlock(context.Background(), wrapped.Bytes())
if err != nil {
t.Fatalf("ParseBlock(gossiped bytes): %v", err)
}
if got := consensuschain.PChainHeightOfForTest(parsed); got != epoch {
t.Fatalf("pChainHeightOf(parsed block) = %d, want %d — a follower recomputed/lost the height; "+
"finality would stall on a dynamic set (worse than the genesis fallback)", got, epoch)
}
if parsed.ID() != wrapped.ID() {
t.Fatalf("parsed block ID %s != built block ID %s — the inner identity must be preserved across the envelope", parsed.ID(), wrapped.ID())
}
}
// --- (2) K>1 finalizes at genesis (set@H0) -----------------------------------
// TestPChainHeightVM_FinalizesAtGenesis proves the fix UNBRICKS finality in the
// base case: a K>1 quorum chain whose validator set is the genesis set (current
// P-chain height 0) finalizes through the real BLS quorum sources. This is the
// safe floor the genesis fallback guarantees — even here the height is delivered
// honestly (0), and the set@0 is non-empty so a ⅔ quorum finalizes.
func TestPChainHeightVM_FinalizesAtGenesis(t *testing.T) {
const genesis = uint64(0)
netID := constantsPrimaryNetworkID()
g := make([]blsValidator, 5)
for i := range g {
g[i] = newBLSValidator(t, 20) // equal stake, total 100
}
state := stateByHeight(netID, genesis, map[uint64][]blsValidator{genesis: g})
f := newQuorumEngineFixture(t, netID, state, g[0], map[uint64][]blsValidator{genesis: g})
inner := newFakeInner(42, ids.Empty, "genesis-finalize") // value height advanced past genesis
wrapped, pos := f.proposeViaWrapper(t, inner)
// The block was stamped at the genesis height (0); the position binds set@0.
if got := consensuschain.PChainHeightOfForTest(wrapped); got != genesis {
t.Fatalf("expected genesis stamp %d, got %d", genesis, got)
}
if pos.ValidatorSetRoot == ids.Empty {
t.Fatal("genesis set-root must be non-Empty (the genesis set is non-empty)")
}
// proposer g[0] self-voted; drive 3 more signed accepts → 4/5 = 80/100 stake > ⅔.
f.engine.ReceiveVote(signBLSVote(t, g[1], pos))
f.engine.ReceiveVote(signBLSVote(t, g[2], pos))
f.engine.ReceiveVote(signBLSVote(t, g[3], pos))
if !waitForCond(2*time.Second, func() bool { return inner.accepted() == 1 }) {
t.Fatalf("UNBRICK: a K>1 block must finalize against the genesis set (VM.Accept=%d)", inner.accepted())
}
if f.certs.count() == 0 {
t.Fatal("a verified quorum cert must be assembled + gossiped at finality")
}
}
// --- (3) K>1 finalizes AFTER a staking change (THE b2 proof) ------------------
// TestPChainHeightVM_FinalizesAfterStakingChange is the load-bearing b2 proof:
// validators that JOINED after genesis cast the deciding votes + stake, and the
// block finalizes — which is IMPOSSIBLE on the broken set@0 path, where the
// joiners are absent from the genesis set so their votes are dropped and their
// stake is uncounted.
//
// The set@epoch (height 7) holds {g0, j1, j2, j3, j4}: only g0 overlaps genesis;
// j1..j4 JOINED at 7. The genesis set@0 holds five DIFFERENT validators with only
// g0 in common. A 4-voter cert {g0, j1, j2, j3} = 80/100 stake-at-7 > ⅔.
//
// - With the b2 fix: the block is stamped 7, the verifier resolves j1..j3 at
// set@7 (present) and the ⅔ tally is measured at 7 → the cert verifies →
// FINALIZES.
// - On the broken set@0 path: j1..j3 are unknown at height 0 → their signed
// votes are dropped → only g0 (20/100) verifies < ⅔ → STALLS FOREVER.
//
// The test asserts BOTH directly: (a) the block finalizes, and (b) the production
// verifier itself rejects a joiner's vote at height 0 but accepts it at height 7 —
// pinning the exact mechanism that would stall the frozen-set path.
func TestPChainHeightVM_FinalizesAfterStakingChange(t *testing.T) {
const (
genesis = uint64(0)
epoch = uint64(7) // staking change landed here; current P-chain height = 7
)
netID := constantsPrimaryNetworkID()
// g0 is a validator present at BOTH epochs (so the fixture proposer key resolves
// at the stamped epoch). j1..j4 JOINED at epoch 7. gen1..gen4 are the OTHER four
// genesis validators (present only at 0) — they make set@0 a genuinely different
// set so "the joiners decide" is unambiguous.
g0 := newBLSValidator(t, 20)
j1 := newBLSValidator(t, 20)
j2 := newBLSValidator(t, 20)
j3 := newBLSValidator(t, 20)
j4 := newBLSValidator(t, 20)
gen1 := newBLSValidator(t, 20)
gen2 := newBLSValidator(t, 20)
gen3 := newBLSValidator(t, 20)
gen4 := newBLSValidator(t, 20)
genesisSet := []blsValidator{g0, gen1, gen2, gen3, gen4} // total 100 at height 0
epochSet := []blsValidator{g0, j1, j2, j3, j4} // total 100 at height 7
state := stateByHeight(netID, epoch, map[uint64][]blsValidator{
genesis: genesisSet,
epoch: epochSet,
})
// (b) PIN THE MECHANISM that makes the frozen path stall: the production verifier
// rejects a joiner at height 0 (frozen/genesis) and accepts it at height 7.
verifier := newBLSVoteVerifier(state, netID)
// Build a position to sign (any height-7-bound position works for this probe).
probeBlk := newFakeInner(123, ids.Empty, "probe")
probeWrapper := newPChainHeightVM(newFakeInnerVM(), state, netID)
probeWrapper.inner.(*fakeInnerVM).stage(probeBlk)
probeWrapped, _ := probeWrapper.BuildBlock(context.Background())
probePos := consensuschain.VotePosition{
ChainID: ids.GenerateTestID(),
Height: probeWrapped.Height(),
BlockID: probeWrapped.ID(),
ParentID: ids.Empty,
ValidatorSetRoot: newValidatorSetRootSource(state, netID).ValidatorSetRoot(epoch),
}
probeMsg := consensuschain.CanonicalVoteMessage(probePos)
j1Sig, err := j1.sk.Sign(probeMsg)
if err != nil {
t.Fatalf("sign probe: %v", err)
}
j1SigBytes := bls.SignatureToBytes(j1Sig)
if verifier.VerifyVote(j1.nodeID, probeMsg, j1SigBytes, genesis) {
t.Fatal("frozen-path mechanism broken: a post-genesis joiner must NOT verify at height 0 " +
"(if it does, the genesis-set path would not actually stall and the test is vacuous)")
}
if !verifier.VerifyVote(j1.nodeID, probeMsg, j1SigBytes, epoch) {
t.Fatal("a joiner present at the epoch MUST verify at the epoch height — the b2 read is broken")
}
// (a) THE END-TO-END FINALIZATION: drive the real engine with the joiners.
f := newQuorumEngineFixture(t, netID, state, g0, map[uint64][]blsValidator{
genesis: genesisSet,
epoch: epochSet,
})
inner := newFakeInner(10_000_000, ids.Empty, "post-staking-change") // value height far ahead
wrapped, pos := f.proposeViaWrapper(t, inner)
// The block carries the LIVE epoch height (7), and the position binds set@7.
if got := consensuschain.PChainHeightOfForTest(wrapped); got != epoch {
t.Fatalf("block must carry the live epoch height %d, got %d", epoch, got)
}
if pos.ValidatorSetRoot != newValidatorSetRootSource(state, netID).ValidatorSetRoot(epoch) {
t.Fatal("position must bind the set-root at the epoch height (set@7), not another height")
}
if pos.ValidatorSetRoot == newValidatorSetRootSource(state, netID).ValidatorSetRoot(genesis) {
t.Fatal("test vacuous: set@7 root must differ from set@0 root (the sets must genuinely differ)")
}
// proposer g0 self-voted; the JOINERS j1,j2,j3 cast the deciding votes.
// 4 distinct accepts {g0,j1,j2,j3} = 80/100 stake-at-7 > ⅔ → MUST finalize.
f.engine.ReceiveVote(signBLSVote(t, j1, pos))
f.engine.ReceiveVote(signBLSVote(t, j2, pos))
f.engine.ReceiveVote(signBLSVote(t, j3, pos))
if !waitForCond(3*time.Second, func() bool { return inner.accepted() == 1 }) {
t.Fatalf("b2: a block whose ⅔ quorum is post-genesis JOINERS must finalize against the LIVE set@%d "+
"(VM.Accept=%d). On the broken set@0 path the joiners are unknown → votes dropped → permanent stall.",
epoch, inner.accepted())
}
// The gossiped cert must verify stake-weighted AT THE EPOCH, and must FAIL at
// genesis (where the joiners are absent) — proving the height is load-bearing.
if f.certs.count() == 0 {
t.Fatal("a verified quorum cert must be assembled + gossiped at finality")
}
f.certs.mu.Lock()
lastCert := f.certs.certs[len(f.certs.certs)-1]
f.certs.mu.Unlock()
cert, err := consensuschain.UnmarshalQuorumCert(lastCert)
if err != nil {
t.Fatalf("decode gossiped cert: %v", err)
}
stake := newValidatorStakeSource(state, netID)
if err := cert.VerifyWeighted(verifier, stake, epoch); err != nil {
t.Fatalf("cert must verify stake-weighted at the epoch height %d: %v", epoch, err)
}
if err := cert.VerifyWeighted(verifier, stake, genesis); err == nil {
t.Fatal("b2: cert must NOT verify at the genesis height (joiners absent / below ⅔ there) — " +
"if it does, the epoch height is not actually being used")
}
// The cert must contain at least one joiner — proving a post-genesis validator's
// vote was counted (the exact case the frozen-set path drops).
var hasJoiner bool
for i := range cert.Votes {
if cert.Votes[i].NodeID == j1.nodeID || cert.Votes[i].NodeID == j2.nodeID || cert.Votes[i].NodeID == j3.nodeID {
hasJoiner = true
break
}
}
if !hasJoiner {
t.Fatal("the finality cert must include a post-genesis joiner's vote (it was the deciding quorum)")
}
}
// constantsPrimaryNetworkID returns the primary-network ID the node uses for
// native-chain validator lookups (ids.Empty). Declared here so the test reads the
// SAME net the production wiring resolves native chains against, without importing
// the constants package for one value.
func constantsPrimaryNetworkID() ids.ID { return ids.Empty }
+327
View File
@@ -0,0 +1,327 @@
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// pchain_height_hardening_test.go — the receive-side hardening proofs for the b2
// transport wrapper (Red round): the three node-layer fixes that close the
// receive-side gaps the build-side stamping left open.
//
// (HIGH-1, predicate b — RECENCY) ParseBlock rejects a wrapped block whose
// stamped P-chain epoch exceeds this node's live P-chain height by more than the
// recency slack (an absurd-future epoch). Honest forward skew within the slack —
// including a legitimate P-chain advance during a staking change — still parses.
//
// (MEDIUM-3 — MAP DoS) the heights map is bounded: it plateaus at the finalized
// watermark under mass parse+accept, and a still-pending block's epoch is never
// evicted by the watermark prune. A sustained unverified-parse flood is capped.
//
// (LOW-2 — FRAME DISCRIMINATOR) a raw inner block whose first bytes coincidentally
// equal the 4-byte frame magic is NOT mis-parsed as framed: the inner re-parse of
// the framed payload fails, so ParseBlock falls back to a raw whole-block parse.
package chains
import (
"context"
"testing"
"github.com/luxfi/ids"
)
// --- (HIGH-1 b) RECENCY: reject absurd-future epoch ---------------------------
// TestParseBlock_RejectsFarFutureEpoch is the receive-side upper-bound proof. A
// node whose live P-chain height is 100 parses a wrapped block stamped at epoch
// 10_000 (far beyond 100 + slack). ParseBlock REJECTS it fail-closed — the block
// is dropped, never returned to be tracked. This stops a Byzantine proposer from
// pinning a block to an epoch the network has not reached.
func TestParseBlock_RejectsFarFutureEpoch(t *testing.T) {
const localH = uint64(100)
netID := ids.GenerateTestID()
v := newBLSValidator(t, 20)
state := stateByHeight(netID, localH, map[uint64][]blsValidator{localH: {v}})
wrapper := newPChainHeightVM(newFakeInnerVM(), state, netID)
// Craft a frame stamped at an absurd-future epoch, with a registered inner block
// so the ONLY reason to reject is the recency bound (not an inner parse failure).
inner := newFakeInner(5_000_000, ids.Empty, "far-future-epoch")
wrapper.inner.(*fakeInnerVM).register(inner)
framed := wrapPChainHeight(inner.Bytes(), localH+pChainHeightRecencySlack+1) // just past the bound
if _, err := wrapper.ParseBlock(context.Background(), framed); err != errPChainHeightNotRecent {
t.Fatalf("ParseBlock(far-future epoch) err = %v, want errPChainHeightNotRecent — an absurd-future "+
"P-chain epoch must be rejected fail-closed so a follower never tracks a block at an unreachable epoch", err)
}
}
// TestParseBlock_AcceptsEpochWithinSlack proves the recency gate admits HONEST
// forward skew: a follower at local P-chain height 100 parses a block stamped at
// 100 + slack (the boundary — the largest legitimate skew, e.g. a proposer that
// saw P-chain blocks this follower has not yet synced during a staking change).
// The block parses and carries its real epoch.
func TestParseBlock_AcceptsEpochWithinSlack(t *testing.T) {
const localH = uint64(100)
netID := ids.GenerateTestID()
v := newBLSValidator(t, 20)
state := stateByHeight(netID, localH, map[uint64][]blsValidator{localH: {v}})
wrapper := newPChainHeightVM(newFakeInnerVM(), state, netID)
inner := newFakeInner(5_000_000, ids.Empty, "within-slack-epoch")
wrapper.inner.(*fakeInnerVM).register(inner)
atBound := localH + pChainHeightRecencySlack // exactly at the bound — must be admitted
framed := wrapPChainHeight(inner.Bytes(), atBound)
parsed, err := wrapper.ParseBlock(context.Background(), framed)
if err != nil {
t.Fatalf("ParseBlock(epoch within slack) err = %v, want nil — honest forward skew up to the slack "+
"(a real P-chain advance during a staking change) must still parse", err)
}
if got := parsed.(*pChainHeightBlock).PChainHeight(); got != atBound {
t.Fatalf("parsed epoch = %d, want %d — the wrapper must carry the real (recent) epoch unchanged", got, atBound)
}
}
// TestParseBlock_RecencyDisabledWithoutState proves the fail-soft: a wrapper with
// no P-chain view (nil state, current height 0) cannot bound recency, so it admits
// the block — the verifier still fails closed on an unresolvable future epoch at
// set-resolution time. (Production installs the wrapper only on K>1 chains the
// manager guards to have a live state, so this is a defensive path, not a mode.)
func TestParseBlock_RecencyDisabledWithoutState(t *testing.T) {
wrapper := newPChainHeightVM(newFakeInnerVM(), nil, ids.GenerateTestID())
inner := newFakeInner(7, ids.Empty, "no-state-epoch")
wrapper.inner.(*fakeInnerVM).register(inner)
framed := wrapPChainHeight(inner.Bytes(), 9_999_999) // would be far-future if state existed
if _, err := wrapper.ParseBlock(context.Background(), framed); err != nil {
t.Fatalf("ParseBlock with nil state must admit (recency unenforceable, verifier fails closed downstream): %v", err)
}
}
// --- (LOW-2) FRAME DISCRIMINATOR: a magic-colliding raw block is NOT mis-framed -
// TestParseBlock_MagicCollisionParsesRaw is the discriminator proof. A raw inner
// block whose Bytes() coincidentally BEGIN with the 4-byte frame magic (the
// 2^-32 collision the raw-hash-prefix VMs hit) must parse as a RAW block, not be
// mis-classified as framed (which used to fail the inner decode → bootstrap stall).
// The inner re-parse of the framed payload fails (the payload is the block minus
// its first 12 bytes — not a valid inner block), so ParseBlock falls back to a raw
// whole-block parse.
func TestParseBlock_MagicCollisionParsesRaw(t *testing.T) {
netID := ids.GenerateTestID()
v := newBLSValidator(t, 20)
state := stateByHeight(netID, 50, map[uint64][]blsValidator{50: {v}})
inner := newFakeInnerVM()
wrapper := newPChainHeightVM(inner, state, netID)
// A raw block whose bytes START with the exact magic prefix, then arbitrary
// payload. Length is >= the header width so the prefix check would treat it as a
// candidate frame. Crucially, bytes[12:] is NOT a registered inner block, so the
// framed-payload re-parse fails and the whole-bytes parse must be used.
raw := &fakeInnerBlock{
id: ids.GenerateTestID(),
parentID: ids.Empty,
height: 1234,
bytes: append(append([]byte{}, pChainHeightMagic[:]...),
[]byte("RAW-BLOCK-COLLIDES-WITH-MAGIC-PREFIX-payload")...),
}
inner.register(raw) // registers byBytes[whole] = raw; bytes[12:] stays UNregistered
parsed, err := wrapper.ParseBlock(context.Background(), raw.bytes)
if err != nil {
t.Fatalf("ParseBlock(magic-colliding raw block) err = %v, want nil — a raw block whose prefix collides "+
"with the frame magic must parse as RAW (the old behavior mis-framed it → inner decode fail → bootstrap stall)", err)
}
if parsed.ID() != raw.id {
t.Fatalf("parsed block ID %s != raw block ID %s — the colliding block must decode to the SAME raw inner block", parsed.ID(), raw.id)
}
// A raw block falls back to the genesis-set epoch (0): it was NOT framed, so no
// proposer epoch was carried.
if got := parsed.(*pChainHeightBlock).PChainHeight(); got != 0 {
t.Fatalf("magic-colliding raw block epoch = %d, want 0 — it must NOT be read as a framed epoch", got)
}
// Its re-gossip bytes must be the raw passthrough (the inner bytes verbatim), not
// re-framed — so a follower of THIS node also sees it raw.
if string(parsed.Bytes()) != string(raw.bytes) {
t.Fatal("a raw colliding block must re-gossip its inner bytes verbatim (passthrough), not a new frame")
}
}
// TestParseBlock_GenuineFrameStillParses guards the discriminator's other side: a
// GENUINE frame (whose payload IS a valid inner block) still parses as framed and
// recovers the stamped epoch. This pins that the collision fix did not break the
// normal framed path.
func TestParseBlock_GenuineFrameStillParses(t *testing.T) {
netID := ids.GenerateTestID()
v := newBLSValidator(t, 20)
const epoch = uint64(33)
state := stateByHeight(netID, epoch, map[uint64][]blsValidator{epoch: {v}})
inner := newFakeInnerVM()
wrapper := newPChainHeightVM(inner, state, netID)
innerBlk := newFakeInner(9000, ids.Empty, "genuine-frame")
inner.register(innerBlk)
framed := wrapPChainHeight(innerBlk.Bytes(), epoch)
parsed, err := wrapper.ParseBlock(context.Background(), framed)
if err != nil {
t.Fatalf("ParseBlock(genuine frame) err = %v, want nil", err)
}
if parsed.ID() != innerBlk.ID() {
t.Fatalf("genuine frame parsed ID %s != inner ID %s", parsed.ID(), innerBlk.ID())
}
if got := parsed.(*pChainHeightBlock).PChainHeight(); got != epoch {
t.Fatalf("genuine frame epoch = %d, want %d", got, epoch)
}
}
// --- (MEDIUM-3) MAP BOUND: plateau at watermark, pending never evicted --------
// TestHeightsMap_PlateausAtWatermark is the DoS bound proof. We parse a long run
// of distinct framed blocks (each adds a heights entry) and ACCEPT them in order;
// each Accept advances the finalized-height watermark and prunes entries at/below
// it. The map plateaus — it does NOT grow without bound as the chain advances.
func TestHeightsMap_PlateausAtWatermark(t *testing.T) {
netID := ids.GenerateTestID()
v := newBLSValidator(t, 20)
const epoch = uint64(10)
state := stateByHeight(netID, epoch, map[uint64][]blsValidator{epoch: {v}})
inner := newFakeInnerVM()
wrapper := newPChainHeightVM(inner, state, netID)
const n = 500
var lastParsed *pChainHeightBlock
for h := uint64(1); h <= n; h++ {
blk := newFakeInner(h, ids.Empty, "plateau-"+itoa(h))
inner.register(blk)
framed := wrapPChainHeight(blk.Bytes(), epoch)
parsed, err := wrapper.ParseBlock(context.Background(), framed)
if err != nil {
t.Fatalf("ParseBlock #%d: %v", h, err)
}
// Accept the PREVIOUS block (so the most-recent one is still "pending").
if lastParsed != nil {
if err := lastParsed.Accept(context.Background()); err != nil {
t.Fatalf("Accept #%d: %v", h-1, err)
}
}
lastParsed = parsed.(*pChainHeightBlock)
// After each accept the map must be bounded: only entries strictly above the
// watermark survive. We accept (h-1) blocks, so at most a tiny constant remain
// (the just-parsed, not-yet-accepted block, plus any equal-height entries).
wrapper.mu.Lock()
size := len(wrapper.heights)
wm := wrapper.finalizedHeight
wrapper.mu.Unlock()
if size > 4 { // generous constant: the pending working set, not O(n)
t.Fatalf("heights map grew to %d entries at height %d (watermark %d) — it must plateau at the "+
"finalized watermark, not accrete one permanent entry per parsed block (MEDIUM-3 DoS)", size, h, wm)
}
}
}
// TestHeightsMap_PendingNeverEvictedByWatermark proves the watermark prune NEVER
// drops a still-pending block's epoch. We track a pending block at height 1000
// (epoch recalled), accept a LOWER-height block (watermark advances only to that
// lower height), and assert the pending block's epoch is still recallable. A blind
// LRU would have dropped it; the watermark prune must not.
func TestHeightsMap_PendingNeverEvictedByWatermark(t *testing.T) {
netID := ids.GenerateTestID()
v := newBLSValidator(t, 20)
const epoch = uint64(12)
state := stateByHeight(netID, epoch, map[uint64][]blsValidator{epoch: {v}})
inner := newFakeInnerVM()
wrapper := newPChainHeightVM(inner, state, netID)
// Pending block at a HIGH value height (1000).
pending := newFakeInner(1000, ids.Empty, "pending-high")
inner.register(pending)
pendingFrame := wrapPChainHeight(pending.Bytes(), epoch)
if _, err := wrapper.ParseBlock(context.Background(), pendingFrame); err != nil {
t.Fatalf("ParseBlock(pending): %v", err)
}
// A different block at a LOW value height (5) finalizes → watermark = 5.
low := newFakeInner(5, ids.Empty, "finalized-low")
inner.register(low)
lowFrame := wrapPChainHeight(low.Bytes(), epoch)
lowParsed, err := wrapper.ParseBlock(context.Background(), lowFrame)
if err != nil {
t.Fatalf("ParseBlock(low): %v", err)
}
if err := lowParsed.Accept(context.Background()); err != nil {
t.Fatalf("Accept(low): %v", err)
}
// The watermark advanced to 5 (the low block). The PENDING block at height 1000
// is strictly above the watermark, so its epoch MUST survive the prune.
if got, ok := wrapper.recall(pending.ID()); !ok || got != epoch {
t.Fatalf("pending block's epoch recall = (%d,%v), want (%d,true) — the watermark prune (wm=5) must NEVER "+
"evict a still-pending block at height 1000 (that would reset its epoch to 0 = re-freeze)", got, ok, epoch)
}
// And the finalized low block's entry WAS pruned (it is at/below the watermark).
if _, ok := wrapper.recall(low.ID()); ok {
t.Fatal("the finalized low block's entry should have been pruned at/below the watermark (loss-free: epoch already captured)")
}
}
// TestHeightsMap_FloodCappedPreservingNearTip proves the hard-cap backstop: a
// sustained UNVERIFIED-parse flood (blocks that never finalize, so the watermark
// never advances) cannot grow the map past the cap, AND the cap evicts
// highest-value-height-first so the near-tip pending band survives. We seed a
// near-tip pending entry (low height), then flood with far-future-height frames;
// the map stays at the cap and the near-tip entry is retained.
func TestHeightsMap_FloodCappedPreservingNearTip(t *testing.T) {
netID := ids.GenerateTestID()
v := newBLSValidator(t, 20)
const epoch = uint64(8)
// Slack must admit the flood frames' epoch; we stamp them at the current epoch so
// the recency gate is not what bounds them — the CAP is what we are testing.
state := stateByHeight(netID, epoch, map[uint64][]blsValidator{epoch: {v}})
inner := newFakeInnerVM()
wrapper := newPChainHeightVM(inner, state, netID)
// Near-tip pending block at LOW value height 1.
nearTip := newFakeInner(1, ids.Empty, "near-tip")
inner.register(nearTip)
if _, err := wrapper.ParseBlock(context.Background(), wrapPChainHeight(nearTip.Bytes(), epoch)); err != nil {
t.Fatalf("ParseBlock(near-tip): %v", err)
}
// Flood: cap + 200 distinct frames at FAR-future value heights (never accepted).
for i := 0; i < pChainHeightMapCap+200; i++ {
h := uint64(1_000_000 + i) // far above the near-tip height
blk := newFakeInner(h, ids.Empty, "flood-"+itoa(h))
inner.register(blk)
if _, err := wrapper.ParseBlock(context.Background(), wrapPChainHeight(blk.Bytes(), epoch)); err != nil {
t.Fatalf("ParseBlock(flood %d): %v", i, err)
}
}
wrapper.mu.Lock()
size := len(wrapper.heights)
_, nearTipKept := wrapper.heights[nearTip.ID()]
wrapper.mu.Unlock()
if size > pChainHeightMapCap {
t.Fatalf("heights map = %d entries, must be capped at %d under flood (MEDIUM-3 backstop)", size, pChainHeightMapCap)
}
if !nearTipKept {
t.Fatal("the near-tip pending entry (height 1) must SURVIVE the cap — eviction is highest-height-first, " +
"so a flood of far-future-height spam is shed before any near-tip pending block (NOT a blind LRU)")
}
}
// itoa is a tiny, allocation-light uint64→string for unique test tags (avoids
// importing strconv into a hot test loop and keeps byBytes keys distinct).
func itoa(v uint64) string {
if v == 0 {
return "0"
}
var buf [20]byte
i := len(buf)
for v > 0 {
i--
buf[i] = byte('0' + v%10)
v /= 10
}
return string(buf[i:])
}
+480
View File
@@ -0,0 +1,480 @@
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// pchain_height_vm.go — the node-layer boundary that delivers the REAL P-CHAIN
// EPOCH HEIGHT to the consensus chain engine (the b2 wiring; the LAST
// QUORUM_FINALITY blocker).
//
// THE BUG IT FIXES. The chain engine pins a block's weighted validator set to a
// P-CHAIN height (engine/chain: pChainHeightOf → epochHeightLocked → the SINGLE
// height the set-root commitment, the ⅔-by-stake tally, AND the per-voter pubkey
// resolution are read at). It reads that height by asserting the VM block to a
// `PChainHeight() uint64` (consensus block.SignedBlock). A bare VM block — every
// block the node's plugin VMs (C-Chain EVM, dexvm, …) produce — does NOT expose
// one, so pChainHeightOf returns 0 and the engine resolves the set at P-chain
// height 0: the GENESIS set. That is SAFE (non-empty, identical on every node,
// ≤ current) and UNBRICKS finality, but it FREEZES the epoch at genesis: a
// validator that JOINED after genesis is absent from set@0, so its vote is
// dropped and its stake is not counted — finality cannot track a DYNAMIC
// validator set (a departed genesis majority could even collude). This is the
// frozen-set caveat Gate D must sign away.
//
// THE FIX. pChainHeightVM wraps the chain's BlockBuilder (the VM the engine
// builds/parses blocks through) and makes the block the engine sees carry the
// proposer's P-chain epoch height — WITHOUT changing the inner VM's block format,
// IDs, or ledger state:
//
// - BuildBlock (proposer): build the inner block, then stamp the proposer's
// live P-chain epoch height H = max(GetCurrentHeight, parentH) (the
// proposervm selectChildPChainHeight rule — monotone, ≤ current). Return a
// pChainHeightBlock whose Bytes() are [magic|H|innerBytes] and whose
// PChainHeight() is H. Every other method (ID, ParentID, Height, Verify,
// Accept, …) delegates to the inner block — so the block's IDENTITY and the
// VM's state are the inner VM's, unchanged.
// - ParseBlock (follower): split [magic|H|innerBytes], parse the inner bytes
// through the inner VM, and re-attach H. Bytes WITHOUT the magic (a raw inner
// block from a pre-wrapper peer, GetAncestors, or genesis) parse with H=0 →
// the SAFE genesis-set fallback — never worse than today.
//
// DETERMINISM is the whole point and is why H must travel IN the gossiped bytes.
// The engine gossips only blk.Bytes(); a follower recovers the block solely via
// ParseBlock(those bytes). The proposer STAMPS H; every follower ADOPTS the
// identical H from the envelope — it never recomputes H from its own (skewing)
// current P-chain view. So every honest node derives the SAME epoch height from
// the SAME signed block, which is the invariant the engine's cert verifier
// requires (engine/chain HandleIncomingCert cross-checks the cert's set-root
// against the set-root WE recompute at OUR epoch height for the block: equal H ⟹
// equal root ⟹ the cert verifies; a post-genesis validator's vote+stake now
// count). A build-time-only stamp would give the proposer a real H but leave
// followers computing a DIFFERENT root from their own height → the cert is
// dropped → finality STALLS on a dynamic set (strictly worse than the genesis
// fallback). That is why the height is carried, not recomputed.
//
// This is a consensus-TRANSPORT framing (an 8-byte height + 4-byte magic prefix
// on the gossiped bytes), NOT a chain/ledger fork: the inner VM's bytes, block
// IDs, and execution state are byte-identical, so there is no re-genesis — only a
// coordinated node upgrade (the whole validator set ships together).
package chains
import (
"context"
"encoding/binary"
"errors"
"sync"
"time"
consensuschain "github.com/luxfi/consensus/engine/chain"
"github.com/luxfi/consensus/engine/chain/block"
"github.com/luxfi/ids"
validators "github.com/luxfi/validators"
)
// pChainHeightMagic tags a transport-wrapped block so ParseBlock can tell a
// wrapped block ([magic|H:8|innerBytes]) from a raw inner block (no magic →
// H=0, the genesis-set fallback). Distinct, fixed, version-pinned: a future
// envelope change bumps the last byte and is non-malleable. "LXP" = Lux P-chain.
var pChainHeightMagic = [4]byte{'L', 'X', 'P', 0x01}
// pChainHeightHeaderLen is the fixed transport-header width: magic(4) + height(8).
const pChainHeightHeaderLen = 4 + 8
// pChainHeightRecencySlack is the receive-side UPPER bound on how far a gossiped
// block's stamped P-chain epoch height H may exceed THIS node's live P-chain
// height before the block is rejected as absurd-future (HIGH-1, predicate b). The
// proposer stamps H = its own GetCurrentHeight (the proposervm selectChildPChainHeight
// rule, ≤ its current); a follower lagging the P-chain sees a smaller current, so
// some forward skew is LEGITIMATE during a staking change or a gossip burst. 256
// P-chain heights swamps any honest inter-node skew (P-chain blocks are seconds
// apart; gossip+verify is sub-second) while still rejecting a wildly-future H. The
// bound is a LIVENESS/DoS sanity gate, NOT the safety bound (that is the monotone
// gate in the engine): a future H always FAILS set resolution at the verifier
// (errUnfinalizedHeight) and never finalizes against a bogus set regardless — this
// just fails it fast and keeps the heights map from accreting unresolvable entries.
const pChainHeightRecencySlack = uint64(256)
// pChainHeightMapCap is the hard backstop on the heights map size — the cap that
// bounds the gossip-parse DoS (MEDIUM-3): ParseBlock runs before the engine's
// dedup/Verify, so an attacker peer could stream distinct unverified blocks, each
// adding a permanent entry. The map plateaus far below this in steady state (the
// watermark prune evicts every finalized block's entry as finality advances); the
// cap fires only under a sustained flood, and then evicts HIGHEST-chain-height
// first — spam claims wild heights, real pending blocks cluster just above the
// finalized watermark, so the near-tip pending band is preserved (NOT a blind LRU
// that could drop a live block). Chosen >> any realistic pending working set.
const pChainHeightMapCap = 4096
// errPChainHeightNotRecent is returned by ParseBlock when a wrapped block's stamped
// P-chain epoch exceeds this node's live P-chain height by more than the recency
// slack. Returning an error fails CLOSED: HandleIncomingBlock drops the block (it
// is never tracked or voted), which is the correct response to an absurd-future
// epoch a follower cannot resolve a set at.
var errPChainHeightNotRecent = errors.New("chains: gossiped block P-chain epoch height exceeds local P-chain height + recency slack (absurd-future epoch, rejected fail-closed)")
// pChainHeightVM wraps a chain BlockBuilder so the block the consensus engine
// sees carries the proposer's P-chain epoch height. It is the ONLY thing that
// changes between the engine and the inner VM; everything else is the inner VM,
// verbatim.
//
// It is installed ONLY on K>1 (quorum) chains: a K==1 chain finalizes on its
// sole validator's self-vote with no cert and no validator-set epoch, so the
// stamp is inert there and the wrapper is not installed (the inner VM is used
// directly — one obvious path per mode).
type pChainHeightVM struct {
inner consensuschain.BlockBuilder
state validators.State
networkID ids.ID
// heights memoises blockID → (stamped P-chain epoch, value-chain height) for
// blocks this node has built or parsed, so GetBlock can re-attach the epoch a
// block was stamped with (the inner VM stores only the inner bytes, which carry
// no epoch). Best-effort: a cache miss yields H=0 (the genesis-set fallback).
// GetBlock is NOT on the finality-capture path — the engine records a block's
// epoch the first time it BUILDS or PARSES the block (where the epoch is always
// present) and reads it back from its own pending-block record, never by
// re-fetching via GetBlock — so a miss here cannot drop a LIVE block's epoch
// (the consensus PendingBlock holds it, not this map).
//
// BOUNDED (MEDIUM-3): the value-chain height tagged on each entry lets onAccepted
// PRUNE every entry at or below the finalized-height watermark (a finalized block
// never needs a GetBlock epoch re-attach), so under steady finality the map
// plateaus at the watermark. finalizedHeight is that watermark, advanced as
// blocks Accept. A hard cap (pChainHeightMapCap) backstops a sustained
// unverified-parse flood, evicting highest-height-first to preserve the near-tip
// pending band. A still-PENDING block (height above the watermark) is never
// evicted by the watermark prune.
mu sync.Mutex
heights map[ids.ID]heightEntry
finalizedHeight uint64
}
// heightEntry records, for a remembered block, both the P-chain EPOCH it was
// stamped with (re-attached by GetBlock) and its VALUE-CHAIN height (the prune key:
// an entry at/below the finalized watermark is evictable).
type heightEntry struct {
epoch uint64
chainHeight uint64
}
// newPChainHeightVM wraps inner with P-chain-epoch-height stamping backed by the
// height-indexed validators.State. networkID is the set the height is resolved
// against (PrimaryNetworkID for native chains; the L1's set ID otherwise) — it is
// recorded for symmetry with the engine's epoch reads, though GetCurrentHeight is
// a P-chain-global height. A nil state disables stamping (BuildBlock falls back to
// H=0): callers install this ONLY for K>1 chains, which the manager already
// guards to have a live height-indexed state, so the nil path is a defensive
// fail-soft, not a runtime mode.
func newPChainHeightVM(inner consensuschain.BlockBuilder, state validators.State, networkID ids.ID) *pChainHeightVM {
return &pChainHeightVM{
inner: inner,
state: state,
networkID: networkID,
heights: make(map[ids.ID]heightEntry),
}
}
var _ consensuschain.BlockBuilder = (*pChainHeightVM)(nil)
// remember records the epoch a block (at value-chain height chainHeight) was
// stamped with so GetBlock can re-attach it. BOUNDED (MEDIUM-3): the map is pruned
// against the finalized-height watermark by onAccepted, so it plateaus under steady
// finality; remember additionally enforces the hard cap as a flood backstop. The
// chainHeight is the prune key.
func (vm *pChainHeightVM) remember(id ids.ID, epoch, chainHeight uint64) {
vm.mu.Lock()
defer vm.mu.Unlock()
vm.heights[id] = heightEntry{epoch: epoch, chainHeight: chainHeight}
vm.enforceCapLocked()
}
func (vm *pChainHeightVM) recall(id ids.ID) (uint64, bool) {
vm.mu.Lock()
e, ok := vm.heights[id]
vm.mu.Unlock()
return e.epoch, ok
}
// onAccepted advances the finalized-height watermark to acceptedHeight and PRUNES
// every remembered entry at or below it (MEDIUM-3). A finalized block's epoch is
// already captured in the engine's records and will never be re-attached via
// GetBlock, so dropping its entry is loss-free — and a still-PENDING block (height
// strictly above the watermark) is NEVER evicted by this prune, so its GetBlock
// epoch survives. Called from the wrapped block's Accept (the block tells its VM
// the height at which it finalized). Idempotent and monotone: a re-accept or an
// out-of-order lower height never lowers the watermark.
func (vm *pChainHeightVM) onAccepted(acceptedHeight uint64) {
vm.mu.Lock()
defer vm.mu.Unlock()
if acceptedHeight > vm.finalizedHeight {
vm.finalizedHeight = acceptedHeight
}
for id, e := range vm.heights {
if e.chainHeight <= vm.finalizedHeight {
delete(vm.heights, id)
}
}
}
// enforceCapLocked is the flood backstop: if the map exceeds the hard cap, evict
// the entry with the HIGHEST value-chain height. Real pending blocks cluster just
// above the finalized watermark (the next few heights); an unverified-parse flood
// claims arbitrary, typically far-future heights — so evicting highest-first sheds
// spam while preserving the near-tip pending band (NOT a blind LRU that could drop
// a live block's epoch). The cap is reached only under attack: the watermark prune
// keeps the steady-state map far below it. Caller holds vm.mu.
func (vm *pChainHeightVM) enforceCapLocked() {
for len(vm.heights) > pChainHeightMapCap {
var victim ids.ID
var maxHeight uint64
first := true
for id, e := range vm.heights {
if first || e.chainHeight > maxHeight {
victim, maxHeight, first = id, e.chainHeight, false
}
}
delete(vm.heights, victim)
}
}
// currentPChainHeight returns the proposer's live P-chain height, the upper end
// of the proposervm selectChildPChainHeight rule. A nil state or a read error
// yields 0 (the genesis-set fallback): a height the proposer cannot resolve must
// not be stamped onto a block, since a follower could not resolve the set there
// either. A read error is symmetric across nodes for a committed P-chain, so the
// fallback is uniform.
func (vm *pChainHeightVM) currentPChainHeight(ctx context.Context) uint64 {
if vm.state == nil {
return 0
}
h, err := vm.state.GetCurrentHeight(ctx)
if err != nil {
return 0
}
return h
}
// parentHeight returns the P-chain epoch height stamped on parentID, or 0 if it
// is unknown — used to keep a child's stamped height monotone ≥ its parent's
// (selectChildPChainHeight). An unknown parent (0) cannot LOWER the child below
// the current height (the max below), so monotonicity is preserved fail-soft.
func (vm *pChainHeightVM) parentHeight(parentID ids.ID) uint64 {
h, _ := vm.recall(parentID)
return h
}
// BuildBlock builds the inner block and stamps it with the proposer's P-chain
// epoch height H = max(currentPChainHeight, parentH). This is the proposer side
// of the epoch: the one node building this block chooses H from its own live
// P-chain view, and that H rides the gossiped bytes to every follower (which
// adopt it, never recompute it). Monotone ≥ parent so a chain's epoch never goes
// backwards across blocks (a cert at a lower epoch than its parent would bind a
// stale set).
func (vm *pChainHeightVM) BuildBlock(ctx context.Context) (block.Block, error) {
inner, err := vm.inner.BuildBlock(ctx)
if err != nil {
return nil, err
}
h := vm.currentPChainHeight(ctx)
if ph := vm.parentHeight(inner.ParentID()); ph > h {
h = ph
}
vm.remember(inner.ID(), h, inner.Height())
return newPChainHeightBlock(vm, inner, h), nil
}
// ParseBlock recovers a block from transport bytes. Wrapped bytes
// ([magic|H|innerBytes]) yield a block whose PChainHeight() is the EXACT H the
// proposer stamped — the determinism guarantee: every node parsing the same
// gossiped bytes recovers the identical epoch height. Bytes without the magic are
// a raw inner block (a pre-wrapper peer, GetAncestors, genesis): parsed straight
// through with H=0, the SAFE genesis-set fallback.
//
// FRAME DISCRIMINATOR (LOW-2). The 4-byte magic collides at 2^-32 with the raw
// hash prefix some VMs use as their Bytes() (dexvm/quantumvm/dchain serialize
// parentID[0:32]||…). The magic match alone is NOT sufficient: a frame is real
// only if the inner re-parse of the framed payload ALSO succeeds. So when the
// prefix matches, attempt to parse b[header:] as an inner block; if that FAILS,
// fall back to parsing the WHOLE b as a raw inner block (the colliding raw block,
// whose bytes minus the 12-byte prefix are not a valid inner block). This removes
// the bootstrap stall a magic collision used to cause (mis-framed → inner decode
// fails closed → stall on that chain).
//
// RECENCY GATE (HIGH-1, predicate b). A real frame's stamped epoch H must be
// recent relative to THIS node's live P-chain height: H ≤ localCurrentH + slack.
// An absurd-future H (a Byzantine proposer claiming an epoch the network has not
// reached) is rejected fail-closed (the block is dropped, never tracked). This is
// the upper half of the epoch bound; the engine's monotone gate is the lower half
// (≥ parent's recorded epoch), so the epoch is pinned to [parentEpoch, localH+slack].
func (vm *pChainHeightVM) ParseBlock(ctx context.Context, b []byte) (block.Block, error) {
candidateInner, h, magicMatched := unwrapPChainHeight(b)
if magicMatched {
// The prefix looks like a frame. It IS a frame only if the framed payload
// parses as an inner block AND the stamped epoch is recent.
if inner, err := vm.inner.ParseBlock(ctx, candidateInner); err == nil {
if !vm.epochRecent(ctx, h) {
return nil, errPChainHeightNotRecent
}
vm.remember(inner.ID(), h, inner.Height())
return newPChainHeightBlock(vm, inner, h), nil
}
// Magic matched but the framed payload did NOT parse → this is a RAW block
// whose first bytes coincidentally equal the magic. Parse the whole b raw.
}
// Raw inner block: no proposer epoch available → genesis-set fallback. Still
// wrap (uniform block type to the engine) but with H=0 and a passthrough
// Bytes() so re-gossip of a raw block stays raw.
inner, err := vm.inner.ParseBlock(ctx, b)
if err != nil {
return nil, err
}
return newRawPChainHeightBlock(vm, inner), nil
}
// epochRecent reports whether a stamped P-chain epoch height is within the recency
// slack of this node's live P-chain height (HIGH-1, predicate b). A node with no
// resolvable current height (state nil / read error → 0) cannot bound recency, so
// it admits the block (the verifier still fails closed on an unresolvable future
// epoch at set-resolution time — recency here is a fast-fail/DoS sanity gate, not
// the safety bound). Heights at or below local are always recent.
func (vm *pChainHeightVM) epochRecent(ctx context.Context, h uint64) bool {
localH := vm.currentPChainHeight(ctx)
if localH == 0 {
return true
}
return h <= localH+pChainHeightRecencySlack
}
// GetBlock fetches a block from the inner VM and re-attaches the P-chain height
// it was stamped with (recalled from build/parse). A miss yields H=0 — acceptable
// because GetBlock is not on the engine's finality-capture path (see the heights
// field doc). The returned block's Bytes() is the inner bytes (the inner VM's
// canonical at-rest form); a stamped height is re-attached for the engine's
// in-memory use only.
func (vm *pChainHeightVM) GetBlock(ctx context.Context, id ids.ID) (block.Block, error) {
inner, err := vm.inner.GetBlock(ctx, id)
if err != nil {
return nil, err
}
if h, ok := vm.recall(id); ok {
return newRawPChainHeightBlockWithHeight(vm, inner, h), nil
}
return newRawPChainHeightBlock(vm, inner), nil
}
// LastAccepted delegates to the inner VM.
func (vm *pChainHeightVM) LastAccepted(ctx context.Context) (ids.ID, error) {
return vm.inner.LastAccepted(ctx)
}
// SetPreference delegates to the inner VM.
func (vm *pChainHeightVM) SetPreference(ctx context.Context, id ids.ID) error {
return vm.inner.SetPreference(ctx, id)
}
// wrapPChainHeight frames inner bytes with the proposer's P-chain height:
// magic(4) || height(8,BE) || innerBytes. The header is fixed-width so unwrap is
// a constant-time prefix read.
func wrapPChainHeight(innerBytes []byte, height uint64) []byte {
out := make([]byte, pChainHeightHeaderLen+len(innerBytes))
copy(out[0:4], pChainHeightMagic[:])
binary.BigEndian.PutUint64(out[4:12], height)
copy(out[pChainHeightHeaderLen:], innerBytes)
return out
}
// unwrapPChainHeight is a PURE prefix splitter: it reports whether b carries the
// frame magic at the header width and, if so, returns the candidate payload and
// the encoded height. magicMatched==true means ONLY that the prefix looks like a
// frame — NOT that b is definitely framed. ParseBlock makes the real decision by
// re-parsing the candidate payload (LOW-2): a raw block whose first bytes collide
// with the magic (2^-32) yields magicMatched==true here but its payload fails the
// inner re-parse, so ParseBlock falls back to a raw whole-b parse. Keeping this a
// pure split (no VM dependency) localizes the collision handling to ParseBlock.
func unwrapPChainHeight(b []byte) (payload []byte, height uint64, magicMatched bool) {
if len(b) < pChainHeightHeaderLen ||
b[0] != pChainHeightMagic[0] || b[1] != pChainHeightMagic[1] ||
b[2] != pChainHeightMagic[2] || b[3] != pChainHeightMagic[3] {
return b, 0, false
}
height = binary.BigEndian.Uint64(b[4:12])
return b[pChainHeightHeaderLen:], height, true
}
// --- the wrapped block -------------------------------------------------------
// pChainHeightBlock is a chain block that carries a P-chain epoch height for the
// engine while delegating identity, verification, and acceptance to the inner VM
// block. It satisfies consensus block.SignedBlock's PChainHeight() (the subset
// the engine reads via pChainHeightOf), so the engine records the real epoch
// height — every other behaviour is the inner VM's.
//
// `bytes` is what the engine gossips. For a proposer-built / wrapped-parsed block
// it is the framed [magic|H|innerBytes] so a follower recovers H; for a raw block
// (genesis-set fallback) it is the inner bytes verbatim (passthrough) so re-gossip
// stays raw.
type pChainHeightBlock struct {
vm *pChainHeightVM
inner block.Block
pChainHeight uint64
bytes []byte
}
// newPChainHeightBlock wraps inner with height h and a FRAMED Bytes()
// ([magic|h|inner]) — the proposer/parse path, where H must travel to followers.
func newPChainHeightBlock(vm *pChainHeightVM, inner block.Block, h uint64) *pChainHeightBlock {
return &pChainHeightBlock{
vm: vm,
inner: inner,
pChainHeight: h,
bytes: wrapPChainHeight(inner.Bytes(), h),
}
}
// newRawPChainHeightBlock wraps inner with H=0 and a PASSTHROUGH Bytes() (the
// inner bytes verbatim) — the genesis-set fallback for a raw inner block.
func newRawPChainHeightBlock(vm *pChainHeightVM, inner block.Block) *pChainHeightBlock {
return &pChainHeightBlock{vm: vm, inner: inner, pChainHeight: 0, bytes: inner.Bytes()}
}
// newRawPChainHeightBlockWithHeight re-attaches a recalled height to an inner
// block fetched via GetBlock while keeping a PASSTHROUGH Bytes() (the inner VM's
// at-rest bytes). Used off the finality-capture path; the height is for the
// engine's in-memory epoch read, not for re-gossip framing.
func newRawPChainHeightBlockWithHeight(vm *pChainHeightVM, inner block.Block, h uint64) *pChainHeightBlock {
return &pChainHeightBlock{vm: vm, inner: inner, pChainHeight: h, bytes: inner.Bytes()}
}
func (b *pChainHeightBlock) ID() ids.ID { return b.inner.ID() }
func (b *pChainHeightBlock) Parent() ids.ID { return b.inner.Parent() }
func (b *pChainHeightBlock) ParentID() ids.ID { return b.inner.ParentID() }
func (b *pChainHeightBlock) Height() uint64 { return b.inner.Height() }
func (b *pChainHeightBlock) Timestamp() time.Time { return b.inner.Timestamp() }
func (b *pChainHeightBlock) Status() uint8 { return b.inner.Status() }
func (b *pChainHeightBlock) Bytes() []byte { return b.bytes }
// PChainHeight is the method the engine reads via pChainHeightOf — the SOLE
// reason this wrapper exists. The inner block does not expose it; this does.
func (b *pChainHeightBlock) PChainHeight() uint64 { return b.pChainHeight }
// Verify/Reject delegate to the inner VM block: the wrapper changes the epoch the
// engine SEES, never the block's validity or state transition.
func (b *pChainHeightBlock) Verify(ctx context.Context) error { return b.inner.Verify(ctx) }
func (b *pChainHeightBlock) Reject(ctx context.Context) error { return b.inner.Reject(ctx) }
// Accept finalizes the inner block, then advances the VM's finalized-height
// watermark and prunes the heights map at/below it (MEDIUM-3). The inner Accept
// runs FIRST: finalization must not depend on the bookkeeping prune, and the prune
// is a pure memory-management side effect that can never fail. The block carries
// its own height, so the VM learns the exact finalized height with no extra read.
func (b *pChainHeightBlock) Accept(ctx context.Context) error {
if err := b.inner.Accept(ctx); err != nil {
return err
}
if b.vm != nil {
b.vm.onAccepted(b.inner.Height())
}
return nil
}
// Unwrap exposes the inner block for code that must reach the underlying VM block
// (e.g. a missing-context reparse). Kept narrow on purpose.
func (b *pChainHeightBlock) Unwrap() block.Block { return b.inner }
+375
View File
@@ -0,0 +1,375 @@
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// quorum.go — the node-layer wiring that makes the consensus engine's α-of-K
// quorum-cert finality LIVE (HIGH-4). The consensus engine (luxfi/consensus
// engine/chain) defines the quorum RULE and the vote/cert topology interfaces;
// THIS file supplies the concrete node implementations the engine needs:
//
// - blsVoteVerifier — verifies a validator's BLS signature over the canonical
// vote message against the chain's validator set (the engine is scheme-
// agnostic; the node injects BLS).
// - blsVoteSigner — signs THIS node's accept votes with its staking BLS key
// so its signature can be collected into a cert.
// - validatorStakeSource — supplies per-validator stake so finality is a
// ⅔-by-STAKE supermajority (HIGH-3), not a raw voter count.
// - networkGossiper.BroadcastVote / GossipCert — the QuorumGossiper transport:
// a follower broadcasts its signed vote to ALL validators and any node that
// collects α distinct signed votes gossips the assembled cert, so finality
// never hinges on one node's inbound Chits (liveness; no proposer-freeze).
//
// Inbound votes/certs arrive as app-gossip and are demuxed in blockHandler.Gossip
// (see manager.go) into engine.HandleIncomingVote / HandleIncomingCert.
package chains
import (
"bytes"
"context"
"crypto/sha256"
"encoding/binary"
"errors"
"sort"
consensusconfig "github.com/luxfi/consensus/config"
consensuschain "github.com/luxfi/consensus/engine/chain"
"github.com/luxfi/constants"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/ids"
validators "github.com/luxfi/validators"
)
// isLocalDevNetwork reports whether networkID is an explicitly-local developer
// network: devnet (3) or localnet (1337). These are EXACT IDs (per luxfi/constants
// convention), NOT a range — a custom value L1 with a high networkID (e.g. an
// L1 whose chainID == networkID) is a VALUE network and must NOT match here.
// Local dev networks are the only IDs that run the minimal-BFT committee
// (LocalBFTParams, K=4) instead of the large-committee Default (K=20), because a
// handful of localhost validators cannot reach an α=14-of-K=20 quorum.
func isLocalDevNetwork(networkID uint32) bool {
return networkID == constants.DevnetID || networkID == constants.LocalID
}
// selectConsensusParams picks the consensus parameters for a chain.
//
// - sybilProtection == false (--dev / single-node): K=1, the sole validator's
// accept is the 1-of-1 quorum (no peer signatures).
// - sybilProtection == true, VALUE network: a large BYZANTINE-fault-tolerant
// param set — NEVER LocalParams() (K=3/α=2, f=0, which a single Byzantine
// validator forks; this was CRITICAL-2). Mainnet→K=21, Testnet→K=11, every
// other value net→Default K=20.
// - sybilProtection == true, LOCAL DEV network (devnet 3 / localnet 1337):
// LocalBFTParams() (K=4/α=3, f=1) — the MINIMAL real-BFT committee. Default
// K=20 is unsatisfiable on a few localhost validators: α=14 affirmative votes
// are unreachable with 3-4 validators, so no block ever finalizes and the
// P-Chain freezes at height 0 (no C-Chain is ever created). K=4 makes quorum
// reachable (3 of 4) while staying genuinely BFT — it still clears
// ValidateForValueNetwork (K≥4, f≥1) and the CRITICAL-2 multi-node-is-BFT
// regression, so production safety is untouched. (A local devnet should run
// ≥4 validators to realise f=1; with 3 it degrades to near-unanimous f=0,
// which is safe though not live under a fault.)
//
// All branches satisfy the 2α−K ≥ f+1 overlap bound; the manager call site also
// asserts ValidateForValueNetwork as a fail-closed backstop (K=4 passes it).
func selectConsensusParams(sybilProtection bool, networkID uint32) consensusconfig.Parameters {
if !sybilProtection {
return consensusconfig.SingleValidatorParams()
}
switch networkID {
case constants.MainnetID:
return consensusconfig.MainnetParams()
case constants.TestnetID:
return consensusconfig.TestnetParams()
default:
if isLocalDevNetwork(networkID) {
return consensusconfig.LocalBFTParams()
}
return consensusconfig.DefaultParams()
}
}
// --- BLS vote verifier -------------------------------------------------------
// blsVoteVerifier verifies a validator's BLS signature over the canonical vote
// message. The validator's BLS public key is resolved FROM THE HEIGHT-INDEXED
// validators.State AT THE BLOCK'S P-CHAIN EPOCH HEIGHT (RESIDUAL-B) — the SAME
// height-pinned source the set-root and the ⅔-by-stake tally read from
// (validatorSetAtHeight). It is NOT resolved from the CURRENT validator map.
//
// Why the epoch, not the current map: during an async validator-set change a
// validator can be present in the set@H (it legitimately signed the block being
// voted on at height H) yet ALREADY GONE from the current map. Resolving its
// pubkey from the current map drops its valid vote, and if that validator holds
// >⅓ of the stake-at-H the stable set falls below ⅔ and block H NEVER finalizes
// (this is not self-healing for that block — the skew is permanent for H). Pinning
// pubkey resolution to set@H — alongside membership, set-root, and stake — makes
// the cert internally consistent at exactly one epoch.
//
// An unknown validator at the epoch, a validator with no BLS key, or a bad
// signature all yield false (never an error/panic) — a cert with such a voter is
// simply invalid, the fail-closed contract the engine requires. A nil state (the
// no-op on a non-quorum node) yields false for every voter; a K>1 chain is
// guarded against ever reaching here with a nil/no-op state (manager.go).
type blsVoteVerifier struct {
state validators.State
networkID ids.ID
}
func newBLSVoteVerifier(state validators.State, networkID ids.ID) *blsVoteVerifier {
return &blsVoteVerifier{state: state, networkID: networkID}
}
// VerifyVote implements consensuschain.VoteVerifier. epochHeight is the block's
// P-chain height; the voter's pubkey is read from the set IN FORCE AT that height.
func (v *blsVoteVerifier) VerifyVote(nodeID ids.NodeID, message []byte, sig []byte, epochHeight uint64) bool {
out, ok := validatorSetAtHeight(v.state, v.networkID, epochHeight)[nodeID]
if !ok || out == nil || len(out.PublicKey) == 0 {
return false
}
pk, err := bls.PublicKeyFromCompressedBytes(out.PublicKey)
if err != nil || pk == nil {
return false
}
signature, err := bls.SignatureFromBytes(sig)
if err != nil || signature == nil {
return false
}
return bls.Verify(pk, signature, message)
}
var _ consensuschain.VoteVerifier = (*blsVoteVerifier)(nil)
// --- BLS vote signer ---------------------------------------------------------
// blsVoteSigner signs this node's accept votes with its staking BLS key.
type blsVoteSigner struct {
signer bls.Signer
}
func newBLSVoteSigner(signer bls.Signer) *blsVoteSigner {
if signer == nil {
return nil
}
return &blsVoteSigner{signer: signer}
}
// SignVote implements consensuschain.VoteSigner.
func (s *blsVoteSigner) SignVote(message []byte) ([]byte, error) {
sig, err := s.signer.Sign(message)
if err != nil {
return nil, err
}
return bls.SignatureToBytes(sig), nil
}
var _ consensuschain.VoteSigner = (*blsVoteSigner)(nil)
// --- height-pinned epoch read (MEDIUM-1) -------------------------------------
// validatorSetAtHeight reads the validator set IN FORCE AT a value-chain height
// from the height-indexed validators.State. This is the SINGLE source of epoch
// truth shared by the stake source and the set-root source: both membership and
// weights are read at the SAME height H so a cert's signed set-root and its
// ⅔-by-stake tally are measured against the identical set.
//
// Determinism across nodes is the whole point. validators.State.GetValidatorSet
// returns the set the network already agreed on at height H (P-chain / L1-staking
// consensus), so every honest node computes the same set — and therefore the
// same set-root and the same tally — for a given H, INDEPENDENT of async
// current-map skew during a validator-set change. (The previous Manager.GetMap()
// read hashed the CURRENT map, which diverges between the signer and the
// assembler across that skew window → mismatched canonical messages → dropped
// votes → finality stall at every staking change. That was MEDIUM-1.)
//
// A nil state, a lookup error, or an empty set yields a nil map, which the
// callers fold into their fail-soft answers (0 weight / Empty root). An error is
// SYMMETRIC across nodes (a committed height H reads the same on every node, or
// fails the same way), so the degraded answer is uniform — it never makes one
// node's view disagree with another's, which is the property that matters here.
func validatorSetAtHeight(state validators.State, networkID ids.ID, height uint64) map[ids.NodeID]*validators.GetValidatorOutput {
if state == nil {
return nil
}
set, err := state.GetValidatorSet(context.Background(), height, networkID)
if err != nil || len(set) == 0 {
return nil
}
return set
}
// --- stake source (HIGH-3, height-pinned by MEDIUM-1) ------------------------
// validatorStakeSource supplies validator voting weights so the engine can
// require a ⅔-by-stake supermajority for finality (HIGH-3). Weights are read
// from the HEIGHT-INDEXED validators.State at the cert-position height, the same
// height the set-root commits to (MEDIUM-1). Reading the tally at the same epoch
// as the signed membership means a validator whose vote is in the cert (its
// signature verifies against the height-H set-root) also contributes its height-H
// weight to the tally — eliminating the second skew (a current-map weight read
// could drop a legitimately-signed quorum when membership changed between sign
// and tally).
type validatorStakeSource struct {
state validators.State
networkID ids.ID
}
func newValidatorStakeSource(state validators.State, networkID ids.ID) *validatorStakeSource {
return &validatorStakeSource{state: state, networkID: networkID}
}
// Weight implements consensuschain.StakeSource. Returns the validator's stake in
// the set IN FORCE AT height — deterministic across nodes for a given height. An
// unknown validator (or a fail-soft empty read) yields 0, which cannot inflate
// the numerator.
func (s *validatorStakeSource) Weight(nodeID ids.NodeID, height uint64) uint64 {
out, ok := validatorSetAtHeight(s.state, s.networkID, height)[nodeID]
if !ok || out == nil {
return 0
}
return out.Light
}
// TotalStake implements consensuschain.StakeSource. Total active stake of the set
// IN FORCE AT height (the denominator of the ⅔ predicate), measured at the same
// epoch as Weight and the set-root.
func (s *validatorStakeSource) TotalStake(height uint64) uint64 {
var total uint64
for _, out := range validatorSetAtHeight(s.state, s.networkID, height) {
if out != nil {
total += out.Light
}
}
return total
}
var _ consensuschain.StakeSource = (*validatorStakeSource)(nil)
// --- validator-set-root source (MEDIUM: epoch binding) -----------------------
// validatorSetRootSource computes the deterministic commitment to the validator
// set IN FORCE AT a value-chain height — the value the engine stamps into every
// vote's VotePosition.ValidatorSetRoot so a cert is pinned to the exact set it
// was certified under. It is the node side of the MEDIUM fix: it turns the
// "⅔-by-stake measured at the cert-position epoch" property into an ENFORCED
// invariant (a cross-epoch cert fails verification because every signature was
// over this root).
//
// HEIGHT-PINNED (MEDIUM-1). The root is read from the HEIGHT-INDEXED
// validators.State at the value-chain block height, NOT from the Manager's
// CURRENT map. At a given height H, GetValidatorSet returns the set the network
// already agreed on, so every honest node — signer and assembler alike — computes
// the IDENTICAL root for H, independent of async current-map skew during a
// validator-set change. Reading the current map (the prior bug) let the signer
// and the assembler hold different maps across that skew window → different roots
// → the canonical signed message differed → signatures failed verification →
// votes dropped → finality stalled at every staking change.
//
// The commitment is a SHA-256 over the set serialized in a canonical order
// (validators sorted by NodeID, each as nodeID || light || len(pubkey) ||
// pubkey) — see hashValidatorSet. Sorting by NodeID + length-prefixing the
// pubkey makes the encoding canonical and unambiguous; the byte layout is
// UNCHANGED from the prior implementation, so the wire format and the engine's
// epoch-binding contract are preserved (only the SOURCE of the set changed from
// the current map to the height-indexed set).
type validatorSetRootSource struct {
state validators.State
networkID ids.ID
}
func newValidatorSetRootSource(state validators.State, networkID ids.ID) *validatorSetRootSource {
return &validatorSetRootSource{state: state, networkID: networkID}
}
// ValidatorSetRoot implements consensuschain.ValidatorSetRootSource. Returns the
// commitment to the weighted set IN FORCE AT height (deterministic across nodes).
// Returns ids.Empty when the state is absent or the set is empty (the explicit
// "unbound" answer, consistent with the engine default); a height-read error is
// symmetric across nodes, so the Empty fallback is uniform and never creates a
// cross-node root disagreement.
func (s *validatorSetRootSource) ValidatorSetRoot(height uint64) ids.ID {
return hashValidatorSet(validatorSetAtHeight(s.state, s.networkID, height))
}
var _ consensuschain.ValidatorSetRootSource = (*validatorSetRootSource)(nil)
// hashValidatorSet computes the canonical SHA-256 commitment to a weighted
// validator set: validators sorted by NodeID, each serialized as
// nodeID || light(8,BE) || len(pubkey)(8,BE) || pubkey. An empty/nil set commits
// to ids.Empty (the "unbound" answer). This is the SINGLE definition of the
// set-root encoding (DRY) — both the live source and its tests hash through here,
// so the wire format cannot drift between them.
func hashValidatorSet(set map[ids.NodeID]*validators.GetValidatorOutput) ids.ID {
if len(set) == 0 {
return ids.Empty
}
nodeIDs := make([]ids.NodeID, 0, len(set))
for nodeID := range set {
nodeIDs = append(nodeIDs, nodeID)
}
sort.Slice(nodeIDs, func(i, j int) bool {
return bytes.Compare(nodeIDs[i][:], nodeIDs[j][:]) < 0
})
h := sha256.New()
var u64 [8]byte
for _, nodeID := range nodeIDs {
v := set[nodeID]
h.Write(nodeID[:])
binary.BigEndian.PutUint64(u64[:], v.Light)
h.Write(u64[:])
binary.BigEndian.PutUint64(u64[:], uint64(len(v.PublicKey)))
h.Write(u64[:])
h.Write(v.PublicKey)
}
var root ids.ID
copy(root[:], h.Sum(nil))
return root
}
// --- app-gossip envelope for votes/certs -------------------------------------
// The QuorumGossiper transport rides on app-gossip. A single framed envelope
// carries either a signed vote or an assembled cert. The receiver (blockHandler
// .Gossip) demuxes on the magic + kind and routes to the engine. A payload
// without the magic is a plain block gossip (legacy Put path), so the demux is
// backward-compatible.
//
// Layout (big-endian):
//
// magic:4 ("LXQ\x01") kind:1 blockID:32 payload:...
//
// kind 1 = signed vote (payload = engine encodeSignedVote: nodeID+sig)
// kind 2 = finality cert (payload = engine cert MarshalBinary)
var quorumGossipMagic = [4]byte{'L', 'X', 'Q', 0x01}
const (
quorumKindVote byte = 1
quorumKindCert byte = 2
)
// ErrNotQuorumGossip signals a payload is not a quorum envelope (so the caller
// falls through to the block-gossip path).
var ErrNotQuorumGossip = errors.New("chains: not a quorum gossip envelope")
// encodeQuorumGossip frames a vote/cert payload for app-gossip.
func encodeQuorumGossip(kind byte, blockID ids.ID, payload []byte) []byte {
buf := make([]byte, 0, 4+1+32+len(payload))
buf = append(buf, quorumGossipMagic[:]...)
buf = append(buf, kind)
buf = append(buf, blockID[:]...)
buf = append(buf, payload...)
return buf
}
// decodeQuorumGossip parses an envelope. Returns ErrNotQuorumGossip if the magic
// is absent (a normal block gossip) — fail-soft so the legacy path is preserved.
func decodeQuorumGossip(data []byte) (kind byte, blockID ids.ID, payload []byte, err error) {
if len(data) < 4+1+32 || [4]byte{data[0], data[1], data[2], data[3]} != quorumGossipMagic {
return 0, ids.Empty, nil, ErrNotQuorumGossip
}
kind = data[4]
copy(blockID[:], data[5:5+32])
payload = data[5+32:]
if kind != quorumKindVote && kind != quorumKindCert {
return 0, ids.Empty, nil, ErrNotQuorumGossip
}
return kind, blockID, payload, nil
}
+109
View File
@@ -0,0 +1,109 @@
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// quorum_guard_node_test.go — CRITICAL-1(c) fail-closed guard + the wiring proof
// the MEDIUM-1 round lacked. The round-1 tests injected a validators.State into
// the source constructors directly and never exercised the path where
// m.validatorState is nil (the production default until the P-Chain publishes
// its State). These tests pin:
//
// (1) the guard predicate: a K>1 chain with NO height-indexed state is refused
// (would otherwise stall finality forever);
// (2) the failure mechanism: getValidatorState(nil) → the no-op State, whose
// GetValidatorSet is EMPTY at every height → the stake source totals 0 and
// the set-root is Empty (exactly the inputs that make VerifyWeighted fail
// closed). This is what the guard exists to prevent silently;
// (3) the fix: once a REAL height-indexed state is published, getValidatorState
// returns it and the same sources read a live set (non-zero stake / non-Empty
// root) — finality can proceed.
package chains
import (
"context"
"testing"
"github.com/luxfi/ids"
validators "github.com/luxfi/validators"
"github.com/luxfi/validators/validatorstest"
)
// TestQuorumGuard_RefusesNoopState pins the guard predicate (CRITICAL-1(c)): a
// nil (unpublished) validator state is NOT live, so a K>1 chain must refuse to
// start; a published state IS live.
func TestQuorumGuard_RefusesNoopState(t *testing.T) {
if quorumValidatorStateLive(nil) {
t.Fatal("CRITICAL-1(c): a nil validator state must NOT be considered live (would stall finality)")
}
live := validatorstest.NewTestState()
if !quorumValidatorStateLive(live) {
t.Fatal("a published height-indexed validator state must be considered live")
}
}
// TestGetValidatorState_NoopIsEmptyAtEveryHeight proves the failure mechanism the
// guard prevents: with no state published, getValidatorState yields the no-op
// State, and the height-pinned sources read an EMPTY set at every height — zero
// total stake and an Empty set-root, the exact inputs that make the engine's
// VerifyWeighted fail closed (ErrQCStakeBelowSupermajority) so NO block finalizes.
func TestGetValidatorState_NoopIsEmptyAtEveryHeight(t *testing.T) {
netID := ids.GenerateTestID()
// Production default: validatorState unset → getValidatorState(nil) = no-op.
noop := getValidatorState(nil)
if noop == nil {
t.Fatal("getValidatorState(nil) must return the no-op State, not nil")
}
stake := newValidatorStakeSource(noop, netID)
root := newValidatorSetRootSource(noop, netID)
for _, h := range []uint64{0, 1, 7, 1_000, 10_000_000} {
if total := stake.TotalStake(h); total != 0 {
t.Fatalf("no-op State must report zero total stake at height %d, got %d", h, total)
}
if r := root.ValidatorSetRoot(h); r != ids.Empty {
t.Fatalf("no-op State must commit to Empty set-root at height %d, got %s", h, r)
}
}
}
// TestGetValidatorState_LiveStateReadsSet proves the fix: once a real
// height-indexed state is published (as the P-Chain does), getValidatorState
// returns it and the SAME sources read a live set — non-zero stake and a
// non-Empty set-root — so the ⅔-by-stake finality predicate can be satisfied.
func TestGetValidatorState_LiveStateReadsSet(t *testing.T) {
netID := ids.GenerateTestID()
n0, n1, n2 := ids.GenerateTestNodeID(), ids.GenerateTestNodeID(), ids.GenerateTestNodeID()
const H = uint64(7)
live := validatorstest.NewTestState()
live.GetValidatorSetF = func(_ context.Context, height uint64, gotNet ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
if gotNet != netID || height != H {
return map[ids.NodeID]*validators.GetValidatorOutput{}, nil
}
return map[ids.NodeID]*validators.GetValidatorOutput{
n0: {NodeID: n0, PublicKey: []byte("pk0"), Light: 30, Weight: 30},
n1: {NodeID: n1, PublicKey: []byte("pk1"), Light: 30, Weight: 30},
n2: {NodeID: n2, PublicKey: []byte("pk2"), Light: 40, Weight: 40},
}, nil
}
// getValidatorState passes a non-nil state through unchanged.
got := getValidatorState(live)
stake := newValidatorStakeSource(got, netID)
root := newValidatorSetRootSource(got, netID)
if total := stake.TotalStake(H); total != 100 {
t.Fatalf("live State must report total stake 100 at the epoch, got %d", total)
}
if w := stake.Weight(n2, H); w != 40 {
t.Fatalf("live State must report n2 weight 40 at the epoch, got %d", w)
}
if r := root.ValidatorSetRoot(H); r == ids.Empty {
t.Fatal("live State must commit to a NON-Empty set-root at the epoch")
}
// A different height (no set) is still Empty/zero — the read is height-pinned.
if stake.TotalStake(H+1) != 0 || root.ValidatorSetRoot(H+1) != ids.Empty {
t.Fatal("live State read must be height-pinned (empty at a height with no set)")
}
}
+132
View File
@@ -0,0 +1,132 @@
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// quorum_params_test.go — CRITICAL-2 node-layer regression: the node must NEVER
// wire a non-BFT consensus param set for a multi-validator (sybil-protected)
// chain. The round-1 hole was manager.go selecting LocalParams() (K=3/α=2 → f=0,
// CFT) for ALL multi-node nets — a single Byzantine validator forks K=3/α=2.
// These tests pin selectConsensusParams to a BFT-safe set for every multi-node
// network and prove the value-network backstop (ValidateForValueNetwork) accepts
// the selected params.
package chains
import (
"testing"
consensusconfig "github.com/luxfi/consensus/config"
"github.com/luxfi/constants"
)
// TestSelectConsensusParams_MultiNodeIsBFT proves that for EVERY multi-validator
// (sybilProtection==true) network the node selects a Byzantine-fault-tolerant
// param set (f≥1, i.e. K≥4) that also clears the value-network validator — and
// that it is NEVER LocalParams (K=3) or any K<4 set. This is the node half of
// CRITICAL-2 (the consensus half is config.ValidateForValueNetwork, tested in
// the consensus module).
func TestSelectConsensusParams_MultiNodeIsBFT(t *testing.T) {
local := consensusconfig.LocalParams()
cases := []struct {
name string
networkID uint32
}{
{"mainnet", constants.MainnetID},
{"testnet", constants.TestnetID},
{"localnet-multinode", constants.LocalID},
{"unittest-multinode", constants.UnitTestID},
{"arbitrary-multinode", 424242},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
p := selectConsensusParams(true /* sybilProtection */, tc.networkID)
// MUST be Byzantine-fault-tolerant: f≥1 ⟹ K≥4.
if p.ByzantineFaultTolerance() < 1 {
t.Fatalf("multi-node net %q got non-BFT params K=%d f=%d (CRITICAL-2: a single faulty validator forks)",
tc.name, p.K, p.ByzantineFaultTolerance())
}
// MUST NOT be the CFT LocalParams (K=3/α=2) that was the round-1 hole.
if p.K == local.K && p.AlphaPreference == local.AlphaPreference && p.K == 3 {
t.Fatalf("multi-node net %q selected LocalParams (K=3/α=2) — the CRITICAL-2 fork config", tc.name)
}
// The selected params MUST themselves pass Valid() (the BFT α-floor:
// 2·AlphaPreference K ≥ f+1).
if err := p.Valid(); err != nil {
t.Fatalf("multi-node net %q selected params fail Valid(): %v (K=%d α=%d)",
tc.name, err, p.K, p.AlphaPreference)
}
})
}
}
// TestSelectConsensusParams_LocalDevIsSatisfiableBFT proves that an explicitly-
// local dev network (devnet 3 / localnet 1337) selects the MINIMAL real-BFT set
// (LocalBFTParams: K=4/α=3, f=1) — satisfiable by a handful of localhost
// validators — and NOT the large Default (K=20/α=14), whose α=14 quorum is
// unreachable with 3-4 validators (the freeze-at-height-0 bug). It must still be
// genuinely BFT (f≥1) and pass the value backstop (K=4 ≥ 4), so production
// safety / CRITICAL-2 is untouched: a custom VALUE L1 (high networkID) still gets
// the large Default set, never the local one.
func TestSelectConsensusParams_LocalDevIsSatisfiableBFT(t *testing.T) {
for _, networkID := range []uint32{constants.DevnetID, constants.LocalID} {
p := selectConsensusParams(true /* sybilProtection */, networkID)
// Satisfiable on a small committee: α must be small (K=4 → α=3), NOT 14.
if p.K != 4 || p.AlphaPreference != 3 {
t.Fatalf("local dev net %d: want minimal-BFT K=4/α=3, got K=%d/α=%d (Default K=20 is unsatisfiable on localhost)",
networkID, p.K, p.AlphaPreference)
}
// Still genuinely BFT (f≥1) — does NOT regress CRITICAL-2.
if p.ByzantineFaultTolerance() < 1 {
t.Fatalf("local dev net %d: K=%d is not BFT (f=%d)", networkID, p.K, p.ByzantineFaultTolerance())
}
// Must clear the value backstop the manager asserts before engine start.
if err := p.ValidateForValueNetwork(networkID); err != nil {
t.Fatalf("local dev net %d: minimal-BFT params must pass the value backstop, got %v", networkID, err)
}
}
// A custom value L1 with a high networkID is NOT a local dev network: it must
// keep the large Default set (the isLocalDevNetwork predicate is EXACT, not a
// >=1337 range that would wrongly catch value L1s).
const customValueL1 = uint32(909090)
if p := selectConsensusParams(true, customValueL1); p.K != consensusconfig.DefaultParams().K {
t.Fatalf("custom value L1 %d must get Default K=%d, got K=%d (must NOT match the local-dev path)",
customValueL1, consensusconfig.DefaultParams().K, p.K)
}
}
// TestSelectConsensusParams_SingleNodeIsK1 proves --dev / sybil-disabled selects
// the K=1 single-validator regime (the sole validator's accept is the 1-of-1
// quorum) — and NOT a multi-node BFT set.
func TestSelectConsensusParams_SingleNodeIsK1(t *testing.T) {
p := selectConsensusParams(false /* sybilProtection */, constants.LocalID)
if p.K != 1 {
t.Fatalf("sybil-disabled (single-node) must select K=1, got K=%d", p.K)
}
}
// TestSelectConsensusParams_ValueBackstop proves the params selected for a
// multi-node net pass the STRICTER value-network validator for that net — the
// fail-closed backstop asserted at the manager call site before starting the
// engine. (Mainnet enforces K≥11, so MainnetParams K=21 passes; Default K=20
// passes for an arbitrary value net.)
func TestSelectConsensusParams_ValueBackstop(t *testing.T) {
cases := []struct {
name string
networkID uint32
}{
{"mainnet", constants.MainnetID},
{"arbitrary-value-net", 909090},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
p := selectConsensusParams(true, tc.networkID)
if err := p.ValidateForValueNetwork(tc.networkID); err != nil {
t.Fatalf("selected params for value net %q must pass the value backstop, got %v (K=%d)",
tc.name, err, p.K)
}
})
}
}
+293
View File
@@ -0,0 +1,293 @@
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// quorum_setroot_test.go — node-layer tests for MEDIUM-1: the set-root and the
// ⅔-by-stake tally MUST be read from the HEIGHT-INDEXED validators.State at the
// cert-position height, so every node computes the IDENTICAL root, weights, and
// total for a given value-chain height — INDEPENDENT of async current-map skew
// during a validator-set change.
//
// The cross-node test (TestValidatorSetRoot_CrossNodeAgreesDespiteSkew) is the
// one the red flagged as MISSING: it models two nodes whose CURRENT validator
// views diverge (a set-change in flight) but who agree on the historical set at a
// pinned height H — and proves they nonetheless compute the SAME set-root at H.
// Reading the current map (the prior bug) would have made their roots differ →
// mismatched canonical signed messages → dropped votes → finality stall.
package chains
import (
"bytes"
"context"
"crypto/sha256"
"encoding/binary"
"errors"
"sort"
"testing"
"github.com/luxfi/ids"
validators "github.com/luxfi/validators"
"github.com/luxfi/validators/validatorstest"
)
// expectedSetRoot is an INDEPENDENT reimplementation of the canonical set-root
// spec, used only by the golden test to cross-check hashValidatorSet. It is
// deliberately NOT a call to hashValidatorSet (that would be a tautology): if the
// production encoding ever diverges from this spec the golden test fails.
func expectedSetRoot(t *testing.T, vdrs []vdr) ids.ID {
t.Helper()
sort.Slice(vdrs, func(i, j int) bool {
return bytes.Compare(vdrs[i].nodeID[:], vdrs[j].nodeID[:]) < 0
})
h := sha256.New()
var u64 [8]byte
for _, v := range vdrs {
h.Write(v.nodeID[:])
binary.BigEndian.PutUint64(u64[:], v.light)
h.Write(u64[:])
binary.BigEndian.PutUint64(u64[:], uint64(len(v.pk)))
h.Write(u64[:])
h.Write(v.pk)
}
var root ids.ID
copy(root[:], h.Sum(nil))
return root
}
// vdr is a tiny height→set fixture: which validators (and weights/keys) the
// height-indexed state reports at each value-chain height.
type vdr struct {
nodeID ids.NodeID
pk []byte
light uint64
}
// stateWithHistory builds a validators.State whose GetValidatorSet returns the
// set registered for the requested height (and an empty set for unknown heights),
// scoped to netID. This is the height-indexed source MEDIUM-1 reads from.
func stateWithHistory(netID ids.ID, byHeight map[uint64][]vdr) *validatorstest.TestState {
s := validatorstest.NewTestState()
s.GetValidatorSetF = func(_ context.Context, height uint64, gotNet ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
if gotNet != netID {
return map[ids.NodeID]*validators.GetValidatorOutput{}, nil
}
out := make(map[ids.NodeID]*validators.GetValidatorOutput)
for _, v := range byHeight[height] {
out[v.nodeID] = &validators.GetValidatorOutput{
NodeID: v.nodeID,
PublicKey: v.pk,
Light: v.light,
Weight: v.light,
}
}
return out, nil
}
return s
}
// TestValidatorSetRoot_CrossNodeAgreesDespiteSkew is the MEDIUM-1 regression
// guard. Two nodes are mid validator-set change: their CURRENT sets differ
// (height 11 vs height 12), but both still serve the SAME committed set at the
// value-chain block height H=10 being voted on. The set-root at H MUST be
// identical on both nodes — that identity is what makes their signatures over the
// block's canonical message mutually verifiable. The prior current-map read would
// have produced two different roots here and stalled finality.
func TestValidatorSetRoot_CrossNodeAgreesDespiteSkew(t *testing.T) {
netID := ids.GenerateTestID()
n0, n1, n2, n3 := ids.GenerateTestNodeID(), ids.GenerateTestNodeID(), ids.GenerateTestNodeID(), ids.GenerateTestNodeID()
pk0, pk1, pk2, pk3 := []byte("pk-0-48bytes-placeholder"), []byte("pk-1"), []byte("pk-2"), []byte("pk-3")
const H = uint64(10)
setAtH := []vdr{{n0, pk0, 10}, {n1, pk1, 20}, {n2, pk2, 30}}
// Node A has already applied a stake bump that took effect at height 11
// (n1: 20→25) and sees that as its current set. It still knows the height-10
// set (the block being voted on).
nodeA := stateWithHistory(netID, map[uint64][]vdr{
H: setAtH,
11: {{n0, pk0, 10}, {n1, pk1, 25}, {n2, pk2, 30}},
})
// Node B has already applied a NEW validator that joined at height 12
// (n3 added) — a different, later current set. It too still knows height 10.
nodeB := stateWithHistory(netID, map[uint64][]vdr{
H: setAtH,
12: {{n0, pk0, 10}, {n1, pk1, 25}, {n2, pk2, 30}, {n3, pk3, 5}},
})
rootA := newValidatorSetRootSource(nodeA, netID).ValidatorSetRoot(H)
rootB := newValidatorSetRootSource(nodeB, netID).ValidatorSetRoot(H)
if rootA == ids.Empty {
t.Fatal("set-root at the voted height must be non-Empty (the set is non-empty)")
}
if rootA != rootB {
t.Fatalf("MEDIUM-1: cross-node set-root at height %d MUST be identical despite "+
"current-set skew, got A=%s B=%s", H, rootA, rootB)
}
// Sanity: had either node (wrongly) committed to its CURRENT set instead of
// the height-H set, the roots WOULD differ — proving the test actually
// exercises the skew (not a vacuous match).
rootA_cur := newValidatorSetRootSource(nodeA, netID).ValidatorSetRoot(11)
rootB_cur := newValidatorSetRootSource(nodeB, netID).ValidatorSetRoot(12)
if rootA_cur == rootB_cur {
t.Fatal("test is vacuous: the two nodes' CURRENT sets must differ to exercise the skew")
}
if rootA == rootA_cur {
t.Fatal("test is vacuous: height-H set must differ from node A's current set")
}
}
// TestValidatorSetRoot_HeightSelectsEpoch proves the root is a deterministic
// FUNCTION OF HEIGHT (not a fixed current-set snapshot): different heights with
// different sets yield different roots, the same height always yields the same
// root, and the encoding is insertion-order independent (canonical sort).
func TestValidatorSetRoot_HeightSelectsEpoch(t *testing.T) {
netID := ids.GenerateTestID()
n0, n1, n2 := ids.GenerateTestNodeID(), ids.GenerateTestNodeID(), ids.GenerateTestNodeID()
pk0, pk1, pk2 := []byte("pk-0-48bytes-placeholder"), []byte("pk-1"), []byte("pk-2")
state := stateWithHistory(netID, map[uint64][]vdr{
10: {{n0, pk0, 10}, {n1, pk1, 20}, {n2, pk2, 30}},
11: {{n0, pk0, 10}, {n1, pk1, 21}, {n2, pk2, 30}}, // n1 weight changed
})
src := newValidatorSetRootSource(state, netID)
root10 := src.ValidatorSetRoot(10)
root11 := src.ValidatorSetRoot(11)
if root10 == ids.Empty || root11 == ids.Empty {
t.Fatal("non-empty sets must commit to non-Empty roots")
}
if root10 == root11 {
t.Fatal("a different epoch (height with a changed weight) MUST yield a different root")
}
// Same height is stable (deterministic), repeatedly.
if again := src.ValidatorSetRoot(10); again != root10 {
t.Fatalf("set-root at a fixed height must be deterministic: %s != %s", again, root10)
}
// Insertion-order independence: a state that lists the SAME height-10 members
// in a different slice order yields the SAME root (canonical NodeID sort).
reordered := stateWithHistory(netID, map[uint64][]vdr{
10: {{n2, pk2, 30}, {n0, pk0, 10}, {n1, pk1, 20}},
})
if r := newValidatorSetRootSource(reordered, netID).ValidatorSetRoot(10); r != root10 {
t.Fatalf("set-root must be member-order independent: %s != %s", r, root10)
}
}
// TestValidatorSetRoot_FailSoftIsUniform proves the fail-soft answers are
// Empty/uniform (never a panic, never a per-node-divergent default): a nil state,
// an unknown height (empty set), an unknown network, and a height-read error all
// commit to ids.Empty. Uniformity is the safety property — a symmetric error
// degrades every node to the same Empty root, never to disagreeing roots.
func TestValidatorSetRoot_FailSoftIsUniform(t *testing.T) {
netID := ids.GenerateTestID()
// nil state → Empty.
if got := (&validatorSetRootSource{state: nil, networkID: netID}).ValidatorSetRoot(10); got != ids.Empty {
t.Fatalf("nil state must commit to ids.Empty, got %s", got)
}
// Known network, but a height with no registered set → Empty.
state := stateWithHistory(netID, map[uint64][]vdr{
10: {{ids.GenerateTestNodeID(), []byte("pk"), 10}},
})
if got := newValidatorSetRootSource(state, netID).ValidatorSetRoot(999); got != ids.Empty {
t.Fatalf("unknown height must commit to ids.Empty, got %s", got)
}
// Wrong network → empty set → Empty.
if got := newValidatorSetRootSource(state, ids.GenerateTestID()).ValidatorSetRoot(10); got != ids.Empty {
t.Fatalf("unknown network must commit to ids.Empty, got %s", got)
}
// A height-read ERROR → Empty (and it is symmetric: the same error on every
// node yields the same Empty root).
errState := validatorstest.NewTestState()
errState.GetValidatorSetF = func(_ context.Context, _ uint64, _ ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
return nil, errors.New("state unavailable at height")
}
if got := newValidatorSetRootSource(errState, netID).ValidatorSetRoot(10); got != ids.Empty {
t.Fatalf("a height-read error must commit to ids.Empty, got %s", got)
}
}
// TestValidatorStakeSource_HeightPinned proves the ⅔-by-stake tally is read at
// the SAME height as the set-root (MEDIUM-1's second skew): Weight/TotalStake are
// a deterministic function of height, so a validator whose vote is in a
// height-H cert contributes its height-H weight — not whatever the current map
// happens to hold after a membership change. This is what stops a current-map
// weight read from dropping a legitimately-signed quorum.
func TestValidatorStakeSource_HeightPinned(t *testing.T) {
netID := ids.GenerateTestID()
n0, n1, n2 := ids.GenerateTestNodeID(), ids.GenerateTestNodeID(), ids.GenerateTestNodeID()
state := stateWithHistory(netID, map[uint64][]vdr{
10: {{n0, []byte("pk0"), 70}, {n1, []byte("pk1"), 30}}, // total 100
11: {{n0, []byte("pk0"), 70}, {n1, []byte("pk1"), 30}, {n2, []byte("pk2"), 50}}, // total 150
})
src := newValidatorStakeSource(state, netID)
// At height 10 the tally is the height-10 epoch.
if w := src.Weight(n0, 10); w != 70 {
t.Fatalf("Weight(n0, h=10) = %d, want 70", w)
}
if total := src.TotalStake(10); total != 100 {
t.Fatalf("TotalStake(h=10) = %d, want 100", total)
}
// n2 is NOT in the height-10 set → 0 at h=10, but 50 at h=11. The tally is
// height-pinned, not current-map.
if w := src.Weight(n2, 10); w != 0 {
t.Fatalf("Weight(n2, h=10) = %d, want 0 (n2 joined at h=11)", w)
}
if w := src.Weight(n2, 11); w != 50 {
t.Fatalf("Weight(n2, h=11) = %d, want 50", w)
}
if total := src.TotalStake(11); total != 150 {
t.Fatalf("TotalStake(h=11) = %d, want 150", total)
}
// Unknown node at a known height → 0 (cannot inflate the numerator).
if w := src.Weight(ids.GenerateTestNodeID(), 10); w != 0 {
t.Fatalf("Weight(unknown, h=10) = %d, want 0", w)
}
// nil state → fail-soft zeros.
nilSrc := &validatorStakeSource{state: nil, networkID: netID}
if nilSrc.Weight(n0, 10) != 0 || nilSrc.TotalStake(10) != 0 {
t.Fatal("nil state must yield zero weight and total")
}
}
// TestHashValidatorSet_ByteStability is a GOLDEN test pinning the canonical
// set-root encoding so the wire format cannot drift (the engine's epoch-binding
// contract and any persisted/gossiped cert depend on this exact byte layout). If
// this value changes, the set-root encoding changed and every node in the
// network must upgrade in lockstep — it is a CONSENSUS-BREAKING change.
func TestHashValidatorSet_ByteStability(t *testing.T) {
// Fixed (non-random) NodeIDs so the golden is reproducible.
var a, b ids.NodeID
a[0], b[0] = 0x01, 0x02
set := map[ids.NodeID]*validators.GetValidatorOutput{
a: {NodeID: a, PublicKey: []byte{0xaa, 0xbb}, Light: 10},
b: {NodeID: b, PublicKey: []byte{0xcc}, Light: 20},
}
got := hashValidatorSet(set)
// Independently recompute the expected commitment from the canonical spec:
// sorted-by-NodeID, each nodeID || light(8,BE) || len(pk)(8,BE) || pk, SHA-256.
want := expectedSetRoot(t, []vdr{
{a, []byte{0xaa, 0xbb}, 10},
{b, []byte{0xcc}, 20},
})
if got != want {
t.Fatalf("set-root encoding drifted (CONSENSUS-BREAKING):\n got %s\n want %s", got, want)
}
// Empty/nil set → ids.Empty.
if hashValidatorSet(nil) != ids.Empty {
t.Fatal("nil set must commit to ids.Empty")
}
if hashValidatorSet(map[ids.NodeID]*validators.GetValidatorOutput{}) != ids.Empty {
t.Fatal("empty set must commit to ids.Empty")
}
}
+165
View File
@@ -0,0 +1,165 @@
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// quorum_verifier_height_test.go — node-layer regression for RESIDUAL-B and the
// CRITICAL-1 wiring: the BLS vote verifier resolves the voter's public key from
// the HEIGHT-INDEXED validators.State AT THE BLOCK'S P-CHAIN EPOCH HEIGHT — the
// SAME height-pinned source as the set-root and the ⅔-by-stake tally — NOT from
// the current validator map.
//
// The round-1 fix left the verifier reading the CURRENT map (m.Validators):
// a validator present in set@H (it legitimately signed block H) but already gone
// from the current map during async staking skew had its vote DROPPED, and if it
// held >⅓ of the stake-at-H the block never finalized. These tests prove the
// verifier now reads set@H, so such a vote verifies at H (and a vote keyed to the
// wrong height does not).
package chains
import (
"context"
"testing"
"github.com/luxfi/consensus/engine/chain"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/ids"
validators "github.com/luxfi/validators"
"github.com/luxfi/validators/validatorstest"
)
// blsKey is a test validator's BLS key material.
type blsKey struct {
nodeID ids.NodeID
sk *bls.SecretKey
pkComp []byte
}
func newBLSKey(t *testing.T) blsKey {
t.Helper()
sk, err := bls.NewSecretKey()
if err != nil {
t.Fatalf("NewSecretKey: %v", err)
}
return blsKey{
nodeID: ids.GenerateTestNodeID(),
sk: sk,
pkComp: bls.PublicKeyToCompressedBytes(sk.PublicKey()),
}
}
// stateWithBLSByHeight builds a height-indexed validators.State that reports the
// given BLS validators at each height (empty for unknown heights / wrong net).
func stateWithBLSByHeight(netID ids.ID, byHeight map[uint64][]blsKey) *validatorstest.TestState {
s := validatorstest.NewTestState()
s.GetValidatorSetF = func(_ context.Context, height uint64, gotNet ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
if gotNet != netID {
return map[ids.NodeID]*validators.GetValidatorOutput{}, nil
}
out := make(map[ids.NodeID]*validators.GetValidatorOutput)
for _, k := range byHeight[height] {
out[k.nodeID] = &validators.GetValidatorOutput{
NodeID: k.nodeID,
PublicKey: k.pkComp,
Light: 1,
Weight: 1,
}
}
return out, nil
}
return s
}
// TestBLSVoteVerifier_ResolvesPubkeyAtEpochHeight is the RESIDUAL-B core: a
// validator that is in set@H but has LEFT the set at a later height still has its
// vote verified at H (the verifier reads set@H, not the current map).
func TestBLSVoteVerifier_ResolvesPubkeyAtEpochHeight(t *testing.T) {
netID := ids.GenerateTestID()
keep := newBLSKey(t) // stays in the set across epochs
leaver := newBLSKey(t) // in set@10, GONE from set@11 (departed the current set)
const H = uint64(10)
state := stateWithBLSByHeight(netID, map[uint64][]blsKey{
10: {keep, leaver},
11: {keep}, // leaver has departed by height 11 (the "current" epoch)
})
v := newBLSVoteVerifier(state, netID)
// The leaver signs a message; its vote MUST verify at epoch H=10 (it was a
// member then), proving pubkey resolution is at the epoch, not the current set.
msg := []byte("LUX/chain/vote/v1\x00 — epoch-pinned verify")
sig, err := leaver.sk.Sign(msg)
if err != nil {
t.Fatalf("sign: %v", err)
}
sigBytes := bls.SignatureToBytes(sig)
if !v.VerifyVote(leaver.nodeID, msg, sigBytes, H) {
t.Fatal("RESIDUAL-B: a validator in set@H must verify at H even after leaving the current set " +
"(verifier read the current map instead of set@H)")
}
// At height 11 the leaver is NOT a member → its vote MUST NOT verify there.
// This proves the resolution is genuinely height-pinned (not height-agnostic).
if v.VerifyVote(leaver.nodeID, msg, sigBytes, 11) {
t.Fatal("a validator absent from set@11 must NOT verify at 11 (height pinning is not in effect)")
}
// The keeper verifies at both heights (it is in both sets) — sanity that the
// epoch read is not rejecting valid current members.
keepSig, _ := keep.sk.Sign(msg)
keepBytes := bls.SignatureToBytes(keepSig)
if !v.VerifyVote(keep.nodeID, msg, keepBytes, 10) || !v.VerifyVote(keep.nodeID, msg, keepBytes, 11) {
t.Fatal("a validator present at both epochs must verify at both")
}
}
// TestBLSVoteVerifier_FailClosed proves the verifier never panics and returns
// false for every fail-soft case: nil state, unknown voter at the epoch, wrong
// network, an unknown height (empty set), a wrong-length signature, and the
// HIGH-1 malformed-infinity signature (0x40||zeros) that used to PANIC the purego
// BLS path — here it must be a clean false, not a crash.
func TestBLSVoteVerifier_FailClosed(t *testing.T) {
netID := ids.GenerateTestID()
k := newBLSKey(t)
state := stateWithBLSByHeight(netID, map[uint64][]blsKey{10: {k}})
msg := []byte("msg")
sig, _ := k.sk.Sign(msg)
good := bls.SignatureToBytes(sig)
// nil state → false for any voter.
if newBLSVoteVerifier(nil, netID).VerifyVote(k.nodeID, msg, good, 10) {
t.Fatal("nil state must yield false")
}
v := newBLSVoteVerifier(state, netID)
// unknown voter at a known epoch.
if v.VerifyVote(ids.GenerateTestNodeID(), msg, good, 10) {
t.Fatal("unknown voter at the epoch must yield false")
}
// known voter at an UNKNOWN epoch (empty set) → false.
if v.VerifyVote(k.nodeID, msg, good, 999) {
t.Fatal("known voter at an unknown epoch (empty set) must yield false")
}
// wrong network → empty set → false.
if newBLSVoteVerifier(state, ids.GenerateTestID()).VerifyVote(k.nodeID, msg, good, 10) {
t.Fatal("wrong network must yield false")
}
// wrong-length signature → false (no panic).
if v.VerifyVote(k.nodeID, msg, good[:len(good)-1], 10) {
t.Fatal("wrong-length signature must yield false")
}
// HIGH-1 malformed infinity sig (0x40||zeros) → clean false, NOT a panic.
mal := make([]byte, bls.SignatureLen)
mal[0] = 0x40
if v.VerifyVote(k.nodeID, msg, mal, 10) {
t.Fatal("malformed-infinity signature must yield false")
}
// A WRONG signature (valid form, wrong key) → false.
other := newBLSKey(t)
otherSig, _ := other.sk.Sign(msg)
if v.VerifyVote(k.nodeID, msg, bls.SignatureToBytes(otherSig), 10) {
t.Fatal("a signature by a different key must yield false")
}
}
// ensure the node verifier still satisfies the (now height-aware) engine interface.
var _ chain.VoteVerifier = (*blsVoteVerifier)(nil)
+3 -3
View File
@@ -5,7 +5,7 @@ package rpc
import (
"bytes"
"encoding/json"
"github.com/go-json-experiment/json"
"fmt"
"io"
"net/http"
@@ -132,7 +132,7 @@ func (d *DebugTool) testEndpoint(url string) TestResult {
// Check if we got a valid JSON-RPC response
var rpcResp map[string]interface{}
if err := json.NewDecoder(postResp.Body).Decode(&rpcResp); err == nil {
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"])
@@ -179,7 +179,7 @@ func (d *DebugTool) testRPCMethods(url string) []RPCTest {
defer resp.Body.Close()
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err == nil {
if err := json.UnmarshalRead(resp.Body, &result); err == nil {
if res, ok := result["result"]; ok {
test.Success = true
test.Result = fmt.Sprintf("%v", res)
+1 -1
View File
@@ -70,7 +70,7 @@ func (s *mockServer) AddAliasesWithReadLock(endpoint string, aliases ...string)
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) Shutdown() error { return nil }
func (s *mockServer) SetRootInfoProvider(_ server.RootInfoProvider) {}
func TestHandlerManager_RegisterChainHandlers(t *testing.T) {
+1 -1
View File
@@ -10,7 +10,7 @@ package chains
import (
"encoding/hex"
"encoding/json"
"github.com/go-json-experiment/json"
"testing"
consensusconfig "github.com/luxfi/consensus/config"
-17
View File
@@ -1,17 +0,0 @@
//go:build tools
// +build tools
package main
import (
"fmt"
"github.com/luxfi/node/upgrade"
"github.com/luxfi/runtime"
)
func main() {
var c *upgrade.Config = &upgrade.Config{}
var _ runtime.NetworkUpgrades = c
fmt.Println("Interface satisfied")
}
+6 -6
View File
@@ -35,12 +35,12 @@ import (
var (
// Flags
outputPath string
inputPath string
sinceVer uint64
dataDir string
dbType string
noCompress bool
outputPath string
inputPath string
sinceVer uint64
dataDir string
dbType string
noCompress bool
forceRestore bool
)
+4 -2
View File
@@ -3,10 +3,12 @@ package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"os"
"github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
)
func main() {
@@ -190,7 +192,7 @@ func cmdExport(args []string) {
// stateEnvelope wraps CeremonyState with a SHA-256 integrity hash.
type stateEnvelope struct {
State json.RawMessage `json:"state"`
State jsontext.Value `json:"state"`
Integrity string `json:"integrity"` // hex(SHA-256(state bytes))
}
+3 -2
View File
@@ -3,7 +3,6 @@ package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"math/big"
"os"
"path/filepath"
@@ -12,6 +11,8 @@ import (
"github.com/consensys/gnark-crypto/ecc/bn254"
"github.com/consensys/gnark-crypto/ecc/bn254/fr"
"github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
)
// =============================================================================
@@ -309,7 +310,7 @@ func TestRegressionL03_StateFileHasIntegrity(t *testing.T) {
}
var envelope struct {
State json.RawMessage `json:"state"`
State jsontext.Value `json:"state"`
Integrity string `json:"integrity"`
}
if err := json.Unmarshal(raw, &envelope); err != nil {
+11 -11
View File
@@ -3,7 +3,7 @@ package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"github.com/go-json-experiment/json"
"fmt"
"time"
@@ -12,16 +12,16 @@ import (
// CeremonyState holds the full state of a powers-of-tau ceremony.
type CeremonyState struct {
Circuit string `json:"circuit"`
NumConstraints int `json:"numConstraints"`
PowersNeeded int `json:"powersNeeded"`
Participants int `json:"participants"`
TauG1 []G1Point `json:"tauG1"`
TauG2 []G2Point `json:"tauG2"`
AlphaG1 []G1Point `json:"alphaG1"`
BetaG1 []G1Point `json:"betaG1"`
BetaG2 G2Point `json:"betaG2"`
Contributions []Contribution `json:"contributions"`
Circuit string `json:"circuit"`
NumConstraints int `json:"numConstraints"`
PowersNeeded int `json:"powersNeeded"`
Participants int `json:"participants"`
TauG1 []G1Point `json:"tauG1"`
TauG2 []G2Point `json:"tauG2"`
AlphaG1 []G1Point `json:"alphaG1"`
BetaG1 []G1Point `json:"betaG1"`
BetaG2 G2Point `json:"betaG2"`
Contributions []Contribution `json:"contributions"`
}
// Contribution records a single participant's contribution.
+1 -1
View File
@@ -11,7 +11,7 @@
package main
import (
"encoding/json"
"github.com/go-json-experiment/json"
"fmt"
"os"
"sort"
+7 -6
View File
@@ -2,7 +2,8 @@ package main
import (
"encoding/hex"
"encoding/json"
"github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"fmt"
"os"
"time"
@@ -28,8 +29,8 @@ func main() {
"networkID": 200200,
"allocations": []map[string]interface{}{
{
"ethAddr": "0x9011E888251AB053B7bD1cdB598Db4f9DEd94714",
"luxAddr": zooAddr,
"evmAddr": "0x9011E888251AB053B7bD1cdB598Db4f9DEd94714",
"utxoAddr": zooAddr,
"initialAmount": 0,
"unlockSchedule": []map[string]interface{}{
{
@@ -47,7 +48,7 @@ func main() {
}
// Load validators from Lux genesis
luxGenesis, _ := os.ReadFile("/Users/z/work/lux/mainnet/genesis_mainnet.json")
luxGenesis, _ := os.ReadFile(os.ExpandEnv("$HOME/work/lux/mainnet/genesis_mainnet.json"))
var lux map[string]interface{}
json.Unmarshal(luxGenesis, &lux)
@@ -75,7 +76,7 @@ func main() {
ccBytes, _ := json.Marshal(cc)
genesis["cChainGenesis"] = string(ccBytes)
out, _ := json.MarshalIndent(genesis, "", " ")
os.WriteFile("/Users/z/work/lux/mainnet/zoo_genesis_valid.json", out, 0644)
out, _ := json.Marshal(genesis, jsontext.WithIndent(" "))
os.WriteFile(os.ExpandEnv("$HOME/work/lux/mainnet/zoo_genesis_valid.json"), out, 0644)
fmt.Println("Wrote zoo_genesis_valid.json")
}
+5 -5
View File
@@ -3,7 +3,7 @@ package main
import (
"fmt"
"os"
"github.com/luxfi/crypto/hash"
"github.com/luxfi/ids"
)
@@ -13,15 +13,15 @@ func main() {
fmt.Println("Usage: go run main.go <genesis_file>")
os.Exit(1)
}
data, err := os.ReadFile(os.Args[1])
if err != nil {
fmt.Printf("Error reading file: %v\n", err)
os.Exit(1)
}
fmt.Printf("File size: %d bytes\n", len(data))
// Compute hash the same way luxd does
rawHash := hash.ComputeHash256(data)
id, err := ids.ToID(rawHash)
@@ -29,6 +29,6 @@ func main() {
fmt.Printf("Error converting to ID: %v\n", err)
os.Exit(1)
}
fmt.Printf("Genesis ID: %s\n", id.String())
}
+63
View File
@@ -0,0 +1,63 @@
// pqkeygen provisions the strict-PQ staking keypairs a local luxd needs:
// ML-DSA-65 (FIPS 204) staking key + ML-KEM-768 (FIPS 203) handshake key.
// Writes PEM blocks with the exact types the node's config loader expects.
package main
import (
"bytes"
"crypto/rand"
"encoding/pem"
"fmt"
"os"
"path/filepath"
"github.com/luxfi/crypto/mldsa"
"github.com/luxfi/crypto/mlkem"
)
func writePEM(path, typ string, der []byte) error {
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
return err
}
var b bytes.Buffer
if err := pem.Encode(&b, &pem.Block{Type: typ, Bytes: der}); err != nil {
return err
}
return os.WriteFile(path, b.Bytes(), 0o600)
}
func main() {
if len(os.Args) != 2 {
fmt.Fprintln(os.Stderr, "usage: pqkeygen <staking-dir>")
os.Exit(1)
}
dir := os.Args[1]
// ML-DSA-65 staking key
dsaPriv, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
if err != nil {
panic(err)
}
dsaPub := dsaPriv.PublicKey.Bytes()
if err := writePEM(filepath.Join(dir, "mldsa.key"), "ML-DSA-65 PRIVATE KEY", dsaPriv.Bytes()); err != nil {
panic(err)
}
if err := writePEM(filepath.Join(dir, "mldsa.pub"), "ML-DSA-65 PUBLIC KEY", dsaPub); err != nil {
panic(err)
}
// ML-KEM-768 handshake key
kemPub, kemPriv, err := mlkem.GenerateKeyPair(rand.Reader, mlkem.MLKEM768)
if err != nil {
panic(err)
}
if err := writePEM(filepath.Join(dir, "mlkem.key"), "ML-KEM-768 PRIVATE KEY", kemPriv.Bytes()); err != nil {
panic(err)
}
if err := writePEM(filepath.Join(dir, "mlkem.pub"), "ML-KEM-768 PUBLIC KEY", kemPub.Bytes()); err != nil {
panic(err)
}
fmt.Printf("ML-DSA-65 priv=%dB pub=%dB; ML-KEM-768 priv=%dB pub=%dB written to %s\n",
len(dsaPriv.Bytes()), len(dsaPub), len(kemPriv.Bytes()), len(kemPub.Bytes()), dir)
}
+1 -1
View File
@@ -46,7 +46,7 @@ func (c *ChainDatabaseConfig) GetDatabaseType(chainAlias string) string {
func (c *ChainDatabaseConfig) Validate() error {
validTypes := map[string]bool{
"pebbledb": true,
"zapdb": true,
"zapdb": true,
"memdb": true,
}
+86 -397
View File
@@ -8,7 +8,6 @@ import (
"crypto/tls"
"encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
@@ -19,6 +18,8 @@ import (
"strings"
"time"
"github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"github.com/spf13/viper"
compression "github.com/luxfi/compress"
@@ -26,7 +27,6 @@ import (
"github.com/luxfi/constants"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/crypto/bls/signer/localsigner"
"github.com/luxfi/crypto/hash"
"github.com/luxfi/crypto/mldsa"
mlkemcrypto "github.com/luxfi/crypto/mlkem"
"github.com/luxfi/filesystem/perms"
@@ -51,7 +51,6 @@ import (
"github.com/luxfi/node/version"
"github.com/luxfi/node/vms/components/gas"
"github.com/luxfi/node/vms/platformvm/reward"
pchaintxs "github.com/luxfi/node/vms/platformvm/txs"
"github.com/luxfi/node/vms/platformvm/validators/fee"
"github.com/luxfi/node/vms/proposervm"
"github.com/luxfi/timer"
@@ -99,90 +98,16 @@ var (
errFileDoesNotExist = errors.New("file does not exist")
)
// AutomineNetworkConfig captures immutable network state for automine mode persistence.
// Once written to automine-network.json, this ensures the same genesis is produced
// on every restart, making C-Chain/EVM state persistence work correctly.
type AutomineNetworkConfig struct {
// Version for forward compatibility
Version int `json:"version"`
// Genesis start time - captured on first boot, reused on restart
StartTime uint64 `json:"startTime"`
// Node identity
NodeID string `json:"nodeId"`
// BLS credentials (hex-encoded)
BLSPublicKey string `json:"blsPublicKey"`
BLSPopProof string `json:"blsPopProof"`
// Computed genesis bytes and hash (for verification)
GenesisBytes []byte `json:"genesisBytes"`
GenesisHash string `json:"genesisHash"` // Actual hash of GenesisBytes
// X-Chain asset ID (LUX token ID)
XAssetID string `json:"xAssetId"`
// C-Chain genesis (stored separately for EVM immutability)
CChainGenesis string `json:"cChainGenesis"`
}
const (
devNetworkConfigVersion = 1
devNetworkConfigFilename = "automine-network.json"
)
// loadAutomineNetworkConfig attempts to load automine-network.json from the data directory.
// Returns nil if the file doesn't exist (first boot scenario).
func loadAutomineNetworkConfig(dataDir string) (*AutomineNetworkConfig, error) {
path := filepath.Join(dataDir, devNetworkConfigFilename)
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, nil // First boot - no config yet
}
return nil, fmt.Errorf("failed to read dev network config: %w", err)
}
var cfg AutomineNetworkConfig
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("failed to parse dev network config: %w", err)
}
if cfg.Version != devNetworkConfigVersion {
return nil, fmt.Errorf("unsupported dev network config version: %d (expected %d)", cfg.Version, devNetworkConfigVersion)
}
return &cfg, nil
}
// saveAutomineNetworkConfig atomically writes the dev network config to disk.
// Uses write-to-temp-then-rename pattern for crash safety.
func saveAutomineNetworkConfig(dataDir string, cfg *AutomineNetworkConfig) error {
path := filepath.Join(dataDir, devNetworkConfigFilename)
tempPath := path + ".tmp"
cfg.Version = devNetworkConfigVersion
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal dev network config: %w", err)
}
// Write to temp file
if err := os.WriteFile(tempPath, data, 0o600); err != nil {
return fmt.Errorf("failed to write temp dev network config: %w", err)
}
// Atomic rename
if err := os.Rename(tempPath, path); err != nil {
os.Remove(tempPath) // Clean up on failure
return fmt.Errorf("failed to rename dev network config: %w", err)
}
return nil
}
// (Removed: AutomineNetworkConfig + loadAutomineNetworkConfig +
// saveAutomineNetworkConfig + automine-network.json persistence.)
//
// --automine is a *consensus-mode* flag (single-node, single-validator
// quorum), nothing more. It MUST NOT swap in a different C-Chain
// genesis or persist any "dev network config" sidecar. C-Chain
// genesis is canonical genesis — for network-id 1/2/3/1337 it's the
// embedded luxfi/genesis config, period. If the operator wants a
// custom genesis they pass --genesis-file. No backwards compatibility
// for the deleted automine-network.json sidecar.
func getConsensusConfig(v *viper.Viper) consensusconfig.Parameters {
// Start with default parameters
@@ -850,15 +775,18 @@ func loadPEMBlock(path, expectType string) ([]byte, error) {
// pemBytesOrFile resolves either a base64-encoded PEM in
// contentKey (highest precedence, matches the
// --foo-file-content / --foo-file pattern the existing TLS
// loaders use) or a filesystem path in pathKey. Empty return =
// neither was set; caller decides whether that's a fatal config
// error or a "fall through to classical-compat" path.
// loaders use) or a filesystem path in pathKey. A set-but-empty
// contentKey is treated as absent and falls through to pathKey, so a
// blank content flag and a missing one behave identically. Empty
// return = neither was provided; caller decides whether that's a fatal
// config error or a "fall through to classical-compat" path.
func pemBytesOrFile(v *viper.Viper, contentKey, pathKey, expectType string) ([]byte, string, error) {
if v.IsSet(contentKey) {
raw := v.GetString(contentKey)
if raw == "" {
return nil, "", nil
}
// A non-empty *-content flag wins. A set-but-empty one (e.g. an env var or
// a deploy template that rendered to "") is treated identically to an
// absent flag: fall through to the *-file path below. Short-circuiting on
// the empty value here would silently degrade a strict-PQ validator to a
// classical ECDSA NodeID — the strict-PQ activation outage this guards.
if raw := v.GetString(contentKey); raw != "" {
decoded, err := base64.StdEncoding.DecodeString(raw)
if err != nil {
return nil, "", fmt.Errorf("decode %s base64: %w", contentKey, err)
@@ -916,7 +844,7 @@ func loadStakingMLDSA(v *viper.Viper) (*mldsa.PrivateKey, []byte, string, string
// Sanity: the public key on disk must match the one derivable
// from the private key. Mismatch would point a NodeID at a
// public key the validator cannot actually sign for — silent
// authentication failure that snowman++ would later report as
// authentication failure that the consensus engine would later report as
// "this validator is offline" rather than "you misconfigured
// your keys".
derivedPubBytes := priv.PublicKey.Bytes()
@@ -1108,20 +1036,17 @@ func getUpgradeConfig(v *viper.Viper, networkID uint32) (upgrade.Config, error)
}
var upgradeConfig upgrade.Config
if err := json.Unmarshal(upgradeBytes, &upgradeConfig); err != nil {
if err := json.Unmarshal(upgradeBytes, &upgradeConfig, json.MatchCaseInsensitiveNames(true)); err != nil {
return upgrade.Config{}, fmt.Errorf("unable to unmarshal upgrade bytes: %w", err)
}
return upgradeConfig, nil
}
func getGenesisData(v *viper.Viper, networkID uint32, stakingCfg *builder.StakingConfig, dataDir string) ([]byte, ids.ID, error) {
// Get allow-custom-genesis flag (defaults to true for development)
allowCustomGenesis := v.GetBool(AllowCustomGenesisKey)
// Handle automine mode genesis - dynamically generate genesis with the node's own credentials
if v.GetBool(DevModeKey) && !v.IsSet(GenesisFileKey) && !v.IsSet(GenesisFileContentKey) && !v.IsSet(GenesisDBKey) {
return getOrCreateAutomineGenesis(stakingCfg, dataDir)
}
// (Removed: automine-mode genesis swap.) --automine is single-node
// consensus only; it falls through to the canonical genesis loader
// below — same as a real network. C-Chain genesis comes from the
// canonical luxfi/genesis embedded config or --genesis-file. Period.
// HIGHEST PRIORITY: Raw genesis bytes - use directly without rebuilding
// This is critical for snapshot resume to avoid hash mismatch
@@ -1131,16 +1056,15 @@ func getGenesisData(v *viper.Viper, networkID uint32, stakingCfg *builder.Stakin
if err != nil {
return nil, ids.Empty, fmt.Errorf("failed to decode %s: %w", GenesisRawBytesKey, err)
}
// Extract xAssetID from the X-chain genesis within the platform genesis
xAssetID, err := extractXAssetID(genesisBytes)
utxoAssetID, err := resolveUTXOAssetID(networkID, genesisBytes)
if err != nil {
return nil, ids.Empty, fmt.Errorf("failed to extract xAssetID from raw genesis bytes: %w", err)
return nil, ids.Empty, fmt.Errorf("resolve X-Chain asset ID from %s: %w", GenesisRawBytesKey, err)
}
log.Info("loaded raw genesis bytes directly",
"size", len(genesisBytes),
"xAssetID", xAssetID,
"utxoAssetID", utxoAssetID,
)
return genesisBytes, xAssetID, nil
return genesisBytes, utxoAssetID, nil
}
// Check if genesis-db is specified for database replay
@@ -1162,7 +1086,7 @@ func getGenesisData(v *viper.Viper, networkID uint32, stakingCfg *builder.Stakin
// try first loading genesis content directly from flag/env-var
if v.IsSet(GenesisFileContentKey) {
genesisData := v.GetString(GenesisFileContentKey)
return builder.FromFlag(networkID, genesisData, stakingCfg, allowCustomGenesis)
return builder.FromFlag(networkID, genesisData, stakingCfg)
}
// if content is not specified go for the file
@@ -1171,21 +1095,32 @@ func getGenesisData(v *viper.Viper, networkID uint32, stakingCfg *builder.Stakin
// Check if we have cached genesis bytes to avoid rebuilding
cacheFile := filepath.Join(dataDir, "genesis.bytes")
if cachedBytes, err := loadCachedGenesisBytes(cacheFile); err == nil && len(cachedBytes) > 0 {
xAssetID, err := extractXAssetID(cachedBytes)
if err != nil {
log.Warn("failed to extract xAssetID from cached genesis, rebuilding",
"error", err,
)
} else {
utxoAssetID, err := resolveUTXOAssetID(networkID, cachedBytes)
if err == nil {
log.Info("loaded cached genesis bytes for hash stability",
"cacheFile", cacheFile,
"size", len(cachedBytes),
"utxoAssetID", utxoAssetID,
)
return cachedBytes, xAssetID, nil
return cachedBytes, utxoAssetID, nil
}
// Cache parse failed — almost certainly a codec mismatch
// after a binary upgrade (e.g. v1-codec bytes persisted by
// an older luxd, multi-version v0+v1 dispatcher in this
// luxd doesn't recognise a type the cached blob still
// uses). Drop the cache and rebuild from the file rather
// than wedging in a CrashLoop. Hash stability is forfeit
// for this single restart — intentional, the alternative
// is a permanent outage on every binary bump.
log.Warn("cached genesis bytes failed to parse — invalidating cache and rebuilding from genesis-file",
"cacheFile", cacheFile,
"size", len(cachedBytes),
"error", err,
)
_ = os.Remove(cacheFile)
}
// No cache or invalid cache - build from file and cache the result
genesisBytes, xAssetID, err := builder.FromFile(networkID, genesisFileName, stakingCfg, allowCustomGenesis)
genesisBytes, utxoAssetID, err := builder.FromFile(networkID, genesisFileName, stakingCfg)
if err != nil {
return nil, ids.Empty, err
}
@@ -1200,7 +1135,7 @@ func getGenesisData(v *viper.Viper, networkID uint32, stakingCfg *builder.Stakin
"size", len(genesisBytes),
)
}
return genesisBytes, xAssetID, nil
return genesisBytes, utxoAssetID, nil
}
// finally if file is not specified/readable go for the predefined config
@@ -1208,261 +1143,36 @@ func getGenesisData(v *viper.Viper, networkID uint32, stakingCfg *builder.Stakin
return builder.FromConfig(config)
}
// getOrCreateAutomineGenesis handles automine mode genesis with persistence.
// On first boot: generates genesis, saves to automine-network.json, returns genesis.
// On restart: loads from automine-network.json to ensure genesis hash stability.
// Returns: (genesisBytes, xAssetID, error) - xAssetID is the LUX token asset ID
func getOrCreateAutomineGenesis(stakingCfg *builder.StakingConfig, dataDir string) ([]byte, ids.ID, error) {
// Try to load existing dev network config
devCfg, err := loadAutomineNetworkConfig(dataDir)
if err != nil {
return nil, ids.Empty, fmt.Errorf("failed to load dev network config: %w", err)
}
if devCfg != nil {
// Existing config found - check if credentials match
// In automine mode with ephemeral certs, credentials will change on restart.
// We log a warning but continue with the stored genesis since automine mode
// is single-node and doesn't require credential consistency.
expectedNodeID := stakingCfg.NodeID
expectedBLSPK := fmt.Sprintf("0x%x", stakingCfg.BLSPublicKey)
expectedBLSPoP := fmt.Sprintf("0x%x", stakingCfg.BLSProofOfPossession)
credentialsMismatch := false
if devCfg.NodeID != expectedNodeID {
log.Warn("automine-network.json nodeID mismatch (using stored genesis anyway for automine mode)",
"stored", devCfg.NodeID,
"current", expectedNodeID,
)
credentialsMismatch = true
}
if devCfg.BLSPublicKey != expectedBLSPK {
log.Warn("automine-network.json BLS public key mismatch (using stored genesis anyway for automine mode)")
credentialsMismatch = true
}
if devCfg.BLSPopProof != expectedBLSPoP {
log.Warn("automine-network.json BLS PoP mismatch (using stored genesis anyway for automine mode)")
credentialsMismatch = true
}
if credentialsMismatch {
log.Warn("credentials changed since first boot - for persistent staking, use --staking-ephemeral-cert-enabled=false with persistent key files")
}
// Verify the stored genesis hash matches what we'll compute from the bytes
storedHash, err := ids.FromString(devCfg.GenesisHash)
if err != nil {
return nil, ids.Empty, fmt.Errorf("invalid genesis hash in automine-network.json: %w", err)
}
// Compute actual hash from stored bytes to verify integrity
computedHashBytes := hash.ComputeHash256(devCfg.GenesisBytes)
computedHash, err := ids.ToID(computedHashBytes)
if err != nil {
return nil, ids.Empty, fmt.Errorf("failed to convert computed hash to ID: %w", err)
}
if storedHash != computedHash {
return nil, ids.Empty, fmt.Errorf("genesis bytes corrupted: stored hash %s != computed hash %s", storedHash, computedHash)
}
// Parse stored X-Chain asset ID
xAssetID, err := ids.FromString(devCfg.XAssetID)
if err != nil {
return nil, ids.Empty, fmt.Errorf("invalid xAssetId in automine-network.json: %w", err)
}
log.Info("loaded dev network config",
"path", filepath.Join(dataDir, devNetworkConfigFilename),
"genesisHash", devCfg.GenesisHash,
"xAssetID", devCfg.XAssetID,
"startTime", devCfg.StartTime,
)
return devCfg.GenesisBytes, xAssetID, nil
}
// First boot - generate new genesis with current timestamp
startTime := uint64(time.Now().Unix())
// buildAutomineGenesis returns (genesisBytes, xAssetID) - the LUX token asset ID
genesisBytes, xAssetID, err := buildAutomineGenesis(stakingCfg, startTime)
if err != nil {
return nil, ids.Empty, err
}
// Compute actual genesis hash from bytes (this is what the node uses for DB validation)
genesisHashBytes := hash.ComputeHash256(genesisBytes)
genesisHash, err := ids.ToID(genesisHashBytes)
if err != nil {
return nil, ids.Empty, fmt.Errorf("failed to convert genesis hash to ID: %w", err)
}
// Save for future restarts. The CChainGenesis field is metadata only —
// the actual genesis bytes are embedded in genesisBytes — but mirror
// the resolved value so audits read true.
savedCChain := automineCChainGenesis
newDevCfg := &AutomineNetworkConfig{
Version: devNetworkConfigVersion,
StartTime: startTime,
NodeID: stakingCfg.NodeID,
BLSPublicKey: fmt.Sprintf("0x%x", stakingCfg.BLSPublicKey),
BLSPopProof: fmt.Sprintf("0x%x", stakingCfg.BLSProofOfPossession),
GenesisBytes: genesisBytes,
GenesisHash: genesisHash.String(),
XAssetID: xAssetID.String(),
CChainGenesis: savedCChain,
}
if err := saveAutomineNetworkConfig(dataDir, newDevCfg); err != nil {
// Log warning but don't fail - genesis is still valid
log.Warn("failed to save dev network config (persistence may not work on restart)",
"error", err,
)
} else {
log.Info("created dev network config",
"path", filepath.Join(dataDir, devNetworkConfigFilename),
"genesisHash", genesisHash.String(),
"xAssetID", xAssetID.String(),
"startTime", startTime,
)
}
return genesisBytes, xAssetID, nil
}
// buildAutomineGenesis creates a genesis configuration for single-node development mode.
// It uses the node's own credentials as the sole validator.
// resolveUTXOAssetID extracts the X-Chain native asset ID from the loaded
// platform-genesis blob. It is the canonical source of truth used by
// every load path that bypasses FromConfig (raw bytes, cached bytes —
// the paths that historically defaulted to constants.UTXOAssetIDFor and
// silently disagreed with the genesis content on sovereign L1s).
//
// C-Chain genesis comes from the embedded automineCChainGenesis constant
// below (chainId 31337). Downstream networks that need a different
// chainId ship their own platform-genesis bundle; the node does not
// accept an out-of-band file path at runtime.
func buildAutomineGenesis(stakingCfg *builder.StakingConfig, startTime uint64) ([]byte, ids.ID, error) {
// Parse node ID from staking config
nodeID, err := ids.NodeIDFromString(stakingCfg.NodeID)
// Behaviour:
//
// - If the genesis bakes an X-Chain, returns the runtime asset ID
// derived from that chain's CreateAssetTx (the same value
// vm.initGenesis assigns at runtime).
// - If the genesis is P-only (no X-Chain), returns the network-id-
// keyed constant. The asset ID is unused in that mode.
// - If the genesis is unparseable or the embedded X-Chain genesis
// is malformed, returns the corresponding error — these are
// unrecoverable on a primary-network bootstrap.
//
// Tested against both sovereign-network genesis (X-Chain present, asset
// ID differs from the constant) and upstream Lux genesis fixtures.
func resolveUTXOAssetID(networkID uint32, genesisBytes []byte) (ids.ID, error) {
id, ok, err := builder.UTXOAssetIDFromGenesisBytes(genesisBytes)
if err != nil {
return nil, ids.Empty, fmt.Errorf("failed to parse node ID for automine mode: %w", err)
return ids.Empty, err
}
// C-Chain genesis: built-in default (downstream networks bring
// their own platform-genesis bundle, not a path read at boot).
cChainGenesis := automineCChainGenesis
// Lux Treasury address: 0x9011E888251AB053B7bD1cdB598Db4f9DEd94714
// This is derived from LUX_MNEMONIC and funded on C-Chain
var rewardAddress ids.ShortID
treasuryAddr := "9011E888251AB053B7bD1cdB598Db4f9DEd94714"
treasuryBytes, err := ids.ShortFromString(treasuryAddr)
if err == nil {
rewardAddress = treasuryBytes
} else {
// Fall back to a deterministic address derived from node ID
copy(rewardAddress[:], nodeID[:20])
if !ok {
return constants.UTXOAssetIDFor(networkID), nil
}
// Create automine mode config with embedded C-Chain genesis (resolved
// above — operator override or built-in default).
devCfg := builder.DevModeConfig{
NodeID: nodeID,
BLSPublicKey: fmt.Sprintf("0x%x", stakingCfg.BLSPublicKey),
BLSPopProof: fmt.Sprintf("0x%x", stakingCfg.BLSProofOfPossession),
RewardAddress: rewardAddress,
CChainGenesis: cChainGenesis,
StartTime: startTime, // Use provided start time for determinism
}
return builder.ForDevMode(devCfg, stakingCfg)
return id, nil
}
// automineCChainGenesis is the default C-Chain genesis for automine mode.
// Network ID 1337, EVM Chain ID 31337.
// Funds Lux Treasury (0x9011), light mnemonic accounts (BIP44 m/44'/9000'/0'/0/{0-4}), and Anvil/Hardhat accounts.
const automineCChainGenesis = `{
"config": {
"chainId": 31337,
"homesteadBlock": 0,
"eip150Block": 0,
"eip155Block": 0,
"eip158Block": 0,
"byzantiumBlock": 0,
"constantinopleBlock": 0,
"petersburgBlock": 0,
"istanbulBlock": 0,
"muirGlacierBlock": 0,
"berlinBlock": 0,
"londonBlock": 0,
"arrowGlacierBlock": 0,
"grayGlacierBlock": 0,
"mergeNetsplitBlock": 0,
"shanghaiTime": 0,
"cancunTime": 0,
"blobSchedule": {
"cancun": {"target": 3, "max": 6, "baseFeeUpdateFraction": 3338477}
},
"terminalTotalDifficulty": 0,
"chainEVMTimestamp": 0,
"durangoTimestamp": 0,
"etnaTimestamp": 0,
"feeConfig": {
"gasLimit": 30000000,
"targetBlockRate": 1,
"minBaseFee": 1000000000,
"targetGas": 100000000,
"baseFeeChangeDenominator": 48,
"minBlockGasCost": 0,
"maxBlockGasCost": 1000000,
"blockGasCostStep": 200000
},
"warpConfig": {
"blockTimestamp": 0,
"quorumNumerator": 67,
"requirePrimaryNetworkSigners": false
}
},
"alloc": {
"9011E888251AB053B7bD1cdB598Db4f9DEd94714": {
"balance": "0x200000000000000000000000000000000000000000000000000000000000000"
},
"5369615110ca435bdf798f31c20ba6163d7b0a54": {
"balance": "0x200000000000000000000000000000000000000000000000000000000000000"
},
"2e701063ccdffa2b1872c596222d8067d124d3ef": {
"balance": "0x200000000000000000000000000000000000000000000000000000000000000"
},
"9030463eb1aaa563c8247468416cc0bf06347502": {
"balance": "0x200000000000000000000000000000000000000000000000000000000000000"
},
"f77b06331152fd0e536de0af65688a6559c6f914": {
"balance": "0x200000000000000000000000000000000000000000000000000000000000000"
},
"944fd51713652b9922690b7d06498fbf8742beac": {
"balance": "0x200000000000000000000000000000000000000000000000000000000000000"
},
"f39Fd6e51aad88F6F4ce6aB8827279cffFb92266": {
"balance": "0x200000000000000000000000000000000000000000000000000000000000000"
},
"70997970C51812dc3A010C7d01b50e0d17dc79C8": {
"balance": "0x200000000000000000000000000000000000000000000000000000000000000"
},
"3C44CdDdB6a900fa2b585dd299e03d12FA4293BC": {
"balance": "0x200000000000000000000000000000000000000000000000000000000000000"
},
"90F79bf6EB2c4f870365E785982E1f101E93b906": {
"balance": "0x200000000000000000000000000000000000000000000000000000000000000"
}
},
"nonce": "0x0",
"timestamp": "0x0",
"extraData": "0x",
"gasLimit": "0x1c9c380",
"difficulty": "0x0",
"mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"coinbase": "0x0000000000000000000000000000000000000000",
"number": "0x0",
"gasUsed": "0x0",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000"
}`
func getTrackedChains(v *viper.Viper) (set.Set[ids.ID], error) {
trackChainsStr := v.GetString(TrackChainsKey)
@@ -1601,7 +1311,7 @@ func getAliases(v *viper.Viper, name string, contentKey string, fileKey string)
}
aliasMap := make(map[ids.ID][]string)
if err := json.Unmarshal(fileBytes, &aliasMap); err != nil {
if err := json.Unmarshal(fileBytes, &aliasMap, json.MatchCaseInsensitiveNames(true)); err != nil {
return nil, fmt.Errorf("%w on %s: %w", errUnmarshalling, name, err)
}
return aliasMap, nil
@@ -1641,7 +1351,7 @@ func getChainConfigsFromFlag(v *viper.Viper) (map[string]chains.ChainConfig, err
}
chainConfigs := make(map[string]chains.ChainConfig)
if err := json.Unmarshal(chainConfigContent, &chainConfigs); err != nil {
if err := json.Unmarshal(chainConfigContent, &chainConfigs, json.MatchCaseInsensitiveNames(true)); err != nil {
return nil, fmt.Errorf("could not unmarshal JSON: %w", err)
}
return chainConfigs, nil
@@ -1723,8 +1433,8 @@ func getNetConfigsFromFlags(v *viper.Viper, netIDs []ids.ID) (map[ids.ID]nets.Co
}
// partially parse configs to be filled by defaults later
chainConfigs := make(map[ids.ID]json.RawMessage, len(netIDs))
if err := json.Unmarshal(netConfigContent, &chainConfigs); err != nil {
chainConfigs := make(map[ids.ID]jsontext.Value, len(netIDs))
if err := json.Unmarshal(netConfigContent, &chainConfigs, json.MatchCaseInsensitiveNames(true)); err != nil {
return nil, fmt.Errorf("could not unmarshal JSON: %w", err)
}
@@ -1733,7 +1443,7 @@ func getNetConfigsFromFlags(v *viper.Viper, netIDs []ids.ID) (map[ids.ID]nets.Co
config := getDefaultNetConfig(v)
if rawNetConfigBytes, ok := chainConfigs[chainID]; ok {
if err := json.Unmarshal(rawNetConfigBytes, &config); err != nil {
if err := json.Unmarshal(rawNetConfigBytes, &config, json.MatchCaseInsensitiveNames(true)); err != nil {
return nil, err
}
@@ -1792,7 +1502,7 @@ func getNetConfigsFromDir(v *viper.Viper, chainIDs []ids.ID) (map[ids.ID]nets.Co
}
// Update the default config with the values from the file
if err := json.Unmarshal(file, &config); err != nil {
if err := json.Unmarshal(file, &config, json.MatchCaseInsensitiveNames(true)); err != nil {
return nil, fmt.Errorf("%w: %w", errUnmarshalling, err)
}
@@ -2039,7 +1749,7 @@ func GetNodeConfig(v *viper.Viper) (node.Config, error) {
return node.Config{}, err
}
nodeConfig.TrackAllChains = v.GetBool(TrackAllChainsKey)
nodeConfig.DexValidator = v.GetBool(DexValidatorKey)
// HTTP APIs
nodeConfig.HTTPConfig, err = getHTTPConfig(v)
@@ -2166,11 +1876,10 @@ func GetNodeConfig(v *viper.Viper) (node.Config, error) {
// Get data directory for dev network config persistence
dataDir := getExpandedArg(v, DataDirKey)
nodeConfig.GenesisBytes, nodeConfig.XAssetID, err = getGenesisData(v, nodeConfig.NetworkID, &genesisStakingCfg, dataDir)
nodeConfig.GenesisBytes, nodeConfig.UTXOAssetID, err = getGenesisData(v, nodeConfig.NetworkID, &genesisStakingCfg, dataDir)
if err != nil {
return node.Config{}, fmt.Errorf("unable to load genesis file: %w", err)
}
nodeConfig.AllowGenesisUpdate = v.GetBool(AllowGenesisUpdateKey)
// StateSync Configs
nodeConfig.StateSyncConfig, err = getStateSyncConfig(v)
@@ -2299,26 +2008,6 @@ func saveCachedGenesisBytes(cacheFile string, genesisBytes []byte) error {
return os.WriteFile(cacheFile, genesisBytes, 0o600)
}
// extractXAssetID extracts the LUX asset ID from raw platform genesis bytes.
// This is needed when loading raw genesis bytes directly (for snapshot resume)
// to avoid rebuilding genesis which causes hash mismatch.
func extractXAssetID(genesisBytes []byte) (ids.ID, error) {
// Get the X-chain creation TX from the platform genesis
xChainTx, err := builder.VMGenesis(genesisBytes, constants.XVMID)
if err != nil {
return ids.Empty, fmt.Errorf("couldn't find X-chain genesis in platform genesis: %w", err)
}
// Extract the XVM genesis bytes from the create chain TX
createChainTx, ok := xChainTx.Unsigned.(*pchaintxs.CreateChainTx)
if !ok {
return ids.Empty, fmt.Errorf("X-chain genesis TX is not a CreateChainTx")
}
// Use the builder.XAssetID function to extract the asset ID from XVM genesis
return builder.XAssetID(createChainTx.GenesisData)
}
func providedFlags(v *viper.Viper) map[string]interface{} {
settings := v.AllSettings()
customSettings := make(map[string]interface{}, len(settings))
+1 -1
View File
@@ -586,7 +586,7 @@ Defaults to `0.1`.
Partial sync enables nodes that are not primary network validators to optionally sync
only the P-chain on the primary network. Nodes that use this option can still track
Chains. After the Etna upgrade, nodes that use this option can also validate L1s.
Chains. Nodes that use this option can also validate L1s.
This config defaults to `false`.
## Public IP
+85 -5
View File
@@ -4,9 +4,10 @@
package config
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/go-json-experiment/json"
"log"
"os"
"path/filepath"
@@ -17,13 +18,34 @@ import (
"github.com/stretchr/testify/require"
consensusconfig "github.com/luxfi/consensus/config"
"github.com/luxfi/constants"
"github.com/luxfi/ids"
"github.com/luxfi/node/chains"
"github.com/luxfi/node/genesis/builder"
"github.com/luxfi/node/nets"
pchaingenesis "github.com/luxfi/node/vms/platformvm/genesis"
pchaintxs "github.com/luxfi/node/vms/platformvm/txs"
)
const chainConfigFilenameExtension = ".ex"
// equalChainConfigs compares two ChainConfig maps using bytes.Equal for the
// []byte fields, so nil and empty []byte compare equal. json/v2 unmarshals
// absent/null []byte fields to empty (non-nil), unlike v1 which left them nil.
func equalChainConfigs(t *testing.T, expected, actual map[string]chains.ChainConfig) {
t.Helper()
require := require.New(t)
require.Equal(len(expected), len(actual), "map length mismatch")
for k, want := range expected {
got, ok := actual[k]
require.True(ok, "missing key %q", k)
require.True(bytes.Equal(want.Config, got.Config),
"%q Config: expected %x got %x", k, want.Config, got.Config)
require.True(bytes.Equal(want.Upgrade, got.Upgrade),
"%q Upgrade: expected %x got %x", k, want.Upgrade, got.Upgrade)
}
}
func TestGetChainConfigsFromFiles(t *testing.T) {
tests := map[string]struct {
configs map[string]string
@@ -87,7 +109,7 @@ func TestGetChainConfigsFromFiles(t *testing.T) {
require.Equal(root, v.GetString(ChainConfigDirKey))
chainConfigs, err := getChainConfigs(v)
require.NoError(err)
require.Equal(test.expected, chainConfigs)
equalChainConfigs(t, test.expected, chainConfigs)
})
}
}
@@ -147,7 +169,11 @@ func TestGetChainConfigsDirNotExist(t *testing.T) {
// don't read with getConfigFromViper since it's very slow.
chainConfigs, err := getChainConfigs(v)
require.ErrorIs(err, test.expectedErr)
require.Equal(test.expected, chainConfigs)
if test.expected == nil {
require.Nil(chainConfigs)
} else {
equalChainConfigs(t, test.expected, chainConfigs)
}
})
}
}
@@ -167,7 +193,7 @@ func TestSetChainConfigDefaultDir(t *testing.T) {
chainConfigs, err := getChainConfigs(v)
require.NoError(err)
expected := map[string]chains.ChainConfig{"C": {Config: []byte("helloworld"), Upgrade: []byte(nil)}}
require.Equal(expected, chainConfigs)
equalChainConfigs(t, expected, chainConfigs)
}
func TestGetChainConfigsFromFlags(t *testing.T) {
@@ -234,7 +260,7 @@ func TestGetChainConfigsFromFlags(t *testing.T) {
// Parse config
chainConfigs, err := getChainConfigs(v)
require.NoError(err)
require.Equal(test.expected, chainConfigs)
equalChainConfigs(t, test.expected, chainConfigs)
})
}
}
@@ -672,3 +698,57 @@ func TestDevModeFlags(t *testing.T) {
}
}
// TestResolveUTXOAssetID_FromSovereignGenesis covers the canonical
// behaviour the sovereign-L1 fix relies on: when the loaded platform
// genesis bakes an X-Chain, resolveUTXOAssetID returns the runtime asset
// ID encoded IN the genesis (matches what FromConfig produces, matches
// what the running X-Chain reports via platform.getStakingAssetID).
//
// Critically: NOT constants.UTXOAssetIDFor(networkID). That value is
// network-id-keyed and would silently collide between two sovereign L1s
// sharing a primary-network ID.
func TestResolveUTXOAssetID_FromSovereignGenesis(t *testing.T) {
require := require.New(t)
cfg := builder.GetConfig(constants.LocalID)
require.NotNil(cfg)
require.NotEmpty(cfg.XChainGenesis, "fixture must bake X-Chain")
genesisBytes, expectedID, err := builder.FromConfig(cfg)
require.NoError(err)
require.NotEqual(ids.Empty, expectedID)
gotID, err := resolveUTXOAssetID(constants.LocalID, genesisBytes)
require.NoError(err)
require.Equal(expectedID, gotID,
"resolveUTXOAssetID must agree with FromConfig on the genesis-derived asset ID")
}
// TestResolveUTXOAssetID_POnlyFallback verifies that when the platform
// genesis bakes no X-Chain (P-only mode), resolveUTXOAssetID falls back
// to constants.UTXOAssetIDFor(networkID). That value is unused at
// runtime (no X-Chain to mint on) but keeps the existing nodeConfig
// shape consistent for downstream consumers.
func TestResolveUTXOAssetID_POnlyFallback(t *testing.T) {
require := require.New(t)
pOnly := &pchaingenesis.Genesis{Chains: nil}
pOnlyBytes, err := pchaingenesis.Codec.Marshal(pchaintxs.CodecVersion, pOnly)
require.NoError(err)
gotID, err := resolveUTXOAssetID(42, pOnlyBytes)
require.NoError(err)
require.Equal(constants.UTXOAssetIDFor(42), gotID,
"P-only must fall through to UTXOAssetIDFor(networkID)")
}
// TestResolveUTXOAssetID_Malformed asserts that bad genesis bytes surface
// an error rather than silently returning ids.Empty (which would
// reintroduce the UTXOAssetIDFor fallback and defeat the fix on
// sovereign L1s where the fallback value is wrong).
func TestResolveUTXOAssetID_Malformed(t *testing.T) {
require := require.New(t)
_, err := resolveUTXOAssetID(1, []byte{0xff, 0xfe, 0xfd, 0xfc})
require.Error(err)
}
+34
View File
@@ -0,0 +1,34 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package config
import (
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
)
// TestDexValidatorFlagDefaultsOff pins the runtime opt-in contract for the
// D-Chain DEX: the dex-validator flag exists, defaults to false (most nodes do
// NOT run the DEX even though the DEXVM factory is always linked), and flips to
// true when set. Participation is a runtime decision, not a compile-time one.
func TestDexValidatorFlagDefaultsOff(t *testing.T) {
require := require.New(t)
// The flag must be registered in the canonical flag set.
fs := BuildFlagSet()
flag := fs.Lookup(DexValidatorKey)
require.NotNil(flag, "dex-validator flag must be registered")
require.Equal("false", flag.DefValue, "dex-validator must default to false")
// Default (unset) reads as false.
v := viper.New()
require.NoError(v.BindPFlags(fs))
require.False(v.GetBool(DexValidatorKey), "dex-validator must read false by default")
// Explicit opt-in flips it.
v.Set(DexValidatorKey, true)
require.True(v.GetBool(DexValidatorKey), "dex-validator must read true when set")
}
+17 -18
View File
@@ -13,14 +13,14 @@ import (
"github.com/spf13/pflag"
"github.com/spf13/viper"
compression "github.com/luxfi/compress"
"github.com/luxfi/constants"
"github.com/luxfi/genesis/pkg/genesis"
"github.com/luxfi/net/dynamicip"
"github.com/luxfi/node/genesis/builder"
"github.com/luxfi/node/trace"
"github.com/luxfi/node/vms/components/gas"
"github.com/luxfi/node/vms/proposervm"
compression "github.com/luxfi/compress"
"github.com/luxfi/net/dynamicip"
"github.com/luxfi/sys/ulimit"
consensusconfig "github.com/luxfi/consensus/config"
@@ -46,21 +46,21 @@ var (
defaultStakingTLSKeyPath = filepath.Join(defaultStakingPath, "staker.key")
defaultStakingCertPath = filepath.Join(defaultStakingPath, "staker.crt")
defaultStakingSignerKeyPath = filepath.Join(defaultStakingPath, "signer.key")
// Strict-PQ default paths — mirror /cli `liquid key gen`
// Strict-PQ default paths — mirror downstream-tenant CLI `<tenantctl> key gen`
// layout so the operator init container + lqd see the same files.
defaultStakingMLDSAKeyPath = filepath.Join(defaultStakingPath, "mldsa.key")
defaultStakingMLDSAPubKeyPath = filepath.Join(defaultStakingPath, "mldsa.pub")
defaultStakingMLDSAKeyPath = filepath.Join(defaultStakingPath, "mldsa.key")
defaultStakingMLDSAPubKeyPath = filepath.Join(defaultStakingPath, "mldsa.pub")
defaultHandshakeMLKEMKeyPath = filepath.Join(defaultStakingPath, "mlkem.key")
defaultHandshakeMLKEMPubKeyPath = filepath.Join(defaultStakingPath, "mlkem.pub")
defaultConfigDir = filepath.Join(defaultUnexpandedDataDir, "configs")
defaultChainConfigDir = filepath.Join(defaultConfigDir, "chains")
defaultVMConfigDir = filepath.Join(defaultConfigDir, "vms")
defaultVMAliasFilePath = filepath.Join(defaultVMConfigDir, "aliases.json")
defaultChainAliasFilePath = filepath.Join(defaultChainConfigDir, "aliases.json")
defaultNetConfigDir = filepath.Join(defaultConfigDir, "chains")
defaultPluginDir = filepath.Join(defaultUnexpandedDataDir, "plugins", "current")
defaultChainDataDir = filepath.Join(defaultUnexpandedDataDir, "chainData")
defaultProcessContextPath = filepath.Join(defaultUnexpandedDataDir, DefaultProcessContextFilename)
defaultConfigDir = filepath.Join(defaultUnexpandedDataDir, "configs")
defaultChainConfigDir = filepath.Join(defaultConfigDir, "chains")
defaultVMConfigDir = filepath.Join(defaultConfigDir, "vms")
defaultVMAliasFilePath = filepath.Join(defaultVMConfigDir, "aliases.json")
defaultChainAliasFilePath = filepath.Join(defaultChainConfigDir, "aliases.json")
defaultNetConfigDir = filepath.Join(defaultConfigDir, "chains")
defaultPluginDir = filepath.Join(defaultUnexpandedDataDir, "plugins", "current")
defaultChainDataDir = filepath.Join(defaultUnexpandedDataDir, "chainData")
defaultProcessContextPath = filepath.Join(defaultUnexpandedDataDir, DefaultProcessContextFilename)
)
func deprecateFlags(fs *pflag.FlagSet) error {
@@ -115,8 +115,6 @@ func addNodeFlags(fs *pflag.FlagSet) {
fs.String(GenesisDBKey, "", "Path to existing genesis database for replay. Cannot be used with genesis-file or genesis-file-content")
fs.String(GenesisDBTypeKey, "zapdb", "Database type to use for genesis database. Must be one of {pebbledb, zapdb}")
fs.Uint64(GenesisBlockLimitKey, 0, "Limit number of blocks to replay during genesis (0 = all blocks)")
fs.Bool(AllowCustomGenesisKey, true, "Allow custom genesis for mainnet/testnet (default: true for development, set false for production)")
fs.Bool(AllowGenesisUpdateKey, false, "Allow updating stored genesis hash when genesis changes (use for adding new primary network chains)")
// Upgrade
fs.String(UpgradeFileKey, "", fmt.Sprintf("Specifies an upgrade config file path. Ignored when running standard networks or if %s is specified",
@@ -300,7 +298,7 @@ func addNodeFlags(fs *pflag.FlagSet) {
// When the ML-DSA pubkey is provided, NodeID derivation pivots from
// the classical TLS-cert path to the wire-discriminated strict-PQ
// scheme (ids.NodeIDSchemeMLDSA65.DeriveMLDSA). Both pairs default
// to the layout /cli `liquid key gen` writes — wire the
// to the layout downstream-tenant CLI `<tenantctl> key gen` writes — wire the
// init container and lqd reads the files with zero extra config.
fs.String(StakingMLDSAKeyPathKey, defaultStakingMLDSAKeyPath, fmt.Sprintf("Path to the ML-DSA-65 staking private key (FIPS 204 PEM). Ignored if %s is specified", StakingMLDSAKeyContentKey))
fs.String(StakingMLDSAKeyContentKey, "", "Base64-encoded ML-DSA-65 staking private key (FIPS 204 PEM)")
@@ -311,7 +309,7 @@ func addNodeFlags(fs *pflag.FlagSet) {
fs.String(HandshakeMLKEMPubKeyPathKey, defaultHandshakeMLKEMPubKeyPath, fmt.Sprintf("Path to the ML-KEM-768 handshake public key (FIPS 203 PEM). Ignored if %s is specified", HandshakeMLKEMPubKeyContentKey))
fs.String(HandshakeMLKEMPubKeyContentKey, "", "Base64-encoded ML-KEM-768 handshake public key (FIPS 203 PEM)")
fs.String(StakingKMSEndpointKey, "", "KMS endpoint for staking key retrieval (e.g., https://kms.dev.lux.network)")
fs.String(StakingKMSSecretPathKey, "", "KMS secret path for staking keys (e.g., /staking/liquid-devnet/node-0)")
fs.String(StakingKMSSecretPathKey, "", "KMS secret path for staking keys (e.g., /staking/devnet/node-0)")
fs.String(StakingKMSTokenKey, "", "KMS auth token for staking key retrieval")
fs.Bool(SybilProtectionEnabledKey, true, "Enables sybil protection. If enabled, Network TLS is required")
fs.Uint64(SybilProtectionDisabledWeightKey, 100, "Weight to provide to each peer when sybil protection is disabled")
@@ -337,6 +335,7 @@ func addNodeFlags(fs *pflag.FlagSet) {
// Chain tracking
fs.String(TrackChainsKey, "", "Comma-separated list of chain IDs to track. A node tracking chains will sync those chains and track validator uptimes. Use --track-all-chains for development networks")
fs.Bool(TrackAllChainsKey, false, "If true, track all chains automatically. Useful for development and testing networks where you want to sync all deployed chains without specifying each one")
fs.Bool(DexValidatorKey, false, "If true, this node activates and participates in the D-Chain DEX: it tracks/validates the D-Chain and dials the venue matcher engine. Default off — the DEXVM factory is always linked (D-Chain is a genesis chain), but a node does NOT activate the D-Chain unless this flag is set, even under --track-all-chains. When a network configures the dex-operator NFT collection, activation also requires the node's X-Chain staking address to hold that NFT (flag AND NFT)")
// State syncing
fs.String(StateSyncIPsKey, "", "Comma separated list of state sync peer ips to connect to. Example: 127.0.0.1:9630,127.0.0.1:9631")
+37 -38
View File
@@ -20,8 +20,6 @@ const (
GenesisDBKey = "genesis-db"
GenesisDBTypeKey = "genesis-db-type"
GenesisBlockLimitKey = "genesis-block-limit"
AllowCustomGenesisKey = "allow-custom-genesis"
AllowGenesisUpdateKey = "allow-genesis-update"
UpgradeFileKey = "upgrade-file"
UpgradeFileContentKey = "upgrade-file-content"
NetworkNameKey = "network-id"
@@ -80,36 +78,36 @@ const (
HTTPReadTimeoutKey = "http-read-timeout"
HTTPReadHeaderTimeoutKey = "http-read-header-timeout"
HTTPIdleTimeoutKey = "http-idle-timeout"
StateSyncIPsKey = "state-sync-ips"
StateSyncIDsKey = "state-sync-ids"
BootstrapNodesKey = "bootstrap-nodes"
BootstrapIPsKey = "bootstrap-ips"
BootstrapIDsKey = "bootstrap-ids"
SkipBootstrapKey = "skip-bootstrap"
EnableAutominingKey = "enable-automining"
StakingHostKey = "staking-host"
StakingPortKey = "staking-port"
StakingEphemeralCertEnabledKey = "staking-ephemeral-cert-enabled"
StakingTLSKeyPathKey = "staking-tls-key-file"
StakingTLSKeyContentKey = "staking-tls-key-file-content"
StakingCertPathKey = "staking-tls-cert-file"
StakingCertContentKey = "staking-tls-cert-file-content"
StakingEphemeralSignerEnabledKey = "staking-ephemeral-signer-enabled"
StakingSignerKeyPathKey = "staking-signer-key-file"
StakingSignerKeyContentKey = "staking-signer-key-file-content"
StakingKMSEndpointKey = "staking-kms-endpoint"
StakingKMSSecretPathKey = "staking-kms-secret-path"
StakingKMSTokenKey = "staking-kms-token"
HTTPIdleTimeoutKey = "http-idle-timeout"
StateSyncIPsKey = "state-sync-ips"
StateSyncIDsKey = "state-sync-ids"
BootstrapNodesKey = "bootstrap-nodes"
BootstrapIPsKey = "bootstrap-ips"
BootstrapIDsKey = "bootstrap-ids"
SkipBootstrapKey = "skip-bootstrap"
EnableAutominingKey = "enable-automining"
StakingHostKey = "staking-host"
StakingPortKey = "staking-port"
StakingEphemeralCertEnabledKey = "staking-ephemeral-cert-enabled"
StakingTLSKeyPathKey = "staking-tls-key-file"
StakingTLSKeyContentKey = "staking-tls-key-file-content"
StakingCertPathKey = "staking-tls-cert-file"
StakingCertContentKey = "staking-tls-cert-file-content"
StakingEphemeralSignerEnabledKey = "staking-ephemeral-signer-enabled"
StakingSignerKeyPathKey = "staking-signer-key-file"
StakingSignerKeyContentKey = "staking-signer-key-file-content"
StakingKMSEndpointKey = "staking-kms-endpoint"
StakingKMSSecretPathKey = "staking-kms-secret-path"
StakingKMSTokenKey = "staking-kms-token"
// Strict-PQ staking identity (FIPS 204 ML-DSA-65). When set, the node
// uses the ML-DSA-65 public key as the NodeID source via
// ids.NodeIDSchemeMLDSA65.DeriveMLDSA(chainID, pubKey), replacing the
// classical TLS-cert NodeID derivation. Strict-PQ profiles require
// these; classical-compat chains ignore them.
StakingMLDSAKeyPathKey = "staking-mldsa-key-file"
StakingMLDSAKeyContentKey = "staking-mldsa-key-file-content"
StakingMLDSAPubKeyPathKey = "staking-mldsa-pub-key-file"
StakingMLDSAPubKeyContentKey = "staking-mldsa-pub-key-file-content"
StakingMLDSAKeyPathKey = "staking-mldsa-key-file"
StakingMLDSAKeyContentKey = "staking-mldsa-key-file-content"
StakingMLDSAPubKeyPathKey = "staking-mldsa-pub-key-file"
StakingMLDSAPubKeyContentKey = "staking-mldsa-pub-key-file-content"
// Strict-PQ handshake KEM (FIPS 203 ML-KEM-768). Peer-facing public
// key is published in the validator-set entry so peers can encapsulate
// to it for session-key establishment with no classical fallback.
@@ -177,6 +175,7 @@ const (
PartialSyncPrimaryNetworkKey = "partial-sync-primary-network"
TrackChainsKey = "track-chains"
TrackAllChainsKey = "track-all-chains"
DexValidatorKey = "dex-validator"
AdminAPIEnabledKey = "api-admin-enabled"
InfoAPIEnabledKey = "api-info-enabled"
KeystoreAPIEnabledKey = "api-keystore-enabled"
@@ -266,17 +265,17 @@ const (
ForceIgnoreChecksumKey = "force-ignore-checksum"
// Low Memory / Dev Light Mode Keys
LowMemoryKey = "low-memory"
MemoryProfileKey = "memory-profile"
DevLightKey = "dev-light"
ConfigProfileKey = "config-profile"
DBCacheSizeKey = "db-cache-size"
DBMemtableSizeKey = "db-memtable-size"
StateCacheSizeKey = "state-cache-size"
BlockCacheSizeKey = "block-cache-size"
DisableBloomFiltersKey = "disable-bloom-filters"
LazyChainLoadingKey = "lazy-chain-loading"
SingleValidatorModeKey = "single-validator-mode"
LowMemoryKey = "low-memory"
MemoryProfileKey = "memory-profile"
DevLightKey = "dev-light"
ConfigProfileKey = "config-profile"
DBCacheSizeKey = "db-cache-size"
DBMemtableSizeKey = "db-memtable-size"
StateCacheSizeKey = "state-cache-size"
BlockCacheSizeKey = "block-cache-size"
DisableBloomFiltersKey = "disable-bloom-filters"
LazyChainLoadingKey = "lazy-chain-loading"
SingleValidatorModeKey = "single-validator-mode"
// VM Transport Keys
VMTransportKey = "vm-transport"
+1 -1
View File
@@ -5,7 +5,7 @@ package config
import (
"embed"
"encoding/json"
"github.com/go-json-experiment/json"
"fmt"
"github.com/spf13/viper"
+1 -1
View File
@@ -4,7 +4,7 @@
package config
import (
"encoding/json"
"github.com/go-json-experiment/json"
"testing"
"github.com/spf13/viper"
+48 -12
View File
@@ -10,23 +10,23 @@ import (
"time"
"github.com/luxfi/ids"
"github.com/luxfi/node/server/http"
"github.com/luxfi/node/benchlist"
"github.com/luxfi/node/chains"
"github.com/luxfi/node/genesis/builder"
"github.com/luxfi/node/network"
"github.com/luxfi/node/server/http"
// "github.com/luxfi/consensus/core/router" // Unused
"github.com/luxfi/crypto/bls"
"github.com/luxfi/crypto/mldsa"
mlkemcrypto "github.com/luxfi/crypto/mlkem"
"github.com/luxfi/node/nets"
"github.com/luxfi/node/network/tracker"
"github.com/luxfi/node/upgrade"
"github.com/luxfi/node/trace"
"github.com/luxfi/node/upgrade"
// "github.com/luxfi/log" // Unused
"github.com/luxfi/math/set"
"github.com/luxfi/timer"
"github.com/luxfi/node/utils/profiler"
"github.com/luxfi/timer"
)
type APIIndexerConfig struct {
@@ -101,13 +101,13 @@ type StakingConfig struct {
// publishes in its validator-set entry so peers can encapsulate to it
// for session-key establishment with no classical fallback. The
// HandshakeMLKEMPriv stays local to the pod.
StakingMLDSA *mldsa.PrivateKey `json:"-"`
StakingMLDSAPub []byte `json:"-"`
HandshakeMLKEMPriv *mlkemcrypto.PrivateKey `json:"-"`
HandshakeMLKEMPub []byte `json:"-"`
StakingMLDSA *mldsa.PrivateKey `json:"-"`
StakingMLDSAPub []byte `json:"-"`
HandshakeMLKEMPriv *mlkemcrypto.PrivateKey `json:"-"`
HandshakeMLKEMPub []byte `json:"-"`
// File paths kept for log-line context, mirroring StakingKeyPath etc.
StakingMLDSAKeyPath string `json:"stakingMLDSAKeyPath,omitempty"`
StakingMLDSAPubPath string `json:"stakingMLDSAPubPath,omitempty"`
StakingMLDSAKeyPath string `json:"stakingMLDSAKeyPath,omitempty"`
StakingMLDSAPubPath string `json:"stakingMLDSAPubPath,omitempty"`
HandshakeMLKEMKeyPath string `json:"handshakeMLKEMKeyPath,omitempty"`
HandshakeMLKEMPubPath string `json:"handshakeMLKEMPubPath,omitempty"`
}
@@ -168,9 +168,8 @@ type Config struct {
UpgradeConfig upgrade.Config `json:"upgradeConfig"`
// Genesis information
GenesisBytes []byte `json:"-"`
XAssetID ids.ID `json:"xAssetID"`
AllowGenesisUpdate bool `json:"allowGenesisUpdate,omitempty"`
GenesisBytes []byte `json:"-"`
UTXOAssetID ids.ID `json:"utxoAssetID"`
// ID of the network this node should connect to
NetworkID uint32 `json:"networkID"`
@@ -210,6 +209,43 @@ type Config struct {
TrackedChains set.Set[ids.ID] `json:"trackedChains"`
TrackAllChains bool `json:"trackAllChains"`
// DexValidator gates whether this node PARTICIPATES in the D-Chain DEX —
// i.e. tracks/validates the D-Chain and dials the venue matcher engine.
// Default false: the DEXVM factory is always registered (the D-Chain is a
// first-class genesis chain in every build), but a node only ACTIVATES the
// D-Chain when its operator opts in. The licensed/private piece is the GPU
// venue ENGINE, never the node.
//
// Enforcement is real, not advisory: this flag flows into
// chains.ManagerConfig.DexValidator and the chain manager's
// authorizeChainActivation declines the D-Chain when it is false — even when
// --track-all-chains queued the chain. It composes AND-wise with the
// X-Chain operator-NFT gate (OptionalVMs[DexVMID].RequiredNFT joined with
// NFTAuthorizationAssets): the D-Chain activates only if DexValidator is
// true AND (when a network configures the dex-operator collection) the
// node's staking X-address holds that NFT.
//
// Phase-2 (NOT built here — see participatesInDEXChain in node/node.go and
// the StakingXAddress seam in chains.ManagerConfig): binding the NFT check
// to proof-of-possession of the staking key, and mDNS local-fiber geofence
// discovery among the HFT DEX cluster. Until a network configures the
// dex-operator collection the NFT half is a no-op and this flag alone gates.
DexValidator bool `json:"dexValidator"`
// NFTAuthorizationAssets supplies, per network, the X-Chain operator-NFT
// collection asset for each NFT-gated optional VM, keyed by VMID (e.g.
// DexVMID -> the dex-operator collection asset; BridgeVMID -> the
// bridge-operator collection asset). It is the per-network VALUE half of the
// activation gate; the POLICY half (which VMs need an NFT + which group)
// lives in node.OptionalVMs. node.chainAuthorizationsFor joins the two into
// the chain manager's ChainAuthorizations map.
//
// Empty by default: minting the real operator collections is a follow-up, so
// until a network sets an asset here the corresponding chain is ungated
// (today's opt-in-flag behavior) while the gate itself stays fail-closed for
// any configured-but-unheld NFT.
NFTAuthorizationAssets map[ids.ID]ids.ID `json:"nftAuthorizationAssets"`
NetConfigs map[ids.ID]nets.Config `json:"chainConfigs"`
ChainConfigs map[string]chains.ChainConfig `json:"-"`
+3 -2
View File
@@ -4,7 +4,8 @@
package node
import (
"encoding/json"
"github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"net/netip"
"testing"
@@ -54,7 +55,7 @@ func TestProcessContext(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)
contextJSON, err := json.MarshalIndent(test.context, "", "\t")
contextJSON, err := json.Marshal(test.context, jsontext.WithIndent("\t"))
require.NoError(err)
require.JSONEq(test.expected, string(contextJSON))
})
-7
View File
@@ -131,13 +131,6 @@ func allFlags() []FlagSpec {
Description: "Limit number of blocks to replay during genesis (0 = all blocks)",
Category: CategoryGenesis,
},
{
Key: "allow-custom-genesis",
Type: TypeBool,
Default: true,
Description: "Allow custom genesis for mainnet/testnet (default: true for development, set false for production)",
Category: CategoryGenesis,
},
{
Key: "upgrade-file",
Type: TypeString,
+4 -2
View File
@@ -7,7 +7,9 @@
package spec
import (
"encoding/json"
"github.com/go-json-experiment/json"
jsonv1 "github.com/go-json-experiment/json/v1"
"github.com/go-json-experiment/json/jsontext"
"time"
)
@@ -207,7 +209,7 @@ func (s *ConfigSpec) DeprecatedFlags() []FlagSpec {
// JSON returns the spec as formatted JSON.
func (s *ConfigSpec) JSON() ([]byte, error) {
return json.MarshalIndent(s, "", " ")
return json.Marshal(s, jsontext.WithIndent(" "), jsonv1.FormatDurationAsNano(true))
}
// ValidateValue checks if a value is valid for a flag.
+1 -1
View File
@@ -4,7 +4,7 @@
package spec
import (
"encoding/json"
"github.com/go-json-experiment/json"
"testing"
)
+131
View File
@@ -0,0 +1,131 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package config
import (
"crypto/rand"
"encoding/base64"
"encoding/pem"
"os"
"path/filepath"
"testing"
"github.com/luxfi/crypto/mldsa"
"github.com/stretchr/testify/require"
)
// writeMLDSAFixture generates an ML-DSA-65 keypair, PEM-encodes both halves
// with the block types loadStakingMLDSA expects, writes them under dir, and
// returns (privPath, pubPath, privPEM, pubPEM).
func writeMLDSAFixture(t *testing.T, dir string) (privPath, pubPath string, privPEM, pubPEM []byte) {
t.Helper()
priv, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
require.NoError(t, err)
privPEM = pem.EncodeToMemory(&pem.Block{Type: "ML-DSA-65 PRIVATE KEY", Bytes: priv.Bytes()})
pubPEM = pem.EncodeToMemory(&pem.Block{Type: "ML-DSA-65 PUBLIC KEY", Bytes: priv.PublicKey.Bytes()})
require.NoError(t, os.MkdirAll(dir, 0o755))
privPath = filepath.Join(dir, "mldsa.key")
pubPath = filepath.Join(dir, "mldsa.pub")
require.NoError(t, os.WriteFile(privPath, privPEM, 0o600))
require.NoError(t, os.WriteFile(pubPath, pubPEM, 0o644))
return privPath, pubPath, privPEM, pubPEM
}
// TestLoadStakingMLDSA_PathForm: both keys via explicit --staking-mldsa-*-file
// paths. The canonical strict-PQ deploy form.
func TestLoadStakingMLDSA_PathForm(t *testing.T) {
dir := t.TempDir()
privPath, pubPath, _, _ := writeMLDSAFixture(t, dir)
v, err := BuildViper(BuildFlagSet(), []string{
"--" + StakingMLDSAKeyPathKey + "=" + privPath,
"--" + StakingMLDSAPubKeyPathKey + "=" + pubPath,
})
require.NoError(t, err)
priv, pub, _, _, err := loadStakingMLDSA(v)
require.NoError(t, err)
require.NotNil(t, priv)
require.NotEmpty(t, pub)
}
// TestLoadStakingMLDSA_ContentForm: keys via base64 PEM --*-content flags.
func TestLoadStakingMLDSA_ContentForm(t *testing.T) {
dir := t.TempDir()
_, _, privPEM, pubPEM := writeMLDSAFixture(t, dir)
v, err := BuildViper(BuildFlagSet(), []string{
"--" + StakingMLDSAKeyContentKey + "=" + base64.StdEncoding.EncodeToString(privPEM),
"--" + StakingMLDSAPubKeyContentKey + "=" + base64.StdEncoding.EncodeToString(pubPEM),
})
require.NoError(t, err)
priv, pub, _, _, err := loadStakingMLDSA(v)
require.NoError(t, err)
require.NotNil(t, priv)
require.NotEmpty(t, pub)
}
// TestLoadStakingMLDSA_EmptyContentFallsThroughToPath is the regression guard
// for the strict-PQ activation outage: when a deploy passes the *-content flag
// blank (e.g. an env/template that rendered to "") alongside a valid *-file
// path, the loader MUST consult the path. The previous behavior short-circuited
// on the empty content value and returned no key, so StakingMLDSAPub stayed
// empty, IsStrictPQ() was false, and the node booted under an ECDSA NodeID with
// no error — silently degrading a strict-PQ validator to classical-compat.
func TestLoadStakingMLDSA_EmptyContentFallsThroughToPath(t *testing.T) {
dir := t.TempDir()
privPath, pubPath, _, _ := writeMLDSAFixture(t, dir)
v, err := BuildViper(BuildFlagSet(), []string{
"--" + StakingMLDSAKeyPathKey + "=" + privPath,
"--" + StakingMLDSAKeyContentKey + "=", // blank content
"--" + StakingMLDSAPubKeyPathKey + "=" + pubPath,
"--" + StakingMLDSAPubKeyContentKey + "=", // blank content
})
require.NoError(t, err)
priv, pub, gotPrivPath, gotPubPath, err := loadStakingMLDSA(v)
require.NoError(t, err)
require.NotNil(t, priv, "blank content must fall through to the path")
require.NotEmpty(t, pub, "blank content must fall through to the path")
require.Equal(t, privPath, gotPrivPath)
require.Equal(t, pubPath, gotPubPath)
}
// TestLoadStakingMLDSA_NeitherIsClassicalCompat: no key material at all (and a
// default pub path that does not exist) is classical-compat — empty, no error.
func TestLoadStakingMLDSA_NeitherIsClassicalCompat(t *testing.T) {
// Point DataDir at an empty dir so the default pub path resolves to a
// non-existent file rather than picking up a developer's ~/.lux key.
v, err := BuildViper(BuildFlagSet(), []string{
"--" + DataDirKey + "=" + t.TempDir(),
})
require.NoError(t, err)
priv, pub, _, _, err := loadStakingMLDSA(v)
require.NoError(t, err)
require.Nil(t, priv)
require.Empty(t, pub)
}
// TestLoadStakingMLDSA_PrivOnlyIsFatal: a private key with no public key is a
// loud config error — a strict-PQ validator must never silently degrade.
func TestLoadStakingMLDSA_PrivOnlyIsFatal(t *testing.T) {
base := t.TempDir()
_, _, privPEM, _ := writeMLDSAFixture(t, filepath.Join(base, "staking"))
v, err := BuildViper(BuildFlagSet(), []string{
"--" + DataDirKey + "=" + base, // default pub path under here will be missing... but priv content wins
"--" + StakingMLDSAKeyContentKey + "=" + base64.StdEncoding.EncodeToString(privPEM),
"--" + StakingMLDSAPubKeyPathKey + "=" + filepath.Join(base, "does-not-exist.pub"),
})
require.NoError(t, err)
_, _, _, _, err = loadStakingMLDSA(v)
require.Error(t, err)
require.Contains(t, err.Error(), "both private and public key are required")
}
-8
View File
@@ -1,8 +0,0 @@
version: v1
plugins:
- name: go
out: pb
opt: paths=source_relative
- plugin: connect-go
out: pb
opt: paths=source_relative
-7
View File
@@ -1,7 +0,0 @@
# Generated by buf. DO NOT EDIT.
version: v1
deps:
- remote: buf.build
owner: metric
repository: client-model
commit: 1d56a02d481a412a83b3c4984eb90c2e
-27
View File
@@ -1,27 +0,0 @@
version: v1
name: buf.build/luxfi/lux
build:
excludes:
# for golang we handle metric as a buf dep so we exclude it from generate, this proto
# file is required by languages such as rust.
- io/metric
breaking:
use:
- FILE
deps:
- buf.build/metric/client-model
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
# 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
-288
View File
@@ -1,288 +0,0 @@
//go:build grpc
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.35.1
// protoc (unknown)
// source: xsvm/service.proto
package xsvm
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type PingRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
}
func (x *PingRequest) Reset() {
*x = PingRequest{}
mi := &file_xsvm_service_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *PingRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*PingRequest) ProtoMessage() {}
func (x *PingRequest) ProtoReflect() protoreflect.Message {
mi := &file_xsvm_service_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use PingRequest.ProtoReflect.Descriptor instead.
func (*PingRequest) Descriptor() ([]byte, []int) {
return file_xsvm_service_proto_rawDescGZIP(), []int{0}
}
func (x *PingRequest) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
type PingReply struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
}
func (x *PingReply) Reset() {
*x = PingReply{}
mi := &file_xsvm_service_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *PingReply) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*PingReply) ProtoMessage() {}
func (x *PingReply) ProtoReflect() protoreflect.Message {
mi := &file_xsvm_service_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use PingReply.ProtoReflect.Descriptor instead.
func (*PingReply) Descriptor() ([]byte, []int) {
return file_xsvm_service_proto_rawDescGZIP(), []int{1}
}
func (x *PingReply) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
type StreamPingRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
}
func (x *StreamPingRequest) Reset() {
*x = StreamPingRequest{}
mi := &file_xsvm_service_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *StreamPingRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*StreamPingRequest) ProtoMessage() {}
func (x *StreamPingRequest) ProtoReflect() protoreflect.Message {
mi := &file_xsvm_service_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use StreamPingRequest.ProtoReflect.Descriptor instead.
func (*StreamPingRequest) Descriptor() ([]byte, []int) {
return file_xsvm_service_proto_rawDescGZIP(), []int{2}
}
func (x *StreamPingRequest) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
type StreamPingReply struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
}
func (x *StreamPingReply) Reset() {
*x = StreamPingReply{}
mi := &file_xsvm_service_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *StreamPingReply) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*StreamPingReply) ProtoMessage() {}
func (x *StreamPingReply) ProtoReflect() protoreflect.Message {
mi := &file_xsvm_service_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use StreamPingReply.ProtoReflect.Descriptor instead.
func (*StreamPingReply) Descriptor() ([]byte, []int) {
return file_xsvm_service_proto_rawDescGZIP(), []int{3}
}
func (x *StreamPingReply) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
var File_xsvm_service_proto protoreflect.FileDescriptor
var file_xsvm_service_proto_rawDesc = []byte{
0x0a, 0x12, 0x78, 0x73, 0x76, 0x6d, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x12, 0x04, 0x78, 0x73, 0x76, 0x6d, 0x22, 0x27, 0x0a, 0x0b, 0x50, 0x69,
0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73,
0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73,
0x61, 0x67, 0x65, 0x22, 0x25, 0x0a, 0x09, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x70, 0x6c, 0x79,
0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x2d, 0x0a, 0x11, 0x53, 0x74,
0x72, 0x65, 0x61, 0x6d, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x2b, 0x0a, 0x0f, 0x53, 0x74, 0x72,
0x65, 0x61, 0x6d, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x18, 0x0a, 0x07,
0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d,
0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0x74, 0x0a, 0x04, 0x50, 0x69, 0x6e, 0x67, 0x12, 0x2a,
0x0a, 0x04, 0x50, 0x69, 0x6e, 0x67, 0x12, 0x11, 0x2e, 0x78, 0x73, 0x76, 0x6d, 0x2e, 0x50, 0x69,
0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e, 0x78, 0x73, 0x76, 0x6d,
0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x40, 0x0a, 0x0a, 0x53, 0x74,
0x72, 0x65, 0x61, 0x6d, 0x50, 0x69, 0x6e, 0x67, 0x12, 0x17, 0x2e, 0x78, 0x73, 0x76, 0x6d, 0x2e,
0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x15, 0x2e, 0x78, 0x73, 0x76, 0x6d, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50,
0x69, 0x6e, 0x67, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x28, 0x01, 0x30, 0x01, 0x42, 0x2c, 0x5a, 0x2a,
0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x75, 0x78, 0x66, 0x69,
0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x2f, 0x70, 0x62, 0x2f, 0x78, 0x73, 0x76, 0x6d, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x33,
}
var (
file_xsvm_service_proto_rawDescOnce sync.Once
file_xsvm_service_proto_rawDescData = file_xsvm_service_proto_rawDesc
)
func file_xsvm_service_proto_rawDescGZIP() []byte {
file_xsvm_service_proto_rawDescOnce.Do(func() {
file_xsvm_service_proto_rawDescData = protoimpl.X.CompressGZIP(file_xsvm_service_proto_rawDescData)
})
return file_xsvm_service_proto_rawDescData
}
var file_xsvm_service_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_xsvm_service_proto_goTypes = []any{
(*PingRequest)(nil), // 0: xsvm.PingRequest
(*PingReply)(nil), // 1: xsvm.PingReply
(*StreamPingRequest)(nil), // 2: xsvm.StreamPingRequest
(*StreamPingReply)(nil), // 3: xsvm.StreamPingReply
}
var file_xsvm_service_proto_depIdxs = []int32{
0, // 0: xsvm.Ping.Ping:input_type -> xsvm.PingRequest
2, // 1: xsvm.Ping.StreamPing:input_type -> xsvm.StreamPingRequest
1, // 2: xsvm.Ping.Ping:output_type -> xsvm.PingReply
3, // 3: xsvm.Ping.StreamPing:output_type -> xsvm.StreamPingReply
2, // [2:4] is the sub-list for method output_type
0, // [0:2] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_xsvm_service_proto_init() }
func file_xsvm_service_proto_init() {
if File_xsvm_service_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_xsvm_service_proto_rawDesc,
NumEnums: 0,
NumMessages: 4,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_xsvm_service_proto_goTypes,
DependencyIndexes: file_xsvm_service_proto_depIdxs,
MessageInfos: file_xsvm_service_proto_msgTypes,
}.Build()
File_xsvm_service_proto = out.File
file_xsvm_service_proto_rawDesc = nil
file_xsvm_service_proto_goTypes = nil
file_xsvm_service_proto_depIdxs = nil
}
@@ -1,138 +0,0 @@
//go:build grpc
// Code generated by protoc-gen-connect-go. DO NOT EDIT.
//
// Source: xsvm/service.proto
package xsvmconnect
import (
connect "connectrpc.com/connect"
context "context"
errors "errors"
xsvm "github.com/luxfi/node/connectproto/pb/xsvm"
http "net/http"
strings "strings"
)
// This is a compile-time assertion to ensure that this generated file and the connect package are
// compatible. If you get a compiler error that this constant is not defined, this code was
// generated with a version of connect newer than the one compiled into your binary. You can fix the
// problem by either regenerating this code with an older version of connect or updating the connect
// version compiled into your binary.
const _ = connect.IsAtLeastVersion1_13_0
const (
// PingName is the fully-qualified name of the Ping service.
PingName = "xsvm.Ping"
)
// These constants are the fully-qualified names of the RPCs defined in this package. They're
// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.
//
// Note that these are different from the fully-qualified method names used by
// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
// reflection-formatted method names, remove the leading slash and convert the remaining slash to a
// period.
const (
// PingPingProcedure is the fully-qualified name of the Ping's Ping RPC.
PingPingProcedure = "/xsvm.Ping/Ping"
// PingStreamPingProcedure is the fully-qualified name of the Ping's StreamPing RPC.
PingStreamPingProcedure = "/xsvm.Ping/StreamPing"
)
// PingClient is a client for the xsvm.Ping service.
type PingClient interface {
Ping(context.Context, *connect.Request[xsvm.PingRequest]) (*connect.Response[xsvm.PingReply], error)
StreamPing(context.Context) *connect.BidiStreamForClient[xsvm.StreamPingRequest, xsvm.StreamPingReply]
}
// NewPingClient constructs a client for the xsvm.Ping service. By default, it uses the Connect
// protocol with the binary Protobuf Codec, asks for gzipped responses, and sends uncompressed
// requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or
// connect.WithGRPCWeb() options.
//
// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
// http://api.acme.com or https://acme.com/grpc).
func NewPingClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) PingClient {
baseURL = strings.TrimRight(baseURL, "/")
pingMethods := xsvm.File_xsvm_service_proto.Services().ByName("Ping").Methods()
return &pingClient{
ping: connect.NewClient[xsvm.PingRequest, xsvm.PingReply](
httpClient,
baseURL+PingPingProcedure,
connect.WithSchema(pingMethods.ByName("Ping")),
connect.WithClientOptions(opts...),
),
streamPing: connect.NewClient[xsvm.StreamPingRequest, xsvm.StreamPingReply](
httpClient,
baseURL+PingStreamPingProcedure,
connect.WithSchema(pingMethods.ByName("StreamPing")),
connect.WithClientOptions(opts...),
),
}
}
// pingClient implements PingClient.
type pingClient struct {
ping *connect.Client[xsvm.PingRequest, xsvm.PingReply]
streamPing *connect.Client[xsvm.StreamPingRequest, xsvm.StreamPingReply]
}
// Ping calls xsvm.Ping.Ping.
func (c *pingClient) Ping(ctx context.Context, req *connect.Request[xsvm.PingRequest]) (*connect.Response[xsvm.PingReply], error) {
return c.ping.CallUnary(ctx, req)
}
// StreamPing calls xsvm.Ping.StreamPing.
func (c *pingClient) StreamPing(ctx context.Context) *connect.BidiStreamForClient[xsvm.StreamPingRequest, xsvm.StreamPingReply] {
return c.streamPing.CallBidiStream(ctx)
}
// PingHandler is an implementation of the xsvm.Ping service.
type PingHandler interface {
Ping(context.Context, *connect.Request[xsvm.PingRequest]) (*connect.Response[xsvm.PingReply], error)
StreamPing(context.Context, *connect.BidiStream[xsvm.StreamPingRequest, xsvm.StreamPingReply]) error
}
// NewPingHandler builds an HTTP handler from the service implementation. It returns the path on
// which to mount the handler and the handler itself.
//
// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
// and JSON codecs. They also support gzip compression.
func NewPingHandler(svc PingHandler, opts ...connect.HandlerOption) (string, http.Handler) {
pingMethods := xsvm.File_xsvm_service_proto.Services().ByName("Ping").Methods()
pingPingHandler := connect.NewUnaryHandler(
PingPingProcedure,
svc.Ping,
connect.WithSchema(pingMethods.ByName("Ping")),
connect.WithHandlerOptions(opts...),
)
pingStreamPingHandler := connect.NewBidiStreamHandler(
PingStreamPingProcedure,
svc.StreamPing,
connect.WithSchema(pingMethods.ByName("StreamPing")),
connect.WithHandlerOptions(opts...),
)
return "/xsvm.Ping/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case PingPingProcedure:
pingPingHandler.ServeHTTP(w, r)
case PingStreamPingProcedure:
pingStreamPingHandler.ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
})
}
// UnimplementedPingHandler returns CodeUnimplemented from all methods.
type UnimplementedPingHandler struct{}
func (UnimplementedPingHandler) Ping(context.Context, *connect.Request[xsvm.PingRequest]) (*connect.Response[xsvm.PingReply], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("xsvm.Ping.Ping is not implemented"))
}
func (UnimplementedPingHandler) StreamPing(context.Context, *connect.BidiStream[xsvm.StreamPingRequest, xsvm.StreamPingReply]) error {
return connect.NewError(connect.CodeUnimplemented, errors.New("xsvm.Ping.StreamPing is not implemented"))
}
-26
View File
@@ -1,26 +0,0 @@
syntax = "proto3";
package xsvm;
option go_package = "github.com/luxfi/node/connectproto/pb/xsvm";
service Ping {
rpc Ping(PingRequest) returns (PingReply);
rpc StreamPing(stream StreamPingRequest) returns (stream StreamPingReply);
}
message PingRequest {
string message = 1;
}
message PingReply {
string message = 1;
}
message StreamPingRequest {
string message = 1;
}
message StreamPingReply {
string message = 1;
}
+48
View File
@@ -0,0 +1,48 @@
# Consensus Package - AI Assistant Guide
This package is node-side glue around the canonical consensus engine in
`github.com/luxfi/consensus`. It does NOT implement the protocol — the
protocol lives in that module.
## Package Structure
- `acceptor.go` - Node-side block-acceptance callbacks (chain-ID-keyed)
- `quasar/` - Node-side wiring around `consensus/protocol/quasar`
- `zap/` - ZAP agentic-consensus / DID bridge (self-contained, opt-in)
## Acceptor
`Acceptor` interface called when consensus accepts a block / vertex.
`AcceptorGroup` manages multiple acceptors per chain — used by the indexer
and warp IPC. The variant here differs from the canonical
`luxfi/consensus/core.Acceptor` (different signature: `*runtime.Runtime`
instead of `context.Context`, plus chain-ID-keyed registration).
## Quasar Wiring
The `quasar/` subpackage wraps `github.com/luxfi/consensus/protocol/quasar`
with node-specific adapters (P-Chain validator state, BLS signing keys,
Corona threshold coordinator stub).
### Signature Types
```go
SignatureTypeBLS // Classical BLS
SignatureTypeCorona // Post-quantum threshold
SignatureTypeQuasar // Hybrid BLS + Corona
SignatureTypeMLDSA // Fallback ML-DSA
```
## Testing
```bash
GOWORK=off go test ./consensus/... -v
```
## Dependencies
- `github.com/luxfi/consensus` - Canonical consensus protocol (engine,
protocol/quasar, types, etc.)
- `github.com/luxfi/ids` - ID types
- `github.com/luxfi/log` - Logging
- `github.com/luxfi/runtime` - Runtime context for acceptors
+1 -1
View File
@@ -9,9 +9,9 @@ import (
"go.uber.org/zap"
"github.com/luxfi/runtime"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/runtime"
)
var (
+73 -38
View File
@@ -11,6 +11,8 @@ import (
"time"
"github.com/luxfi/accel"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/crypto/mldsa"
)
// GPUVerifyPipeline fuses multiple cryptographic verification operations into
@@ -59,41 +61,41 @@ type ZKWork struct {
Bases [][]byte // [M, N, point_size]
}
// MLDSAWork holds a batch of ML-DSA (Dilithium) signatures to verify.
// MLDSAWork holds a batch of ML-DSA signatures to verify.
type MLDSAWork struct {
Messages [][]byte // [N, msg_len]
Signatures [][]byte // [N, 3293] Dilithium3
PubKeys [][]byte // [N, 1952] Dilithium3
Signatures [][]byte // [N, 3309] FIPS-204 ML-DSA-65 (3293 was the stale round-3 Dilithium3 size)
PubKeys [][]byte // [N, 1952] FIPS-204 ML-DSA-65
}
// BlockVerifyWork contains all verification batches for a single block.
type BlockVerifyWork struct {
BLS *BLSWork
BLS *BLSWork
Corona *CoronaWork
ZK *ZKWork
MLDSA *MLDSAWork
ZK *ZKWork
MLDSA *MLDSAWork
}
// BlockVerifyResult contains verification results for all batch types.
type BlockVerifyResult struct {
BLSValid []bool
BLSValid []bool
CoronaValid []bool
ZKValid bool
MLDSAValid []bool
ZKValid bool
MLDSAValid []bool
GPUUsed bool
BLSTime time.Duration
GPUUsed bool
BLSTime time.Duration
CoronaTime time.Duration
ZKTime time.Duration
MLDSATime time.Duration
TotalTime time.Duration
ZKTime time.Duration
MLDSATime time.Duration
TotalTime time.Duration
}
var (
ErrBLSSizeMismatch = errors.New("BLS batch size mismatch: messages, signatures, and pubkeys must have equal length")
ErrBLSSizeMismatch = errors.New("BLS batch size mismatch: messages, signatures, and pubkeys must have equal length")
ErrCoronaSizeMismatch = errors.New("Corona batch size mismatch: messages, signatures, and pubkeys must have equal length")
ErrZKSizeMismatch = errors.New("ZK batch size mismatch: scalars and bases must have equal length")
ErrMLDSASizeMismatch = errors.New("ML-DSA batch size mismatch: messages, signatures, and pubkeys must have equal length")
ErrZKSizeMismatch = errors.New("ZK batch size mismatch: scalars and bases must have equal length")
ErrMLDSASizeMismatch = errors.New("ML-DSA batch size mismatch: messages, signatures, and pubkeys must have equal length")
)
// VerifyBlock dispatches all verification work for a block through the GPU pipeline.
@@ -376,8 +378,8 @@ func gpuMLDSAVerify(sess *accel.Session, work *MLDSAWork) ([]bool, error) {
n := len(work.Messages)
msgLen := maxByteLen(work.Messages)
sigLen := 3293 // Dilithium3 signature
pkLen := 1952 // Dilithium3 public key
sigLen := 3309 // FIPS-204 ML-DSA-65 signature (3293 was the stale round-3 Dilithium3 size)
pkLen := 1952 // FIPS-204 ML-DSA-65 public key (unchanged across the round-3 -> final transition)
msgFlat := flattenPadded(work.Messages, n, msgLen)
sigFlat := flattenPadded(work.Signatures, n, sigLen)
@@ -485,42 +487,75 @@ func (p *GPUVerifyPipeline) verifyCPU(work *BlockVerifyWork) *BlockVerifyResult
return result
}
// CPU fallback implementations.
// These verify signatures using pure Go. In a non-CGO build without real
// crypto libraries for BLS/Dilithium, we validate format and return true
// for well-formed inputs (actual verification would use luxfi/crypto).
// CPU fallback implementations — the pure-Go correctness oracle.
//
// These perform REAL per-element cryptographic verification using the
// luxfi/crypto pure-Go primitives (CGO_ENABLED=0-clean). They interpret each
// element's raw bytes with the SAME layout the GPU kernels use (see
// gpuBLSVerify / gpuMLDSAVerify), so the CPU and GPU paths are a genuine
// equivalence pair: a no-GPU node accepts exactly the signatures a GPU node
// accepts, and never rubber-stamps a forged one.
//
// Corona and ZK have no pure-Go verifier in luxfi/crypto, so those paths
// fail closed (return false) rather than format-check-and-accept. They MUST
// be wired to a real verifier before block-accept depends on them.
func cpuBLSVerify(work *BLSWork) []bool {
valid := make([]bool, len(work.Messages))
for i := range work.Messages {
// Format check: message present, sig is 96 bytes, pk is 48 bytes
valid[i] = len(work.Messages[i]) > 0 &&
len(work.Signatures[i]) == 96 &&
len(work.PubKeys[i]) == 48
// Mirror gpuBLSVerify's layout: pk is a 48-byte compressed G1 point,
// sig is a 96-byte compressed G2 point, msg is the raw message.
// PublicKeyFromCompressedBytes / SignatureFromBytes enforce the exact
// length, on-curve and subgroup membership the blst/CGO path enforces;
// any malformed input fails closed (constructor error => false).
pk, err := bls.PublicKeyFromCompressedBytes(work.PubKeys[i])
if err != nil {
continue
}
sig, err := bls.SignatureFromBytes(work.Signatures[i])
if err != nil {
continue
}
valid[i] = bls.Verify(pk, sig, work.Messages[i])
}
return valid
}
func cpuCoronaVerify(work *CoronaWork) []bool {
valid := make([]bool, len(work.Messages))
for i := range work.Messages {
valid[i] = len(work.Messages[i]) > 0 &&
len(work.Signatures[i]) > 0 &&
len(work.PubKeys[i]) > 0
}
return valid
// FAIL CLOSED: luxfi/crypto exposes no pure-Go Corona (lattice threshold)
// signature verifier, and the GPU Corona kernel is the known-wrong-prime
// BLOCKED kernel. There is no correct way to verify a Corona signature on
// the CPU here, so every element is rejected. Never return true for an
// unverified signature. Wire a real Corona verifier before block-accept
// consumes this result.
return make([]bool, len(work.Messages))
}
func cpuZKVerify(work *ZKWork) bool {
return len(work.Scalars) > 0 && len(work.Bases) > 0
// FAIL CLOSED: luxfi/crypto exposes no standalone pure-Go ZK proof
// verifier (the accel MSM path is a GPU primitive, not a proof check), so
// CPU verification cannot establish proof validity. Reject rather than
// rubber-stamp. Wire a real ZK verifier before block-accept consumes this.
return false
}
func cpuMLDSAVerify(work *MLDSAWork) []bool {
valid := make([]bool, len(work.Messages))
for i := range work.Messages {
valid[i] = len(work.Messages[i]) > 0 &&
len(work.Signatures[i]) == 3293 &&
len(work.PubKeys[i]) == 1952
// Mirror gpuMLDSAVerify's layout: ML-DSA-65, pk 1952 bytes, msg raw.
// PublicKeyFromBytes enforces the exact key length and decodes the
// point; VerifySignature uses the FIPS 204 nil-context verify,
// matching the kernel's contextless per-tx verification, and accepts
// the signature at its true FIPS-204 length (3309 bytes). The GPU
// flatten width (gpuMLDSAVerify's sigLen) now agrees at 3309, so the
// CPU oracle and GPU path size the signature identically; see
// TestMLDSA_WorkStructSizeIsCanonical. Malformed pk fails closed
// (constructor error).
pub, err := mldsa.PublicKeyFromBytes(work.PubKeys[i], mldsa.MLDSA65)
if err != nil {
continue
}
valid[i] = pub.VerifySignature(work.Messages[i], work.Signatures[i])
}
return valid
}
+173 -27
View File
@@ -7,6 +7,8 @@ import (
"crypto/rand"
"testing"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/crypto/mldsa"
"github.com/stretchr/testify/require"
)
@@ -17,17 +19,54 @@ func makeRandomBytes(n int) []byte {
return b
}
// makeBLSWork creates BLSWork with n entries using correct BLS sizes.
func makeBLSWork(n int) *BLSWork {
// makeValidBLSEntry returns (msg, sig, pk) for a REAL BLS signature over a
// random 32-byte message: pk is the 48-byte compressed G1 key, sig is the
// 96-byte compressed G2 signature. The CPU oracle must accept this.
func makeValidBLSEntry(t testing.TB) (msg, sig, pk []byte) {
t.Helper()
sk, err := bls.NewSecretKey()
require.NoError(t, err)
msg = makeRandomBytes(32)
s, err := sk.Sign(msg)
require.NoError(t, err)
sig = bls.SignatureToBytes(s)
pk = bls.PublicKeyToCompressedBytes(sk.PublicKey())
require.Len(t, sig, 96)
require.Len(t, pk, 48)
return msg, sig, pk
}
// makeValidMLDSAEntry returns (msg, sig, pk) for a REAL ML-DSA-65 signature
// over a random 64-byte message. The sizes are taken from the crypto package
// constants (FIPS-204 ML-DSA-65: pk 1952 bytes, sig 3309 bytes) rather than
// hard-coded — see TestMLDSA_WorkStructSizeIsCanonical, which holds the
// MLDSAWork struct / gpuMLDSAVerify sig constant pinned at the FIPS-204 3309
// (corrected from the stale round-3 Dilithium3 3293). The CPU oracle uses the
// typed Verify, so it accepts the real signature regardless.
func makeValidMLDSAEntry(t testing.TB) (msg, sig, pk []byte) {
t.Helper()
priv, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
require.NoError(t, err)
msg = makeRandomBytes(64)
sig, err = priv.Sign(rand.Reader, msg, nil)
require.NoError(t, err)
pk = priv.PublicKey.Bytes()
require.Len(t, sig, mldsa.MLDSA65SignatureSize)
require.Len(t, pk, mldsa.MLDSA65PublicKeySize)
return msg, sig, pk
}
// makeBLSWork creates BLSWork with n entries carrying REAL valid BLS
// signatures (the CPU oracle now performs real verification).
func makeBLSWork(t testing.TB, n int) *BLSWork {
t.Helper()
w := &BLSWork{
Messages: make([][]byte, n),
Signatures: make([][]byte, n),
PubKeys: make([][]byte, n),
}
for i := 0; i < n; i++ {
w.Messages[i] = makeRandomBytes(32)
w.Signatures[i] = makeRandomBytes(96) // BLS G2 point
w.PubKeys[i] = makeRandomBytes(48) // BLS G1 point
w.Messages[i], w.Signatures[i], w.PubKeys[i] = makeValidBLSEntry(t)
}
return w
}
@@ -60,17 +99,18 @@ func makeZKWork(m int) *ZKWork {
return w
}
// makeMLDSAWork creates MLDSAWork with n entries using correct Dilithium3 sizes.
func makeMLDSAWork(n int) *MLDSAWork {
// makeMLDSAWork creates MLDSAWork with n entries carrying REAL valid
// ML-DSA-65 (Dilithium3) signatures (the CPU oracle now performs real
// verification).
func makeMLDSAWork(t testing.TB, n int) *MLDSAWork {
t.Helper()
w := &MLDSAWork{
Messages: make([][]byte, n),
Signatures: make([][]byte, n),
PubKeys: make([][]byte, n),
}
for i := 0; i < n; i++ {
w.Messages[i] = makeRandomBytes(64)
w.Signatures[i] = makeRandomBytes(3293) // Dilithium3 signature
w.PubKeys[i] = makeRandomBytes(1952) // Dilithium3 public key
w.Messages[i], w.Signatures[i], w.PubKeys[i] = makeValidMLDSAEntry(t)
}
return w
}
@@ -79,32 +119,33 @@ func TestGPUPipeline_AllFourTypes(t *testing.T) {
pipeline := NewGPUVerifyPipeline()
work := &BlockVerifyWork{
BLS: makeBLSWork(5),
BLS: makeBLSWork(t, 5),
Corona: makeCoronaWork(3),
ZK: makeZKWork(2),
MLDSA: makeMLDSAWork(10),
ZK: makeZKWork(2),
MLDSA: makeMLDSAWork(t, 10),
}
result, err := pipeline.VerifyBlock(work)
require.NoError(t, err)
require.NotNil(t, result)
// BLS results
// BLS results: real valid signatures, CPU oracle accepts.
require.Len(t, result.BLSValid, 5, "should have 5 BLS results")
for i, v := range result.BLSValid {
require.True(t, v, "BLS[%d] should be valid", i)
}
// Corona results
// Corona results: no pure-Go Corona verifier exists, so the CPU oracle
// fails closed — every element is rejected (never rubber-stamped).
require.Len(t, result.CoronaValid, 3, "should have 3 Corona results")
for i, v := range result.CoronaValid {
require.True(t, v, "Corona[%d] should be valid", i)
require.False(t, v, "Corona[%d] must fail closed (no pure-Go verifier)", i)
}
// ZK result
require.True(t, result.ZKValid, "ZK batch should be valid")
// ZK result: no pure-Go ZK verifier exists, so the CPU oracle fails closed.
require.False(t, result.ZKValid, "ZK batch must fail closed (no pure-Go verifier)")
// ML-DSA results
// ML-DSA results: real valid signatures, CPU oracle accepts.
require.Len(t, result.MLDSAValid, 10, "should have 10 ML-DSA results")
for i, v := range result.MLDSAValid {
require.True(t, v, "MLDSA[%d] should be valid", i)
@@ -129,15 +170,15 @@ func TestGPUPipeline_CPUFallback(t *testing.T) {
pipeline := NewGPUVerifyPipeline()
work := &BlockVerifyWork{
BLS: makeBLSWork(3),
MLDSA: makeMLDSAWork(4),
BLS: makeBLSWork(t, 3),
MLDSA: makeMLDSAWork(t, 4),
}
result, err := pipeline.VerifyBlock(work)
require.NoError(t, err)
require.NotNil(t, result)
// CPU fallback must produce valid results for well-formed inputs
// CPU fallback performs real verification; valid signatures are accepted.
require.Len(t, result.BLSValid, 3)
for i, v := range result.BLSValid {
require.True(t, v, "CPU BLS[%d] should be valid", i)
@@ -155,6 +196,111 @@ func TestGPUPipeline_CPUFallback(t *testing.T) {
require.Equal(t, uint64(1), stats.CPUVerifies)
}
// TestCPUVerify_RealOracle proves the CPU fallback is a real cryptographic
// oracle, not a length-checking rubber stamp: a valid signature is accepted
// and a well-formed-but-FORGED signature (correct lengths, wrong bytes) is
// REJECTED. The forged-rejection case is the regression guard against the
// old `return true for well-formed inputs` behavior.
func TestCPUVerify_RealOracle(t *testing.T) {
t.Run("BLS valid accepted, forged rejected", func(t *testing.T) {
msg, sig, pk := makeValidBLSEntry(t)
// Valid signature => accepted.
good := cpuBLSVerify(&BLSWork{
Messages: [][]byte{msg},
Signatures: [][]byte{sig},
PubKeys: [][]byte{pk},
})
require.Equal(t, []bool{true}, good, "valid BLS signature must be accepted")
// Forged signature: correct 96-byte length, random bytes => rejected.
forgedSig := makeRandomBytes(96)
bad := cpuBLSVerify(&BLSWork{
Messages: [][]byte{msg},
Signatures: [][]byte{forgedSig},
PubKeys: [][]byte{pk},
})
require.Equal(t, []bool{false}, bad, "forged BLS signature (right length, wrong bytes) must be REJECTED")
// Valid signature against the WRONG message => rejected.
wrongMsg := cpuBLSVerify(&BLSWork{
Messages: [][]byte{makeRandomBytes(32)},
Signatures: [][]byte{sig},
PubKeys: [][]byte{pk},
})
require.Equal(t, []bool{false}, wrongMsg, "BLS signature over a different message must be REJECTED")
})
t.Run("MLDSA valid accepted, forged rejected", func(t *testing.T) {
msg, sig, pk := makeValidMLDSAEntry(t)
// Valid signature => accepted.
good := cpuMLDSAVerify(&MLDSAWork{
Messages: [][]byte{msg},
Signatures: [][]byte{sig},
PubKeys: [][]byte{pk},
})
require.Equal(t, []bool{true}, good, "valid ML-DSA signature must be accepted")
// Forged signature: correct length, random bytes => rejected.
forgedSig := makeRandomBytes(mldsa.MLDSA65SignatureSize)
bad := cpuMLDSAVerify(&MLDSAWork{
Messages: [][]byte{msg},
Signatures: [][]byte{forgedSig},
PubKeys: [][]byte{pk},
})
require.Equal(t, []bool{false}, bad, "forged ML-DSA signature (right length, wrong bytes) must be REJECTED")
// Valid signature against the WRONG message => rejected.
wrongMsg := cpuMLDSAVerify(&MLDSAWork{
Messages: [][]byte{makeRandomBytes(64)},
Signatures: [][]byte{sig},
PubKeys: [][]byte{pk},
})
require.Equal(t, []bool{false}, wrongMsg, "ML-DSA signature over a different message must be REJECTED")
})
t.Run("Corona fails closed", func(t *testing.T) {
// No pure-Go Corona verifier exists; every element must be rejected,
// never rubber-stamped on length alone.
got := cpuCoronaVerify(&CoronaWork{
Messages: [][]byte{makeRandomBytes(48), makeRandomBytes(48)},
Signatures: [][]byte{makeRandomBytes(512), makeRandomBytes(512)},
PubKeys: [][]byte{makeRandomBytes(256), makeRandomBytes(256)},
})
require.Equal(t, []bool{false, false}, got, "Corona must fail closed for all elements")
})
t.Run("ZK fails closed", func(t *testing.T) {
// No pure-Go ZK proof verifier exists; the batch must be rejected.
got := cpuZKVerify(&ZKWork{
Scalars: [][]byte{makeRandomBytes(32)},
Bases: [][]byte{makeRandomBytes(64)},
})
require.False(t, got, "ZK must fail closed")
})
}
// TestMLDSA_WorkStructSizeIsCanonical pins the FIPS-204 ML-DSA-65 signature and
// public key sizes that the MLDSAWork struct comment and gpuMLDSAVerify's
// fixed-size flatten now use. luxfi/crypto (circl v1.6.3, FIPS-204 final)
// produces 3309-byte ML-DSA-65 signatures (5*640 + 55 + 6 + 48 = 3309); the
// GPU flatten width was corrected from the stale round-3 Dilithium3 size 3293
// to 3309 so the GPU path no longer clamps/corrupts a real signature. The
// public key size (1952) is unchanged across the round-3 -> final transition.
//
// This is the equivalence-pair guard: the CPU oracle (cpuMLDSAVerify) parses
// pk via the typed PublicKeyFromBytes and calls VerifySignature, accepting the
// real 3309-byte signature; the GPU path sizes the signature identically. If
// the crypto constant ever drifts, this test fails before any divergence
// reaches the GPU ML-DSA kernel (not yet wired into block-accept).
func TestMLDSA_WorkStructSizeIsCanonical(t *testing.T) {
require.Equal(t, 1952, mldsa.MLDSA65PublicKeySize,
"ML-DSA-65 public key is 1952 bytes (matches MLDSAWork / gpuMLDSAVerify pkLen)")
require.Equal(t, 3309, mldsa.MLDSA65SignatureSize,
"ML-DSA-65 signature is 3309 bytes (FIPS-204), matching the MLDSAWork struct / gpuMLDSAVerify sigLen")
}
func TestGPUPipeline_EmptyBatches(t *testing.T) {
pipeline := NewGPUVerifyPipeline()
@@ -179,7 +325,7 @@ func TestGPUPipeline_EmptyBatches(t *testing.T) {
{
name: "BLS filled, rest nil",
work: &BlockVerifyWork{
BLS: makeBLSWork(2),
BLS: makeBLSWork(t, 2),
},
},
{
@@ -191,7 +337,7 @@ func TestGPUPipeline_EmptyBatches(t *testing.T) {
{
name: "MLDSA only",
work: &BlockVerifyWork{
MLDSA: makeMLDSAWork(1),
MLDSA: makeMLDSAWork(t, 1),
},
},
{
@@ -276,10 +422,10 @@ func BenchmarkGPUPipeline(b *testing.B) {
pipeline := NewGPUVerifyPipeline()
work := &BlockVerifyWork{
BLS: makeBLSWork(100),
BLS: makeBLSWork(b, 100),
Corona: makeCoronaWork(50),
ZK: makeZKWork(10),
MLDSA: makeMLDSAWork(200),
ZK: makeZKWork(10),
MLDSA: makeMLDSAWork(b, 200),
}
b.ResetTimer()
+19 -19
View File
@@ -116,11 +116,11 @@ func generateValidatorStates(n int) []ValidatorState {
_, _ = rand.Read(rtKey)
states[i] = ValidatorState{
NodeID: ids.GenerateTestNodeID(),
Weight: 1000,
BLSPubKey: blsKey,
NodeID: ids.GenerateTestNodeID(),
Weight: 1000,
BLSPubKey: blsKey,
CoronaKey: rtKey,
Active: true,
Active: true,
}
}
return states
@@ -575,13 +575,13 @@ func TestQuasarMemoryPressure(t *testing.T) {
_, _ = rand.Read(blockID[:])
finality := &QuantumFinality{
BlockID: blockID,
PChainHeight: uint64(i),
QChainHeight: uint64(i),
BLSProof: make([]byte, 96),
CoronaProof: make([]byte, 1024),
TotalWeight: 1000,
SignerWeight: 700,
BlockID: blockID,
PChainHeight: uint64(i),
QChainHeight: uint64(i),
BLSProof: make([]byte, 96),
CoronaProof: make([]byte, 1024),
TotalWeight: 1000,
SignerWeight: 700,
}
q.SetFinalized(blockID, finality)
}
@@ -913,10 +913,10 @@ func TestQuasarEdgeCases(t *testing.T) {
require.NoError(t, err)
finality := &QuantumFinality{
BLSProof: nil,
CoronaProof: nil,
TotalWeight: 1000,
SignerWeight: 700,
BLSProof: nil,
CoronaProof: nil,
TotalWeight: 1000,
SignerWeight: 700,
}
err = q.Verify(finality)
@@ -928,10 +928,10 @@ func TestQuasarEdgeCases(t *testing.T) {
require.NoError(t, err)
finality := &QuantumFinality{
BLSProof: []byte("proof"),
CoronaProof: []byte("proof"),
TotalWeight: 1000,
SignerWeight: 500, // Only 50%, needs 67%
BLSProof: []byte("proof"),
CoronaProof: []byte("proof"),
TotalWeight: 1000,
SignerWeight: 500, // Only 50%, needs 67%
}
err = q.Verify(finality)
+1 -1
View File
@@ -401,7 +401,7 @@ func TestConcurrentPruningStress(t *testing.T) {
require.NoError(t, err)
const (
numWriters = 8
numWriters = 8
opsPerWriter = 5000
)
+45 -45
View File
@@ -61,15 +61,15 @@ import (
// 2. t-of-n validators completed Corona threshold (post-quantum secure)
var (
ErrQuasarNotStarted = errors.New("quasar not started")
ErrPChainNotConnected = errors.New("P-Chain not connected")
ErrQChainNotConnected = errors.New("Q-Chain not connected")
ErrCoronaNotConnected = errors.New("Corona coordinator not connected")
ErrInsufficientWeight = errors.New("insufficient validator weight")
ErrInsufficientSigners = errors.New("insufficient Corona signers")
ErrFinalityFailed = errors.New("hybrid finality verification failed")
ErrBLSFailed = errors.New("BLS aggregation failed")
ErrCoronaFailed = errors.New("Corona threshold signing failed")
ErrQuasarNotStarted = errors.New("quasar not started")
ErrPChainNotConnected = errors.New("P-Chain not connected")
ErrQChainNotConnected = errors.New("Q-Chain not connected")
ErrCoronaNotConnected = errors.New("Corona coordinator not connected")
ErrInsufficientWeight = errors.New("insufficient validator weight")
ErrInsufficientSigners = errors.New("insufficient Corona signers")
ErrFinalityFailed = errors.New("hybrid finality verification failed")
ErrBLSFailed = errors.New("BLS aggregation failed")
ErrCoronaFailed = errors.New("Corona threshold signing failed")
)
// PChainProvider provides P-Chain state and finality events
@@ -87,11 +87,11 @@ type QuantumSignerFallback interface {
// ValidatorState represents a validator's current state
// Each validator has BOTH BLS and Corona keys
type ValidatorState struct {
NodeID ids.NodeID
Weight uint64
BLSPubKey []byte // BLS public key for aggregate signatures
NodeID ids.NodeID
Weight uint64
BLSPubKey []byte // BLS public key for aggregate signatures
CoronaKey []byte // Corona public key share for threshold sigs
Active bool
Active bool
}
// FinalityEvent represents a P-Chain finality event
@@ -104,18 +104,18 @@ type FinalityEvent struct {
// QuantumFinality represents a block that achieved hybrid quantum finality
type QuantumFinality struct {
BlockID ids.ID
PChainHeight uint64
QChainHeight uint64
BLSProof []byte // Aggregated BLS signature (96 bytes)
BlockID ids.ID
PChainHeight uint64
QChainHeight uint64
BLSProof []byte // Aggregated BLS signature (96 bytes)
CoronaProof []byte // Serialized Corona threshold signature
SignerBitset []byte // Which validators signed BLS
SignerBitset []byte // Which validators signed BLS
CoronaSigners []ids.NodeID // Which validators participated in Corona
TotalWeight uint64
SignerWeight uint64
BLSLatency time.Duration
TotalWeight uint64
SignerWeight uint64
BLSLatency time.Duration
CoronaLatency time.Duration
Timestamp time.Time
Timestamp time.Time
}
// Quasar binds P-Chain and Q-Chain consensus into hybrid quantum finality
@@ -369,18 +369,18 @@ func (q *Quasar) processFinality(ctx context.Context, event FinalityEvent) error
coronaProof = coronaSig.Bytes()
}
finality := &QuantumFinality{
BlockID: event.BlockID,
PChainHeight: event.Height,
QChainHeight: q.qHeight,
BLSProof: blsProof,
BlockID: event.BlockID,
PChainHeight: event.Height,
QChainHeight: q.qHeight,
BLSProof: blsProof,
CoronaProof: coronaProof,
SignerBitset: signerBitset,
SignerBitset: signerBitset,
CoronaSigners: coronaSigners,
TotalWeight: totalWeight,
SignerWeight: signerWeight,
BLSLatency: blsLatency,
TotalWeight: totalWeight,
SignerWeight: signerWeight,
BLSLatency: blsLatency,
CoronaLatency: coronaLatency,
Timestamp: time.Now(),
Timestamp: time.Now(),
}
q.finalized[event.BlockID] = finality
@@ -590,13 +590,13 @@ func (q *Quasar) Stats() QuasarStats {
}
return QuasarStats{
PChainHeight: q.pHeight,
QChainHeight: q.qHeight,
FinalizedBlocks: len(q.finalized),
Threshold: q.threshold,
QuorumNum: q.quorumNum,
QuorumDen: q.quorumDen,
Running: q.running,
PChainHeight: q.pHeight,
QChainHeight: q.qHeight,
FinalizedBlocks: len(q.finalized),
Threshold: q.threshold,
QuorumNum: q.quorumNum,
QuorumDen: q.quorumDen,
Running: q.running,
CoronaParties: coronaStats.NumParties,
CoronaThreshold: coronaStats.Threshold,
CoronaReady: coronaStats.Initialized,
@@ -605,13 +605,13 @@ func (q *Quasar) Stats() QuasarStats {
// QuasarStats contains quasar statistics
type QuasarStats struct {
PChainHeight uint64
QChainHeight uint64
FinalizedBlocks int
Threshold int
QuorumNum uint64
QuorumDen uint64
Running bool
PChainHeight uint64
QChainHeight uint64
FinalizedBlocks int
Threshold int
QuorumNum uint64
QuorumDen uint64
Running bool
CoronaParties int
CoronaThreshold int
CoronaReady bool
+2 -2
View File
@@ -181,7 +181,7 @@ func (s *BLSSignature) Signers() []ids.NodeID { return s.signers }
// QuasarSignature combines BLS and Corona signatures for P/Q security
type QuasarSignature struct {
bls *BLSSignature
bls *BLSSignature
corona *CoronaSignature
}
@@ -211,7 +211,7 @@ func (s *QuasarSignature) Signers() []ids.NodeID {
return s.bls.Signers()
}
func (s *QuasarSignature) BLS() *BLSSignature { return s.bls }
func (s *QuasarSignature) BLS() *BLSSignature { return s.bls }
func (s *QuasarSignature) Corona() *CoronaSignature { return s.corona }
// QuasarSigner combines classical and post-quantum signers
-159
View File
@@ -1,159 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpcdb
import (
"context"
"google.golang.org/protobuf/types/known/emptypb"
"github.com/luxfi/database"
rpcdbpb "github.com/luxfi/node/proto/pb/rpcdb"
rpcdb "github.com/luxfi/proto/rpcdb"
)
// GRPCServer is the gRPC transport adapter for the rpcdb Service. It
// wraps *Service and translates gRPC's protobuf-generated request /
// response types into the transport-neutral wire types in
// github.com/luxfi/proto/rpcdb. Pure adapter — no storage logic
// lives here.
type GRPCServer struct {
rpcdbpb.UnimplementedDatabaseServer
svc *Service
}
// NewGRPCServer wraps a database.Database for serving over gRPC.
func NewGRPCServer(db database.Database) *GRPCServer {
return NewGRPCServerFromService(NewService(db))
}
// NewGRPCServerFromService wraps an existing Service for serving over
// gRPC. Symmetric with NewZAPServerFromService.
func NewGRPCServerFromService(svc *Service) *GRPCServer {
return &GRPCServer{svc: svc}
}
// Compile-time interface check.
var _ rpcdbpb.DatabaseServer = (*GRPCServer)(nil)
func toPbErr(code rpcdb.Error) rpcdbpb.Error {
switch code {
case rpcdb.Error_ERROR_NOT_FOUND:
return rpcdbpb.Error_ERROR_NOT_FOUND
case rpcdb.Error_ERROR_CLOSED:
return rpcdbpb.Error_ERROR_CLOSED
default:
return rpcdbpb.Error_ERROR_UNSPECIFIED
}
}
func (g *GRPCServer) Has(ctx context.Context, req *rpcdbpb.HasRequest) (*rpcdbpb.HasResponse, error) {
resp, err := g.svc.Has(ctx, &rpcdb.HasRequest{Key: req.Key})
if err != nil {
return nil, err
}
return &rpcdbpb.HasResponse{Has: resp.Has, Err: toPbErr(resp.Err)}, nil
}
func (g *GRPCServer) Get(ctx context.Context, req *rpcdbpb.GetRequest) (*rpcdbpb.GetResponse, error) {
resp, err := g.svc.Get(ctx, &rpcdb.GetRequest{Key: req.Key})
if err != nil {
return nil, err
}
return &rpcdbpb.GetResponse{Value: resp.Value, Err: toPbErr(resp.Err)}, nil
}
func (g *GRPCServer) Put(ctx context.Context, req *rpcdbpb.PutRequest) (*rpcdbpb.PutResponse, error) {
resp, err := g.svc.Put(ctx, &rpcdb.PutRequest{Key: req.Key, Value: req.Value})
if err != nil {
return nil, err
}
return &rpcdbpb.PutResponse{Err: toPbErr(resp.Err)}, nil
}
func (g *GRPCServer) Delete(ctx context.Context, req *rpcdbpb.DeleteRequest) (*rpcdbpb.DeleteResponse, error) {
resp, err := g.svc.Delete(ctx, &rpcdb.DeleteRequest{Key: req.Key})
if err != nil {
return nil, err
}
return &rpcdbpb.DeleteResponse{Err: toPbErr(resp.Err)}, nil
}
func (g *GRPCServer) WriteBatch(ctx context.Context, req *rpcdbpb.WriteBatchRequest) (*rpcdbpb.WriteBatchResponse, error) {
puts := make([]*rpcdb.PutRequest, len(req.Puts))
for i, p := range req.Puts {
puts[i] = &rpcdb.PutRequest{Key: p.Key, Value: p.Value}
}
dels := make([]*rpcdb.DeleteRequest, len(req.Deletes))
for i, d := range req.Deletes {
dels[i] = &rpcdb.DeleteRequest{Key: d.Key}
}
resp, err := g.svc.WriteBatch(ctx, &rpcdb.WriteBatchRequest{Puts: puts, Deletes: dels})
if err != nil {
return nil, err
}
return &rpcdbpb.WriteBatchResponse{Err: toPbErr(resp.Err)}, nil
}
func (g *GRPCServer) Compact(ctx context.Context, req *rpcdbpb.CompactRequest) (*rpcdbpb.CompactResponse, error) {
resp, err := g.svc.Compact(ctx, &rpcdb.CompactRequest{Start: req.Start, Limit: req.Limit})
if err != nil {
return nil, err
}
return &rpcdbpb.CompactResponse{Err: toPbErr(resp.Err)}, nil
}
func (g *GRPCServer) Close(ctx context.Context, _ *rpcdbpb.CloseRequest) (*rpcdbpb.CloseResponse, error) {
resp, err := g.svc.Close(ctx, &rpcdb.CloseRequest{})
if err != nil {
return nil, err
}
return &rpcdbpb.CloseResponse{Err: toPbErr(resp.Err)}, nil
}
func (g *GRPCServer) HealthCheck(ctx context.Context, _ *emptypb.Empty) (*rpcdbpb.HealthCheckResponse, error) {
resp, err := g.svc.HealthCheck(ctx)
if err != nil {
return nil, err
}
return &rpcdbpb.HealthCheckResponse{Details: resp.Details}, nil
}
func (g *GRPCServer) NewIteratorWithStartAndPrefix(ctx context.Context, req *rpcdbpb.NewIteratorWithStartAndPrefixRequest) (*rpcdbpb.NewIteratorWithStartAndPrefixResponse, error) {
resp, err := g.svc.NewIteratorWithStartAndPrefix(ctx, &rpcdb.NewIteratorWithStartAndPrefixRequest{Start: req.Start, Prefix: req.Prefix})
if err != nil {
return nil, err
}
return &rpcdbpb.NewIteratorWithStartAndPrefixResponse{Id: resp.Id}, nil
}
func (g *GRPCServer) IteratorNext(ctx context.Context, req *rpcdbpb.IteratorNextRequest) (*rpcdbpb.IteratorNextResponse, error) {
resp, err := g.svc.IteratorNext(ctx, &rpcdb.IteratorNextRequest{Id: req.Id})
if err != nil {
return nil, err
}
data := make([]*rpcdbpb.PutRequest, len(resp.Data))
for i, e := range resp.Data {
data[i] = &rpcdbpb.PutRequest{Key: e.Key, Value: e.Value}
}
return &rpcdbpb.IteratorNextResponse{Data: data}, nil
}
func (g *GRPCServer) IteratorError(ctx context.Context, req *rpcdbpb.IteratorErrorRequest) (*rpcdbpb.IteratorErrorResponse, error) {
resp, err := g.svc.IteratorError(ctx, &rpcdb.IteratorErrorRequest{Id: req.Id})
if err != nil {
return nil, err
}
return &rpcdbpb.IteratorErrorResponse{Err: toPbErr(resp.Err)}, nil
}
func (g *GRPCServer) IteratorRelease(ctx context.Context, req *rpcdbpb.IteratorReleaseRequest) (*rpcdbpb.IteratorReleaseResponse, error) {
resp, err := g.svc.IteratorRelease(ctx, &rpcdb.IteratorReleaseRequest{Id: req.Id})
if err != nil {
return nil, err
}
return &rpcdbpb.IteratorReleaseResponse{Err: toPbErr(resp.Err)}, nil
}
+8 -8
View File
@@ -7,9 +7,9 @@ import (
"testing"
"github.com/luxfi/consensus/utils/set"
validators "github.com/luxfi/validators"
"github.com/luxfi/constants"
"github.com/luxfi/ids"
validators "github.com/luxfi/validators"
)
func TestFullValidatorFunctionality(t *testing.T) {
@@ -151,15 +151,15 @@ func TestNetworkConfiguration(t *testing.T) {
t.Logf("✓ Network configuration verified: ID=%d, Name=%s", networkID, expectedName)
}
func TestXAssetID(t *testing.T) {
// Verify XAssetID is used for native asset
xAssetID := ids.Empty // Our implementation uses Empty ID for native asset
func TestUTXOAssetID(t *testing.T) {
// Verify UTXOAssetID is used for native asset
utxoAssetID := ids.Empty // Our implementation uses Empty ID for native asset
if xAssetID != ids.Empty {
t.Fatal("XAssetID should be Empty for native asset")
if utxoAssetID != ids.Empty {
t.Fatal("UTXOAssetID should be Empty for native asset")
}
t.Log("✓ XAssetID verified for native LUX asset")
t.Log("✓ UTXOAssetID verified for native LUX asset")
}
func TestConsensusConfig(t *testing.T) {
@@ -319,7 +319,7 @@ func TestSummary(t *testing.T) {
t.Log("✓ Validator Manager: PASS")
t.Log("✓ Network Configuration: PASS")
t.Log("✓ Consensus Parameters: PASS")
t.Log("✓ XAssetID Configuration: PASS")
t.Log("✓ UTXOAssetID Configuration: PASS")
t.Log("✓ Validator Lifecycle: PASS")
t.Log("✓ Validator Set Interface: PASS")
t.Log("----------------------------------------")
+1 -1
View File
@@ -4,9 +4,9 @@
package debug
import (
validators "github.com/luxfi/validators"
"github.com/luxfi/constants"
"github.com/luxfi/ids"
validators "github.com/luxfi/validators"
"testing"
)
+5 -5
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# Deploy all 4 subnet chains (Zoo, Hanzo, SPC, Pars) to mainnet + testnet
# Usage: ./deploy-subnets.sh [mainnet|testnet|devnet|both]
# Deploy all 4 app chains (Zoo, Hanzo, SPC, Pars) to mainnet + testnet
# Usage: ./deploy-chains.sh [mainnet|testnet|devnet|both]
#
# Prerequisites:
# - macOS keychain must have mainnet-key-02 key (will prompt for approval)
@@ -21,7 +21,7 @@ if [ ! -f "$DEPLOY_BIN" ]; then
fi
# Use mainnet-key-02 (NOT 01!) because 01's funds are all staked by initialStakers
# mainnet-key-02 has 70M LUX unlocked on P-chain, sufficient for subnet creation
# mainnet-key-02 has 70M LUX unlocked on P-chain, sufficient for chain creation
KEY_NAME="mainnet-key-02"
echo "Extracting $KEY_NAME from keychain..."
echo "(Approve the macOS keychain dialog that appears)"
@@ -39,7 +39,7 @@ deploy_network() {
local network=$1
echo ""
echo "=============================="
echo "Deploying subnets to $network"
echo "Deploying chains to $network"
echo "=============================="
PRIVATE_KEY="$KEY" "$DEPLOY_BIN" \
@@ -77,7 +77,7 @@ echo ""
echo "All deployments complete!"
echo ""
echo "Next steps:"
echo " 1. Copy the Subnet IDs and Blockchain IDs from output above"
echo " 1. Copy the Chain IDs and Blockchain IDs from output above"
echo " 2. Update Helm values files:"
echo " ~/work/lux/devops/charts/lux/values-mainnet.yaml"
echo " ~/work/lux/devops/charts/lux/values-testnet.yaml"
+1 -1
View File
@@ -1,5 +1,5 @@
# Build stage
FROM golang:1.26-alpine AS builder
FROM golang:1.26.3-alpine AS builder
RUN apk add --no-cache git make gcc musl-dev linux-headers bash
+1 -1
View File
@@ -1,5 +1,5 @@
# Multi-chain node builder
FROM golang:1.26-alpine AS builder
FROM golang:1.26.3-alpine AS builder
ARG CHAIN_TYPE=platform
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.26-alpine AS builder
FROM golang:1.26.3-alpine AS builder
RUN apk add --no-cache git make gcc musl-dev linux-headers bash
+5 -5
View File
@@ -140,8 +140,8 @@ else
{
"allocations": [
{
"luxAddr": "P-local1jfdgmqduuyuxjp9sq7szp08xpynav88sd7qxvv",
"ethAddr": "0x9011E888251AB053B7bD1cdB598Db4f9DEd94714",
"utxoAddr": "P-local1jfdgmqduuyuxjp9sq7szp08xpynav88sd7qxvv",
"evmAddr": "0x9011E888251AB053B7bD1cdB598Db4f9DEd94714",
"initialAmount": 1000000000000000000,
"unlockSchedule": []
}
@@ -167,8 +167,8 @@ EOF
{
"allocations": [
{
"luxAddr": "X-local1jfdgmqduuyuxjp9sq7szp08xpynav88sd7qxvv",
"ethAddr": "0x9011E888251AB053B7bD1cdB598Db4f9DEd94714",
"utxoAddr": "X-local1jfdgmqduuyuxjp9sq7szp08xpynav88sd7qxvv",
"evmAddr": "0x9011E888251AB053B7bD1cdB598Db4f9DEd94714",
"initialAmount": 1000000000000000000,
"unlockSchedule": []
}
@@ -206,7 +206,7 @@ EOF
"apricotPhase4BlockTimestamp": 0,
"apricotPhase5BlockTimestamp": 0,
"durangoBlockTimestamp": 0,
"etnaTimestamp": 0,
"quasarTimestamp": 0,
"feeConfig": {
"gasLimit": 20000000,
"minBaseFee": 1000000000,
+20 -15
View File
@@ -237,8 +237,8 @@ Final Validation
### Properties
- **Security**: Quantum-resistant (192-bit)
- **Signatures**: ML-DSA-65 (Dilithium)
- **Privacy**: Corona ring signatures
- **Signatures**: ML-DSA-65 (Dilithium) + Corona (LWE-based threshold)
- **Threshold scheme**: Corona — native t-of-n in 2 rounds
- **Performance**: ~1,500 TPS
### Implementation
@@ -247,7 +247,7 @@ Final Validation
type PQConsensus struct {
classicalSigner *bls.Signer
quantumSigner *mldsa.Signer
coronaSigner *corona.Signer
coronaSigner *corona.Signer
threshold int
validatorSet []Validator
@@ -279,27 +279,32 @@ func (pq *PQConsensus) VerifyBlock(block Block, sig Signature) bool {
}
```
### Corona Privacy Layer
### Corona Threshold Layer
Corona is a lattice-based threshold signature scheme from LWE
([eprint 2024/1113](https://eprint.iacr.org/2024/1113)) implemented in
`github.com/luxfi/corona`. Unlike a ring signature, Corona signers are
known and weighted; the scheme provides a single aggregated signature
that anyone can verify against the group public key.
```go
type CoronaSignature struct {
Ring []PublicKey
Signature []byte
KeyImage []byte
Signers []byte // bitset of signing validator indices
Signature []byte // aggregated lattice signature
}
func (pq *PQConsensus) CreateRingSignature(
func (pq *PQConsensus) CoronaSign(
message []byte,
signerKey PrivateKey,
ring []PublicKey,
signerKey *corona.PrivateKey,
signers []*Validator,
) (*CoronaSignature, error) {
// Create anonymous signature within ring
sig := pq.coronaSigner.Sign(message, signerKey, ring)
sig, err := pq.coronaSigner.Sign(message, signerKey, signers)
if err != nil {
return nil, err
}
return &CoronaSignature{
Ring: ring,
Signers: sig.SignerBitset(),
Signature: sig.Bytes(),
KeyImage: sig.KeyImage(),
}, nil
}
```
+3 -3
View File
@@ -25,7 +25,7 @@ Lux implements a 4-chain primary network architecture, each optimized for specif
│ (Quantum) │
│ │
│ Post-Quantum Consensus │
Corona Signatures
│ Corona Threshold Signatures │
└─────────────────────────────────────────────────┘
```
@@ -76,7 +76,7 @@ Lux implements a 4-chain primary network architecture, each optimized for specif
**Purpose:** Post-quantum security layer
- Quantum-resistant cryptography
- ML-DSA-65 (Dilithium) signatures
- Corona ring signatures for privacy
- Corona (LWE-based) threshold signatures
- Hybrid classical/quantum consensus
**Key Features:**
@@ -301,7 +301,7 @@ Native cross-chain communication protocol:
| secp256k1 | ECDSA signatures | 128-bit |
| BLS12-381 | Threshold signatures | 128-bit |
| ML-DSA-65 | Post-quantum signatures | 192-bit |
| Corona | Ring signatures | 128-bit |
| Corona | LWE threshold signatures | 192-bit (post-quantum) |
| SHA-256 | Hashing | 128-bit |
| AES-256-GCM | Encryption | 256-bit |
@@ -310,7 +310,7 @@ Create `~/.luxd/configs/chains/Q/config.json`:
"corona-signatures-enabled": true,
"post-quantum-algorithm": "ml-dsa-65",
"quantum-signature-cache-size": 10000,
"ring-signature-size": 16,
"corona-threshold-size": 16,
"corona-key-size": 1024,
"quantum-stamp-enabled": true,
"quantum-stamp-window": "30s",
@@ -226,7 +226,7 @@ Create `~/.luxd/configs/chains/Q/config.json`:
"quantum-verification-enabled": true,
"corona-signatures-enabled": true,
"post-quantum-algorithm": "ml-dsa-65",
"ring-signature-size": 16,
"corona-threshold-size": 16,
"quantum-cache-size": 10000,
"parallel-batch-size": 10
}
+1 -1
View File
@@ -15,7 +15,7 @@
"fumadocs-mdx": "^12.0.3",
"fumadocs-ui": "^15.8.5",
"lucide-react": "^0.468.0",
"next": "16.1.5",
"next": "16.2.6",
"react": "19.2.0",
"react-dom": "19.2.0",
"tailwindcss": "^4.1.16",
+83 -74
View File
@@ -10,19 +10,19 @@ importers:
dependencies:
fumadocs-core:
specifier: ^15.8.5
version: 15.8.5(@types/react@19.2.7)(lucide-react@0.468.0(react@19.2.0))(next@16.1.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
version: 15.8.5(@types/react@19.2.7)(lucide-react@0.468.0(react@19.2.0))(next@16.2.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
fumadocs-mdx:
specifier: ^12.0.3
version: 12.0.3(fumadocs-core@15.8.5(@types/react@19.2.7)(lucide-react@0.468.0(react@19.2.0))(next@16.1.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(next@16.1.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)
version: 12.0.3(fumadocs-core@15.8.5(@types/react@19.2.7)(lucide-react@0.468.0(react@19.2.0))(next@16.2.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(next@16.2.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)
fumadocs-ui:
specifier: ^15.8.5
version: 15.8.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(lucide-react@0.468.0(react@19.2.0))(next@16.1.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(tailwindcss@4.1.18)
version: 15.8.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(lucide-react@0.468.0(react@19.2.0))(next@16.2.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(tailwindcss@4.1.18)
lucide-react:
specifier: ^0.468.0
version: 0.468.0(react@19.2.0)
next:
specifier: 16.1.5
version: 16.1.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
specifier: 16.2.6
version: 16.2.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
react:
specifier: 19.2.0
version: 19.2.0
@@ -70,8 +70,8 @@ packages:
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
'@emnapi/runtime@1.8.1':
resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==}
'@emnapi/runtime@1.10.0':
resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==}
'@esbuild/aix-ppc64@0.25.12':
resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
@@ -247,8 +247,8 @@ packages:
'@formatjs/intl-localematcher@0.6.2':
resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==}
'@img/colour@1.0.0':
resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==}
'@img/colour@1.1.0':
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
engines: {node: '>=18'}
'@img/sharp-darwin-arm64@0.34.5':
@@ -403,53 +403,53 @@ packages:
'@mdx-js/mdx@3.1.1':
resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==}
'@next/env@16.1.5':
resolution: {integrity: sha512-CRSCPJiSZoi4Pn69RYBDI9R7YK2g59vLexPQFXY0eyw+ILevIenCywzg+DqmlBik9zszEnw2HLFOUlLAcJbL7g==}
'@next/env@16.2.6':
resolution: {integrity: sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==}
'@next/swc-darwin-arm64@16.1.5':
resolution: {integrity: sha512-eK7Wdm3Hjy/SCL7TevlH0C9chrpeOYWx2iR7guJDaz4zEQKWcS1IMVfMb9UKBFMg1XgzcPTYPIp1Vcpukkjg6Q==}
'@next/swc-darwin-arm64@16.2.6':
resolution: {integrity: sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@next/swc-darwin-x64@16.1.5':
resolution: {integrity: sha512-foQscSHD1dCuxBmGkbIr6ScAUF6pRoDZP6czajyvmXPAOFNnQUJu2Os1SGELODjKp/ULa4fulnBWoHV3XdPLfA==}
'@next/swc-darwin-x64@16.2.6':
resolution: {integrity: sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@next/swc-linux-arm64-gnu@16.1.5':
resolution: {integrity: sha512-qNIb42o3C02ccIeSeKjacF3HXotGsxh/FMk/rSRmCzOVMtoWH88odn2uZqF8RLsSUWHcAqTgYmPD3pZ03L9ZAA==}
'@next/swc-linux-arm64-gnu@16.2.6':
resolution: {integrity: sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-arm64-musl@16.1.5':
resolution: {integrity: sha512-U+kBxGUY1xMAzDTXmuVMfhaWUZQAwzRaHJ/I6ihtR5SbTVUEaDRiEU9YMjy1obBWpdOBuk1bcm+tsmifYSygfw==}
'@next/swc-linux-arm64-musl@16.2.6':
resolution: {integrity: sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-x64-gnu@16.1.5':
resolution: {integrity: sha512-gq2UtoCpN7Ke/7tKaU7i/1L7eFLfhMbXjNghSv0MVGF1dmuoaPeEVDvkDuO/9LVa44h5gqpWeJ4mRRznjDv7LA==}
'@next/swc-linux-x64-gnu@16.2.6':
resolution: {integrity: sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-linux-x64-musl@16.1.5':
resolution: {integrity: sha512-bQWSE729PbXT6mMklWLf8dotislPle2L70E9q6iwETYEOt092GDn0c+TTNj26AjmeceSsC4ndyGsK5nKqHYXjQ==}
'@next/swc-linux-x64-musl@16.2.6':
resolution: {integrity: sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-win32-arm64-msvc@16.1.5':
resolution: {integrity: sha512-LZli0anutkIllMtTAWZlDqdfvjWX/ch8AFK5WgkNTvaqwlouiD1oHM+WW8RXMiL0+vAkAJyAGEzPPjO+hnrSNQ==}
'@next/swc-win32-arm64-msvc@16.2.6':
resolution: {integrity: sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@next/swc-win32-x64-msvc@16.1.5':
resolution: {integrity: sha512-7is37HJTNQGhjPpQbkKjKEboHYQnCgpVt/4rBrrln0D9nderNxZ8ZWs8w1fAtzUx7wEyYjQ+/13myFgFj6K2Ng==}
'@next/swc-win32-x64-msvc@16.2.6':
resolution: {integrity: sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -1002,6 +1002,7 @@ packages:
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
deprecated: Potential CWE-502 - Update to 1.3.1 or higher
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
@@ -1034,8 +1035,9 @@ packages:
bail@2.0.2:
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
baseline-browser-mapping@2.9.19:
resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==}
baseline-browser-mapping@2.10.33:
resolution: {integrity: sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==}
engines: {node: '>=6.0.0'}
hasBin: true
browserslist@4.28.1:
@@ -1046,8 +1048,8 @@ packages:
caniuse-lite@1.0.30001760:
resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==}
caniuse-lite@1.0.30001766:
resolution: {integrity: sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==}
caniuse-lite@1.0.30001793:
resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==}
ccount@2.0.1:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
@@ -1594,6 +1596,11 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
nanoid@3.3.12:
resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
negotiator@1.0.0:
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
engines: {node: '>= 0.6'}
@@ -1604,8 +1611,8 @@ packages:
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
next@16.1.5:
resolution: {integrity: sha512-f+wE+NSbiQgh3DSAlTaw2FwY5yGdVViAtp8TotNQj4kk4Q8Bh1sC/aL9aH+Rg1YAVn18OYXsRDT7U/079jgP7w==}
next@16.2.6:
resolution: {integrity: sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==}
engines: {node: '>=20.9.0'}
hasBin: true
peerDependencies:
@@ -1792,8 +1799,8 @@ packages:
scroll-into-view-if-needed@3.1.0:
resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==}
semver@7.7.3:
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
semver@7.8.1:
resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==}
engines: {node: '>=10'}
hasBin: true
@@ -1950,7 +1957,7 @@ snapshots:
'@alloc/quick-lru@5.2.0': {}
'@emnapi/runtime@1.8.1':
'@emnapi/runtime@1.10.0':
dependencies:
tslib: 2.8.1
optional: true
@@ -2054,7 +2061,7 @@ snapshots:
dependencies:
tslib: 2.8.1
'@img/colour@1.0.0':
'@img/colour@1.1.0':
optional: true
'@img/sharp-darwin-arm64@0.34.5':
@@ -2139,7 +2146,7 @@ snapshots:
'@img/sharp-wasm32@0.34.5':
dependencies:
'@emnapi/runtime': 1.8.1
'@emnapi/runtime': 1.10.0
optional: true
'@img/sharp-win32-arm64@0.34.5':
@@ -2200,30 +2207,30 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@next/env@16.1.5': {}
'@next/env@16.2.6': {}
'@next/swc-darwin-arm64@16.1.5':
'@next/swc-darwin-arm64@16.2.6':
optional: true
'@next/swc-darwin-x64@16.1.5':
'@next/swc-darwin-x64@16.2.6':
optional: true
'@next/swc-linux-arm64-gnu@16.1.5':
'@next/swc-linux-arm64-gnu@16.2.6':
optional: true
'@next/swc-linux-arm64-musl@16.1.5':
'@next/swc-linux-arm64-musl@16.2.6':
optional: true
'@next/swc-linux-x64-gnu@16.1.5':
'@next/swc-linux-x64-gnu@16.2.6':
optional: true
'@next/swc-linux-x64-musl@16.1.5':
'@next/swc-linux-x64-musl@16.2.6':
optional: true
'@next/swc-win32-arm64-msvc@16.1.5':
'@next/swc-win32-arm64-msvc@16.2.6':
optional: true
'@next/swc-win32-x64-msvc@16.1.5':
'@next/swc-win32-x64-msvc@16.2.6':
optional: true
'@orama/orama@3.1.17': {}
@@ -2804,11 +2811,11 @@ snapshots:
bail@2.0.2: {}
baseline-browser-mapping@2.9.19: {}
baseline-browser-mapping@2.10.33: {}
browserslist@4.28.1:
dependencies:
baseline-browser-mapping: 2.9.19
baseline-browser-mapping: 2.10.33
caniuse-lite: 1.0.30001760
electron-to-chromium: 1.5.267
node-releases: 2.0.27
@@ -2816,7 +2823,7 @@ snapshots:
caniuse-lite@1.0.30001760: {}
caniuse-lite@1.0.30001766: {}
caniuse-lite@1.0.30001793: {}
ccount@2.0.1: {}
@@ -2971,7 +2978,7 @@ snapshots:
fraction.js@5.3.4: {}
fumadocs-core@15.8.5(@types/react@19.2.7)(lucide-react@0.468.0(react@19.2.0))(next@16.1.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
fumadocs-core@15.8.5(@types/react@19.2.7)(lucide-react@0.468.0(react@19.2.0))(next@16.2.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
dependencies:
'@formatjs/intl-localematcher': 0.6.2
'@orama/orama': 3.1.17
@@ -2994,20 +3001,20 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.7
lucide-react: 0.468.0(react@19.2.0)
next: 16.1.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
next: 16.2.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
transitivePeerDependencies:
- supports-color
fumadocs-mdx@12.0.3(fumadocs-core@15.8.5(@types/react@19.2.7)(lucide-react@0.468.0(react@19.2.0))(next@16.1.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(next@16.1.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0):
fumadocs-mdx@12.0.3(fumadocs-core@15.8.5(@types/react@19.2.7)(lucide-react@0.468.0(react@19.2.0))(next@16.2.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(next@16.2.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0):
dependencies:
'@mdx-js/mdx': 3.1.1
'@standard-schema/spec': 1.0.0
chokidar: 4.0.3
esbuild: 0.25.12
estree-util-value-to-estree: 3.5.0
fumadocs-core: 15.8.5(@types/react@19.2.7)(lucide-react@0.468.0(react@19.2.0))(next@16.1.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
fumadocs-core: 15.8.5(@types/react@19.2.7)(lucide-react@0.468.0(react@19.2.0))(next@16.2.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
js-yaml: 4.1.1
lru-cache: 11.2.4
mdast-util-to-markdown: 2.1.2
@@ -3020,12 +3027,12 @@ snapshots:
unist-util-visit: 5.0.0
zod: 4.1.13
optionalDependencies:
next: 16.1.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
next: 16.2.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
react: 19.2.0
transitivePeerDependencies:
- supports-color
fumadocs-ui@15.8.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(lucide-react@0.468.0(react@19.2.0))(next@16.1.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(tailwindcss@4.1.18):
fumadocs-ui@15.8.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(lucide-react@0.468.0(react@19.2.0))(next@16.2.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(tailwindcss@4.1.18):
dependencies:
'@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@@ -3038,7 +3045,7 @@ snapshots:
'@radix-ui/react-slot': 1.2.4(@types/react@19.2.7)(react@19.2.0)
'@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
class-variance-authority: 0.7.1
fumadocs-core: 15.8.5(@types/react@19.2.7)(lucide-react@0.468.0(react@19.2.0))(next@16.1.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
fumadocs-core: 15.8.5(@types/react@19.2.7)(lucide-react@0.468.0(react@19.2.0))(next@16.2.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
lodash.merge: 4.6.2
next-themes: 0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
postcss-selector-parser: 7.1.1
@@ -3049,7 +3056,7 @@ snapshots:
tailwind-merge: 3.4.0
optionalDependencies:
'@types/react': 19.2.7
next: 16.1.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
next: 16.2.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
tailwindcss: 4.1.18
transitivePeerDependencies:
- '@mixedbread/sdk'
@@ -3686,6 +3693,8 @@ snapshots:
nanoid@3.3.11: {}
nanoid@3.3.12: {}
negotiator@1.0.0: {}
next-themes@0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
@@ -3693,25 +3702,25 @@ snapshots:
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
next@16.1.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
next@16.2.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
dependencies:
'@next/env': 16.1.5
'@next/env': 16.2.6
'@swc/helpers': 0.5.15
baseline-browser-mapping: 2.9.19
caniuse-lite: 1.0.30001766
baseline-browser-mapping: 2.10.33
caniuse-lite: 1.0.30001793
postcss: 8.4.31
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
styled-jsx: 5.1.6(react@19.2.0)
optionalDependencies:
'@next/swc-darwin-arm64': 16.1.5
'@next/swc-darwin-x64': 16.1.5
'@next/swc-linux-arm64-gnu': 16.1.5
'@next/swc-linux-arm64-musl': 16.1.5
'@next/swc-linux-x64-gnu': 16.1.5
'@next/swc-linux-x64-musl': 16.1.5
'@next/swc-win32-arm64-msvc': 16.1.5
'@next/swc-win32-x64-msvc': 16.1.5
'@next/swc-darwin-arm64': 16.2.6
'@next/swc-darwin-x64': 16.2.6
'@next/swc-linux-arm64-gnu': 16.2.6
'@next/swc-linux-arm64-musl': 16.2.6
'@next/swc-linux-x64-gnu': 16.2.6
'@next/swc-linux-x64-musl': 16.2.6
'@next/swc-win32-arm64-msvc': 16.2.6
'@next/swc-win32-x64-msvc': 16.2.6
sharp: 0.34.5
transitivePeerDependencies:
- '@babel/core'
@@ -3766,7 +3775,7 @@ snapshots:
postcss@8.4.31:
dependencies:
nanoid: 3.3.11
nanoid: 3.3.12
picocolors: 1.1.1
source-map-js: 1.2.1
@@ -3947,14 +3956,14 @@ snapshots:
dependencies:
compute-scroll-into-view: 3.1.1
semver@7.7.3:
semver@7.8.1:
optional: true
sharp@0.34.5:
dependencies:
'@img/colour': 1.0.0
'@img/colour': 1.1.0
detect-libc: 2.1.2
semver: 7.7.3
semver: 7.8.1
optionalDependencies:
'@img/sharp-darwin-arm64': 0.34.5
'@img/sharp-darwin-x64': 0.34.5
+259
View File
@@ -0,0 +1,259 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package errors_test
import (
stderrors "errors"
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/require"
nodeerrors "github.com/luxfi/node/errors"
)
// tempErr is a test helper that implements the optional Temporary() interface
// exercised by IsTemporary.
type tempErr struct {
msg string
temp bool
}
func (t *tempErr) Error() string { return t.msg }
func (t *tempErr) Temporary() bool { return t.temp }
func TestWrappedError_ErrorFormat(t *testing.T) {
require := require.New(t)
base := stderrors.New("disk gone")
withMsg := nodeerrors.Wrap(base, nodeerrors.CategoryDatabase, "open failed")
require.Equal("[database] open failed: disk gone", withMsg.Error())
// Empty message — formatter omits the message portion.
emptyMsg := nodeerrors.Wrap(base, nodeerrors.CategoryNetwork, "")
require.Equal("[network] disk gone", emptyMsg.Error())
}
func TestWrap_NilPassThrough(t *testing.T) {
require.Nil(t, nodeerrors.Wrap(nil, nodeerrors.CategoryDatabase, "ignored"))
require.Nil(t, nodeerrors.WrapWithContext(nil, nodeerrors.CategoryDatabase, "ignored", map[string]interface{}{"k": "v"}))
}
func TestWrap_AllocatesContextMap(t *testing.T) {
wrapped := nodeerrors.Wrap(stderrors.New("x"), nodeerrors.CategoryState, "msg")
we := asWrapped(t, wrapped)
require.NotNil(t, we.Context)
// Should be writable without panic.
we.Context["k"] = "v"
require.Equal(t, "v", we.Context["k"])
}
func TestWrapWithContext_PreservesContext(t *testing.T) {
ctx := map[string]interface{}{"tx_id": "0xabc", "height": uint64(42)}
wrapped := nodeerrors.WrapWithContext(nodeerrors.ErrCorrupted, nodeerrors.CategoryDatabase, "block load", ctx)
we := asWrapped(t, wrapped)
require.Equal(t, ctx, we.Context)
require.Equal(t, nodeerrors.CategoryDatabase, we.Category)
require.Equal(t, "block load", we.Message)
}
func TestWrappedError_UnwrapAndIs(t *testing.T) {
require := require.New(t)
wrapped := nodeerrors.Wrap(nodeerrors.ErrNotFound, nodeerrors.CategoryDatabase, "lookup")
require.True(stderrors.Is(wrapped, nodeerrors.ErrNotFound))
require.False(stderrors.Is(wrapped, nodeerrors.ErrClosed))
// errors.As must reach the WrappedError.
var target *nodeerrors.WrappedError
require.True(stderrors.As(wrapped, &target))
require.Equal(nodeerrors.ErrNotFound, target.Unwrap())
}
func TestWrappedError_DoubleWrapChain(t *testing.T) {
require := require.New(t)
inner := nodeerrors.Wrap(nodeerrors.ErrTimeout, nodeerrors.CategoryNetwork, "dial")
outer := nodeerrors.Wrap(inner, nodeerrors.CategoryNetwork, "rpc")
// Sentinel survives through both wrap layers via Unwrap chain.
require.True(stderrors.Is(outer, nodeerrors.ErrTimeout))
require.True(nodeerrors.IsTimeout(outer))
}
func TestIsNotFound(t *testing.T) {
require := require.New(t)
require.True(nodeerrors.IsNotFound(nodeerrors.ErrNotFound))
require.True(nodeerrors.IsNotFound(fmt.Errorf("lookup: %w", nodeerrors.ErrNotFound)))
require.False(nodeerrors.IsNotFound(nodeerrors.ErrClosed))
require.False(nodeerrors.IsNotFound(nil))
}
func TestIsClosed(t *testing.T) {
require := require.New(t)
require.True(nodeerrors.IsClosed(nodeerrors.ErrClosed))
require.True(nodeerrors.IsClosed(fmt.Errorf("shutdown: %w", nodeerrors.ErrClosed)))
require.False(nodeerrors.IsClosed(nodeerrors.ErrTimeout))
require.False(nodeerrors.IsClosed(nil))
}
func TestIsTimeout(t *testing.T) {
require := require.New(t)
require.True(nodeerrors.IsTimeout(nodeerrors.ErrTimeout))
require.True(nodeerrors.IsTimeout(fmt.Errorf("connect: %w", nodeerrors.ErrTimeout)))
require.False(nodeerrors.IsTimeout(nodeerrors.ErrNotFound))
require.False(nodeerrors.IsTimeout(nil))
}
func TestIsTemporary_SentinelErrors(t *testing.T) {
require := require.New(t)
require.True(nodeerrors.IsTemporary(nodeerrors.ErrTimeout))
require.True(nodeerrors.IsTemporary(nodeerrors.ErrRateLimited))
require.True(nodeerrors.IsTemporary(nodeerrors.ErrResourceExhausted))
// Permanent / unrelated sentinels are not temporary.
require.False(nodeerrors.IsTemporary(nodeerrors.ErrNotSupported))
require.False(nodeerrors.IsTemporary(nodeerrors.ErrInvalidInput))
require.False(nodeerrors.IsTemporary(stderrors.New("random")))
}
func TestIsTemporary_TemporaryInterface(t *testing.T) {
require := require.New(t)
require.True(nodeerrors.IsTemporary(&tempErr{msg: "blip", temp: true}))
require.False(nodeerrors.IsTemporary(&tempErr{msg: "fatal", temp: false}))
}
func TestIsPermanent(t *testing.T) {
require := require.New(t)
permanent := []error{
nodeerrors.ErrNotSupported,
nodeerrors.ErrDeprecated,
nodeerrors.ErrInvalidInput,
nodeerrors.ErrInvalidSignature,
nodeerrors.ErrInvalidFormat,
nodeerrors.ErrForbidden,
nodeerrors.ErrUnauthorized,
}
for _, err := range permanent {
require.Truef(nodeerrors.IsPermanent(err), "expected permanent: %v", err)
}
require.False(nodeerrors.IsPermanent(nodeerrors.ErrTimeout))
require.False(nodeerrors.IsPermanent(nodeerrors.ErrRateLimited))
require.False(nodeerrors.IsPermanent(nil))
// fmt.Errorf %w chain still resolves to permanent.
require.True(nodeerrors.IsPermanent(fmt.Errorf("validate: %w", nodeerrors.ErrInvalidSignature)))
}
func TestGetCategory_FromWrappedError(t *testing.T) {
require := require.New(t)
wrapped := nodeerrors.Wrap(stderrors.New("x"), nodeerrors.CategoryResource, "oom")
require.Equal(nodeerrors.CategoryResource, nodeerrors.GetCategory(wrapped))
}
func TestGetCategory_InferredFromSentinel(t *testing.T) {
require := require.New(t)
cases := []struct {
err error
want nodeerrors.Category
}{
{nodeerrors.ErrNotFound, nodeerrors.CategoryDatabase},
{nodeerrors.ErrClosed, nodeerrors.CategoryDatabase},
{nodeerrors.ErrTimeout, nodeerrors.CategoryNetwork},
{nodeerrors.ErrConnectionClosed, nodeerrors.CategoryNetwork},
{nodeerrors.ErrInvalidInput, nodeerrors.CategoryValidation},
{nodeerrors.ErrInvalidSignature, nodeerrors.CategoryValidation},
{nodeerrors.ErrNotInitialized, nodeerrors.CategoryState},
{nodeerrors.ErrAlreadyExists, nodeerrors.CategoryState},
{nodeerrors.ErrResourceExhausted, nodeerrors.CategoryResource},
{nodeerrors.ErrOutOfMemory, nodeerrors.CategoryResource},
{nodeerrors.ErrUnauthorized, nodeerrors.CategoryPermission},
{nodeerrors.ErrForbidden, nodeerrors.CategoryPermission},
{stderrors.New("mystery"), nodeerrors.CategoryUnknown},
}
for _, tc := range cases {
require.Equalf(tc.want, nodeerrors.GetCategory(tc.err),
"category for %v", tc.err)
}
}
func TestGetCategory_WrappedTakesPrecedenceOverInference(t *testing.T) {
// ErrNotFound would normally infer CategoryDatabase. If wrapped with a
// different explicit category, the wrapped value must win.
wrapped := nodeerrors.Wrap(nodeerrors.ErrNotFound, nodeerrors.CategoryInternal, "ctx")
require.Equal(t, nodeerrors.CategoryInternal, nodeerrors.GetCategory(wrapped))
}
func TestMulti_EmptyError(t *testing.T) {
m := &nodeerrors.Multi{}
require.Equal(t, "no errors", m.Error())
require.Nil(t, m.Err())
}
func TestMulti_SingleErrorUnwrapsToInnerString(t *testing.T) {
m := &nodeerrors.Multi{}
m.Add(stderrors.New("only one"))
require.Equal(t, "only one", m.Error())
require.Equal(t, m, m.Err())
}
func TestMulti_MultipleErrorsContainsEach(t *testing.T) {
m := &nodeerrors.Multi{}
m.Add(stderrors.New("first"))
m.Add(stderrors.New("second"))
m.Add(stderrors.New("third"))
s := m.Error()
require.True(t, strings.Contains(s, "multiple errors"), "got: %s", s)
require.True(t, strings.Contains(s, "first"))
require.True(t, strings.Contains(s, "second"))
require.True(t, strings.Contains(s, "third"))
}
func TestMulti_AddIgnoresNil(t *testing.T) {
m := &nodeerrors.Multi{}
m.Add(nil)
m.Add(nil)
require.Empty(t, m.Errors)
require.Nil(t, m.Err())
}
func TestJoin_AllNilReturnsNil(t *testing.T) {
require.Nil(t, nodeerrors.Join())
require.Nil(t, nodeerrors.Join(nil, nil, nil))
}
func TestJoin_DropsNilEntries(t *testing.T) {
err := nodeerrors.Join(nil, stderrors.New("real"), nil)
require.NotNil(t, err)
require.Equal(t, "real", err.Error())
}
func TestJoin_CombinesMultipleErrors(t *testing.T) {
err := nodeerrors.Join(
nodeerrors.ErrNotFound,
nodeerrors.ErrClosed,
)
require.NotNil(t, err)
s := err.Error()
require.True(t, strings.Contains(s, "multiple errors"), "got: %s", s)
require.True(t, strings.Contains(s, "not found"))
require.True(t, strings.Contains(s, "closed"))
}
// asWrapped extracts a *nodeerrors.WrappedError or fails the test.
func asWrapped(t *testing.T, err error) *nodeerrors.WrappedError {
t.Helper()
var w *nodeerrors.WrappedError
require.True(t, stderrors.As(err, &w), "expected *WrappedError, got %T", err)
return w
}

Some files were not shown because too many files have changed in this diff Show More