Compare commits

..
Author SHA1 Message Date
zeekayandHanzo Dev 97e0a0b704 v1.36.7: xvm native-nested composites — 2.8x X-tx build, import/export too
Consume utxo v0.5.6 + zap v1.2.3. X-chain BaseTx / ExportTx / ImportTx now build
their transferable out/in lists as native ZAP AddObjectPtr object-lists (no
per-container envelope prefix, no blob concat, no length lists) via
wire.AppendTransferable{Out,In} + TransferableXFromObject. baseTxWire composes
wire.XVMTransferOut/In directly. Removed the standalone transferableOut/InBytes
node helpers. Composite money-move build 2551ns/37allocs (byte-blob) ->
913ns/11allocs (native-nested), 2.8x. Full xvm suite green (round-trip/state/
block/executor/import/export); components-lux + wallet-x + platformvm-txs green.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-14 06:38:42 -07:00
zeekayandHanzo Dev 24b67abdd0 v1.36.6: consume faster write path — zap v1.2.2 + utxo v0.5.3
zap v1.2.2 (zero-copy SetBytes + Builder pool) + utxo v0.5.3 (pooled wire
builders + SetBytesFixed) cut X-chain tx wire composition ~1.9x (2551->1345ns,
37->19 allocs on the isolated composite; the X build path benefits proportionally
without touching P-chain parse, which stays at 705ns/3allocs). P/X/xvm/
components-lux suites all green against the new deps.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-13 03:45:37 -07:00
zeekayandHanzo Dev 7ee56895d9 v1.36.5: no-replace hermetic build — chains v1.7.5 (pins real node v1.36.4), revert Dockerfile node replace
Supersedes v1.36.4's build recipe which used a build-time 'replace node => /build'
(forbidden: no local replace directives). Instead, chains v1.7.5 pins the real,
published node v1.36.4 tag, so the baked VM plugins resolve node the normal way —
Dockerfile just clones chains and builds; CHAINS_REF v1.7.4 -> v1.7.5; go.mod
chains v1.7.4 -> v1.7.5; genproto realigned. Zero replace directives anywhere.
tidy + -mod=readonly + cold 'go mod download all' clean.

Note: luxfi node/geth/utxo git tags are being periodically wiped by an external
tag-sync (root cause of the 'unknown revision' failures); all four pinned tags
(node v1.36.4, chains v1.7.5, utxo v0.5.1, geth v1.17.12) re-verified present
immediately before this build races the sync window.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-12 21:43:55 -07:00
zeekayandHanzo Dev bbce8e6e13 Dockerfile: build baked VM plugins against the HOST node (/build), not chains' pinned node
The plugin stage cloned chains and built each VM's cmd/plugin standalone, which
resolved chains' pinned github.com/luxfi/node@v1.30.6 — a wiped/disjoint release
tag (325 orphan commits, no merge-base with main) absent from the remote, so the
hermetic build died with 'unknown revision v1.30.6' and the required bridgevm/
mpcvm/zkvm plugins went missing (FATAL).

Fix: inject 'replace github.com/luxfi/node => /build' (the exact vX.Y.Z node source
being built) into every /tmp/chains go.mod before building. This is what the
existing 'plugins in lockstep with the host node' intent actually requires — the
baked plugins now match the node they run in instead of an ancient pin. chains is
the main module during the plugin build, so node(/build)'s own require of chains
resolves back to /tmp/chains (main-module-wins) — no version fetch, no loop. Also
bumped CHAINS_REF default v1.7.2 -> v1.7.4 to match node's go.mod chains pin.

Verified: all 10 required plugins build cold (fresh GOMODCACHE, CGO_ENABLED=0) into
real binaries against local node v1.36.4.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-12 17:35:39 -07:00
zeekayandHanzo Dev 9930e98d26 fix(deps): pin published utxo v0.5.1, drop local ../utxo replace — unbreak hermetic build
The xvm codec kill depends on utxo/wire's TransferableOut/In + nftfx/propertyfx
envelopes, which were only in the local ~/work/lux/utxo working tree (pinned via
'replace github.com/luxfi/utxo => ../utxo'). That local-path replace works for a
local build but breaks the hermetic Docker/CI build ('open /utxo/go.mod: no such
file or directory'). Published those two additive wire commits as utxo v0.5.1
(clean ff on utxo main, +2 over v0.5.0) and pin it here — the exact code node was
built+tested against. Dropped the replace. Cold-cache 'go mod download all' is now
clean; xvm/components/lux/wallet tests green against v0.5.1.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-12 17:17:36 -07:00
zeekayandHanzo Dev 64a770b4cc vms/pcodecs: DELETE the last reflection dispatcher — ZAP native everywhere (v1.36.4)
The codec kill is complete. Every node VM (platformvm, xvm, warp, proposervm,
components/lux) and every chains app-chain VM (bridgevm, zkvm, mpcvm — now native
in chains v1.7.4) marshals via native ZAP struct-is-wire. Nothing imports
node/vms/pcodecs anymore, so the package — the last reflection/serialize-tag
dispatcher (a thin alias over proto/zap_codec's LinearCodec) — is deleted.

- rm vms/pcodecs + vms/pcodecs/pcodecsmock (zero consumers; verified whole tree).
- go.mod: chains v1.7.2 -> v1.7.4 (the pcodecs-free chains), precompile
  v0.19.0 -> v0.19.1. Realigned genproto so the split googleapis/rpc module
  resolves by longest-prefix (no monolith ambiguity); tidy clean, -mod=readonly
  build clean.
- version -> v1.36.4 (constants.go defaultPatch, compatibility.json under the
  same RPCChainVM protocol as v1.36.3 — no protocol change, only a codec rip).

There is one and only one way to put a struct on the wire: ZAP.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-12 16:54:25 -07:00
zeekayandHanzo Dev 6b25706366 wallet/x + components/lux: complete xvm native-wire consumer
Tail of the xvm codec kill (ddb3fbca93): the X-chain wallet builder + signer
now rebuild signed wire bytes as unsigned ‖ fx credential envelopes over the
native luxfi/utxo/wire form, and components/lux parses fx Inputs from their wire
envelope by concrete type. No linearcodec, no reflection on the wallet path.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-12 16:54:12 -07:00
zeekayandHanzo Dev ddb3fbca93 xvm -> native ZAP struct-is-wire: node-side codec kill COMPLETE
X-Chain fully off pcodecs (the last node consumer). kind.go (xkind 1-5), tx.go
(wire.SignedTx envelope), parser (reflect-map + dual LinearCodec DELETED, fx
dispatch by envelope TypeKind/ShapeKind), base/create_asset/operation/import/
export tx, initial_state, operation, block/{standard,parser,builder}, genesis
(native genesis_wire.go: marshalGenesis/parseGenesis + txs.ParseUnsignedTx),
vm/service/static_service/wallet_service/utxo-spender, metrics (pcodecs.Errs ->
errors.Join). ALL xvm test codec threading removed; xvm tests green.

Block-build invariant: writeTxList requires Initialized txs (mempool/parse hand
the builder canonical bytes) — errors on empty tx-bytes rather than a hidden
Initialize side-effect; state_test updated to Initialize its fixtures.

Transport foundation: rpcchainvm NewListener unix-socket fast path gated
LUXD_VM_UNIX_SOCKET=1 (default TCP, non-breaking; api/zap 159b27a infers unix
from socket-path addr). Consumes upstream utxo TransferableOut/In (4ce6a6e).

ZERO real node-side pcodecs consumers remain. vms/pcodecs kept only for 3
external luxfi/chains VMs (Wave B: zkvm/bridgevm/mpcvm) pending their migration.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-12 11:26:36 -07:00
zeekay b113618e5e Merge main: record Quasar-export lineage (content integrated in prior commit; codec-kill supersedes main's codec files) 2026-07-11 22:48:27 -07:00
zeekayandHanzo Dev 3f890bc98e Integrate main's Quasar-export + EVM v1.104.8 content into the codec-kill branch
The non-codec main changes (Dockerfile EVM_VERSION v1.104.8, chains/manager
GetContext size-chunking for behind-validator resync + Quasar EXPORT frontier
bridge, warp/signature, rpcchainvm/zap client) that the branch lacked. The
codec-era main files (codec.go/parse.go/tx.go etc.) are intentionally
superseded by this branch's native-ZAP struct-is-wire — not reintroduced.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-11 22:48:27 -07:00
zeekayandHanzo Dev 44d7651d16 block: reject trailing bytes in Parse (malleability guard) + port determinism test to native ZAP
RED-verified consensus-safety fix: zap.Parse truncates to the header size
field, so a block buffer with extra tail bytes wraps the SAME message but
ID=hash(bytes) differs — a block-hash malleability / fork vector. setID now
rejects msg.Size() != len(bytes) with ErrExtraSpace. (The P-chain TX envelope
already guards this at tx.go:66; only block Parse had the gap.)

Renamed codec_determinism_test.go -> block_determinism_test.go and converted
its assertions to native ZAP: New*Block + Parse(b) (no Codec), native golden
AbortBlock bytes (65B: zap header + kind/parent/height/time object), byte-
stability + BlockID-stability + trailing-bytes-rejected. Block + txs green.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-11 22:45:44 -07:00
zeekayandHanzo Dev dcde2167a8 node v1.36.3: binary self-reports true version + RPCChainVM compat entry
defaultMinor/Patch -> 36/3 (Dockerfile injects no version ldflags, so the
default IS the released binary's self-reported version; v1.36.0-2 shipped
self-reporting 1.32.11). v1.36.3 registered under RPCChainVM protocol 42.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-11 22:36:04 -07:00
zeekayandHanzo Dev 5a2035adb5 Merge feat/lp023-native-tx-codec: FULL codec kill — P-chain/warp/proposervm/components struct-is-wire
The LP-023 native-ZAP migration, complete except xvm (next patch):
- P-Chain: tx + block + state + genesis are the zap buffer (struct=wire, no
  codec, no serialize tags, no versions). Executor fold: CreateNetworkTx/
  ConvertNetworkTx with security.Mode (RestakeParent × own-set Admission/
  Manager) — one definition shared by wire/executor/state; CreateChainTx is
  the sole chain constructor (block-atomic L1 spawn per LP-018).
- warp: registration-order codec (hard-fork footgun) → explicit wkind/mkind/
  pkind discriminator bytes; ChainToL1ConversionID = sha256(Marshal()) same-
  encoder invariant; native wire baselines pinned.
- proposervm block/state/summary, components/{lux,message,keystore,index},
  evm/{predicate,lp176}, example/xsvm: native ZAP; pcodecs consumers reduced
  to xvm only.
- consensus v1.36.1 + node-side view-change plumbing ripped (merged with
  main's parallel rip; main's Nova tombstones kept).
- /ext/ endpoint prefix retired: /v1/ is THE endpoint.
- All codec-era tests restored/converted to struct-is-wire (0 parked); staker
  dispatch-safety guard added.
- Deps: published pins only (consensus v1.36.1, zap v1.2.0, utxo v0.3.7,
  crypto v1.20.0); no local replaces.

Merged-tree gate: go build ./... EXIT 0; go test ./... = 155 ok / 0 FAIL.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-11 22:36:04 -07:00
zeekayandHanzo Dev b4992860f6 deps: publishable pins — zap v1.2.0, drop local replaces
Node builds + full test sweep green (155 ok / 0 FAIL) against PUBLISHED
modules only: consensus v1.36.1, zap v1.2.0, utxo v0.3.7. The local zap/utxo
replaces are gone; the upstream nftfx/propertyfx wire (utxo e790d39) ships as
v0.3.8 with the xvm struct-is-wire patch, which is the only remaining pcodecs
consumer and lands in the next release.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-11 22:32:00 -07:00
zeekayandHanzo Dev 0ba5d05bd8 Retire /ext/ endpoint prefix: /v1/ is THE endpoint
Route registration, client URL builders, and log lines all move to /v1/
(wallet primary api, chains/rpc handler_manager+chain_integration, xvm
client+wallet_client, xsvm api client, multi-network example, manager logs).
Zero /ext/ literals remain. One endpoint namespace, no transition alias.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-11 19:14:51 -07:00
zeekayandHanzo Dev 926ddaafa4 go.mod: replace luxfi/utxo -> local (nftfx/propertyfx wire envelopes)
Consumes the upstream fx-wire addition (utxo commit e790d39: TypeKindNFT/
Property + 6 shapes + NextEnvelope) needed for the xvm struct-is-wire
migration. Local replace like zap; publish as utxo v0.3.8 with the node
release.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-11 17:55:48 -07:00
zeekayandHanzo Dev eda299d30d node v1.36.2: consensus v1.36.1 finality fix on the known-good v1.36.0 base
v1.36.1 bundled staged-but-runtime-untested ZAP work (api v1.0.15->v1.0.16,
vm v1.2.5->v1.2.7, EVM v1.104.7->v1.104.8, platformvm sole-codec cutover,
rpcchainvm Quasar-export plugin-boundary wiring). That bump breaks VM
initialization on EVERY chain at boot:
  failed to initialize VM: zap decode initialize response: unexpected EOF
— a node<->plugin ZAP handshake mismatch (the plugins were not synced to the
new api/vm ZAP wire). Mainnet would not boot on v1.36.1.

v1.36.2 drops that unfinished ZAP transition and ships ONLY the orthogonal
consensus finality fix (v1.36.1 — close the intermediate-ancestor load-livelock,
RED-SHIP 0 crit/high/med) on the known-good v1.36.0 dependency set (api v1.0.15,
vm v1.2.5, EVM v1.104.7). Boots clean AND carries the finality fix. luxd builds
green (-mod=mod). The ZAP Quasar-export / codec cutover ships separately once the
plugins are synced to the api/vm bump and it is runtime-validated.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-11 17:45:11 -07:00
zeekayandHanzo Dev 6a766516b1 warp -> native ZAP struct-is-wire: kill the registration-order codec (hard-fork footgun)
All 3 warp codec registries deleted (codec.go, message/codec.go,
payload/codec.go). Every dispatched type now carries an EXPLICIT 1-byte
discriminator at object offset 0 — the invariant is DATA, not registration
order:

  warp wkind:    0x00 BitSetSignature, 0x01 CoronaSignature,
                 0x02 EncryptedWarpPayload, 0x03 HybridBLSCoronaSignature,
                 0x04 TeleportMessage, 0x05 TeleportTransferPayload,
                 0x06 TeleportAttestPayload   (ids = old registration order)
  message mkind: 0 ChainToL1Conversion, 1 RegisterL1Validator,
                 2 L1ValidatorRegistration, 3 L1ValidatorWeight
  payload pkind: 0 Hash, 1 AddressedCall

CONSENSUS-CRITICAL invariant made structural: ChainToL1ConversionID =
sha256(ChainToL1ConversionData.Marshal()) — the ID calls the SAME encoder, so
ID/Marshal skew (the bug in the reverted agent attempt) is impossible. The
native hash preimage is pinned as a golden with field-offset assertions
(verified by the L1 staking contract => consensus surface).

Message = {unsigned bytes, wkind-tagged sig bytes} container object;
UnsignedMessage = {networkID u32, sourceChainID 32B, payload} @44B header.
wire_baseline_test.go rewritten: pins the native hex golden for all 7 wkinds
+ structural discriminator checks + signature dispatch round-trips. Old-codec
sentinels -> zap errors. Whole node builds; warp+platformvm+proposervm green.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-11 17:44:13 -07:00
zeekayandHanzo Dev 2d227dd3aa Full-codec-kill wave 2: proposervm + xsvm + components + evm + platformvm-residual → native ZAP
Kills pcodecs from 3 complete subsystems (verified go test ./... = 155 ok / 0 FAIL):
- proposervm: block/state/summary struct-is-wire (blockwire.go + statewire.go;
  deleted block/state/summary codec.go). blockKind bytes: reserved=0 signed=1
  option=2. Epoch inlined in unsigned object. proposer/windower chainSource seed
  = binary.LittleEndian (byte-identical to old wrappers.Packer.UnpackLong).
- example/xsvm: tx/block/genesis native Marshal (deleted 3 codec.go); create-chain
  example uses genesis.Marshal().
- components: message.Tx native (deleted codec.go); keystore codec shell removed;
  index pcodecs.LongLen → local const.
- evm/{predicate,lp176}: off pcodecs.
- platformvm residual: state metadata + genesis + metrics + signer + stakeable
  native, vestigial serialize tags removed.

REMAINING (careful solo, agents weekly-capped): warp (reverted — agent left an
ID≠Marshal consensus inconsistency in ChainToL1Conversion; needs proper review,
not a golden-regen) + xvm (reverted — barely started). pcodecs package stays
until those 2 land. /ext/→/v1/ endpoint change also pending.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-11 17:19:20 -07:00
zeekayandHanzo Dev a78aa07e6f node v1.36.1: consensus v1.36.1 (close finality load-livelock)
Bump github.com/luxfi/consensus v1.36.0 -> v1.36.1 — canonicalizes the
intermediate-ancestor walk (pathFromTip alias-collapse) that livelocked finality
under sustained saturation (the one finality path the mainnet-644 canonicalization
missed). RED-SHIP, 0 crit/high/med: alias-collapse can only stand in a
byte-identical inner execution (canonicalRep = CanonicalID, a state-root-binding
hash), so no fork risk HEAD lacked. Carries the v1.36.0 platformvm sole-codec
cutover + ZAP Quasar export already on main.

luxd builds green against consensus v1.36.1 (GOFLAGS=-mod=mod, CI parity).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-11 10:33:58 -07:00
zeekay dd04027476 node v1.36.0: api v1.0.16 + vm v1.2.7 (ZAP Quasar export) + EVM_VERSION v1.104.8
Folds the native-ZAP codec cutover (platformvm sole-codec) + the Quasar EXPORT-frontier
carry across the rpcchainvm ZAP boundary. Whole-node build green.
2026-07-11 10:33:58 -07:00
zeekayandHanzo Dev 28e34ba003 vms/platformvm: rip multi-version codec — ZAP-native is the sole P-Chain codec
Collapse P-Chain tx/block/state serialization to a single ZAP-native codec
(CodecVersion=1). This is the gate before the Nova re-genesis: one write
path, one read path, no version dispatch.

Removed the whole multi-version surface:
- txs: registerV0TxTypes, CodecVersionV0/V1/V2, CodecVersionForTimestamp,
  CodecForTimestamp, CodecAllowsRead, CodecRequiresLegacy, the Version
  alias, and codec_activation.go (ZAPCodecActivationTimestamp).
- block: v0Codec/v0GenesisCodec, the block/v0 package, the lift_v0 path;
  Parse is now single-version.
- state: the v0-probe codec_helpers (it Marshal'd at version 0, which
  would fire a false warning every boot); GenesisCodec.Unmarshal inlined.
- genesis: single-version alias + comment cleanup.
- warp/bench/network: stale "linearcodec"/"reflectcodec" comments corrected
  (all already ride the ZAP-backed pcodecs shim; wire unchanged).
- wallet/chain/p: txs.Version -> txs.CodecVersion.

Value 1 is retained (not renumbered) so no tx ID, block ID, or state root
changes: the surviving codec is exactly the ZAP-native slot the chain
already writes. Every existing serialization golden stays byte-for-byte.

Determinism proof (codec_zap_test.go, block/codec_determinism_test.go):
golden tx/block bytes + IDs, marshal idempotency, round-trip byte-stability
for every tx and block type, and trailing/truncated/wrong-version
rejection. grep -rnE 'linearcodec|reflectcodec|CodecVersion(ForTimestamp|V0|V1)'
vms/platformvm/ is empty.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-11 10:33:58 -07:00
zeekayandHanzo Dev f2dcc91cd3 chains, rpcchainvm: wire the Quasar EXPORT tier across the plugin boundary (client + manager)
Closes the deploy-blocking gap in two-tier consensus v1.36: the C-Chain EVM runs
as a SEPARATE rpcchainvm plugin process, so the chain manager's vmTyped is the
rpcchainvm *Client — which did NOT implement SetLastQuasarFinalized /
LastQuasarHeight (those live only on the concrete *evm.VM, the plugin SERVER
side). The manager's capability assert therefore returned !ok in production, the
QuasarObserver stayed nil, and finalized/safe + the warp export gate stuck at
genesis. Nova consensus was unaffected (node-side).

rpcchainvm/zap client (*Client):
- SetLastQuasarFinalized / LastQuasarHeight: ZAP-call the plugin (Msg 60/61).
  Both short-circuit if the plugin did not advertise CapQuasarExport, so wiring
  them is harmless for a generic plugin (no per-finalization no-op RPC).
- SupportsQuasarExport: reports the capability captured (atomically, once) from
  the Initialize handshake's InitializeResponse.Capabilities.
- Fire-and-forget Set (logged, not returned — the caller is the consensus
  observer); Height returns 0 on any failure (boot re-seed treats it as empty).

chains/manager createChain:
- quasarExportVM interface (values-not-places: the capability is a value, not a
  static type property). Gate the observer + boot re-seed on the capability:
  a *Client reports it via SupportsQuasarExport (false → Nova-only, exactly the
  old !ok semantics — no cross-process spam); a VM WITHOUT the probe (an
  in-process VM whose concrete methods we hold directly) is treated as capable,
  preserving the direct-wire path. Observer is set BEFORE NewRuntime captures
  netCfg; the seed runs after, carried by the exportVM value.

Test (client_quasar_test.go, -race): the REAL cross-process path — node *Client
-> ZAP wire -> luxfi/vm/rpc server -> a fake VM that satisfies the SAME
capability interface as *evm.VM. Capability captured from the handshake; a pushed
height crosses the wire into the VM; the VM's height round-trips back; a
non-capable plugin is a graceful Nova-only no-op.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-11 10:33:58 -07:00
zeekayandHanzo Dev 7123b7399e Restore+convert remaining txs tests to struct-is-wire + dispatch-safety guard
Converts the per-type P-chain tx tests to native New*Tx + accessor methods +
roundTrip/SyntacticVerify (agents, verified): add_validator, add_chain_validator,
create_blockchain, disable/increase/register/set_weight/remove_chain/
transfer_ownership L1, add_permissionless_delegator, transform_chain. Every
error-path sentinel preserved by driving the bad value THROUGH the constructor
(pure byte-writer). Mock-based cases (fxmock/luxmock/verifymock, impossible on
an immutable zap buffer) reproduced with REAL unspendable owners / unsorted
inputs → real sentinels (ErrOutputUnspendable, ErrInputIndicesNotSortedUnique).
Only the un-reproducible 'already verified' cached-flag case dropped per file.

NEW staker_dispatch_guard_test.go pins the interface-satisfaction invariant the
staker type-switches depend on: struct-is-wire made *AddValidatorTx satisfy both
ValidatorTx+DelegatorTx (safe — ValidatorTx checked first everywhere), but
*AddDelegatorTx must NOT satisfy ValidatorTx (verified: lacks
ValidationRewardsOwner/Shares) or delegators would mis-route. Guard fails loudly
if a future edit breaks either property.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-11 05:46:10 -07:00
zeekayandHanzo Dev 43cbbac511 Restore+convert genesis/state/network tests to struct-is-wire (4 files, zero coverage dropped)
- genesis: field->method on *txs.AddValidatorTx (.Validator().NodeID/.End/
  .StakeOuts()); 3 error-path builders preserved (errUTXOHasNoValue,
  errValidatorHasZeroWeight, errValidatorAlreadyExited).
- state/staker: generateStakerTx via NewAddPermissionlessValidatorTx; the
  mutable-Signer-mock trick (impossible on immutable zap buffer) ->
  NewMockScheduledStaker.PublicKey()=errCustom, errCustom preserved exactly.
- network/gossip+network: nil-buffer &txs.BaseTx{} -> newBaseTx helper (avoids
  InputIDs() panic); SetBytes 2-arg->1-arg; all expectedErr preserved
  (ErrDuplicateTx, ErrTxTooLarge, ErrConflictsWithOtherTx, ErrMempoolFull...).
go test ./genesis/ ./state/ ./network/ = all ok.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-11 05:45:21 -07:00
zeekayandHanzo Dev 7ea0c0383f Restore+convert fee/mempool/txheap tests to struct-is-wire (6 files, green)
- fee/complexity: kept all 5 component tests + error paths; dropped codec
  byte-cross-check + dead pre-LP-023 BE-hex fixtures, REPLACED with native
  TestTxComplexity (visitor single-path + batch-additivity + ErrUnsupportedTx).
  Removed TestConvertNetworkToL1ValidatorComplexity (fn deleted; per-validator
  convert complexity now inline in complexityVisitor.ConvertNetworkTx).
- fee/static + dynamic_calculator: skipped hex-loops -> native (TxFee/
  CreateChainTxFee/positive-priced supported, ErrUnsupportedTx unsupported).
- mempool/{auth,mempool}: native New*Tx + Initialize(); strict-PQ credential
  gate (ErrLegacyCredentialUnderStrictPQ) + Add/Get/Peek/Remove/DropExpired.
- txheap/by_end_time: NewAddValidatorTx + heap-ordering assertions.
go test ./txs/fee/ ./txs/mempool/ ./txs/txheap/ = ok (17 tests, no skips).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-11 05:40:20 -07:00
hanzo-dev 6b599d29e1 ci: run linux jobs on lux-build-amd64 ARC scale set (no GitHub-hosted builders) 2026-07-11 00:14:50 -07:00
zeekayandHanzo Dev c5e999487c Green the 8 post-bump test failures (real fixes, no delete-to-hide)
- spending.go: harden spendingTx.Bytes() against nil msg (uninitialized tx no
  longer panics the spend-verification path; production always sets msg).
- config/genesis-builder tests: codec-era Codec.Marshal -> native g.Bytes();
  AddValidator field access -> methods (Weight()/Validator()/StakeOuts());
  drop unused pchaintxs imports.
- config test: consensus params K=30 alpha 16->20/25 to satisfy v1.36's tighter
  BFT bound (2*alpha-K >= floor((K-1)/3)+1); assertions updated.
- version/compatibility.json: register Current v1.32.11 under RPCChainVM proto 42
  (pre-existing registration gap).
- cevm e2e: genesis fixture completed with gasLimit+difficulty (strict geth
  genesis validation in the installed CEvm plugin; pre-existing fixture drift).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-10 21:20:17 -07:00
zeekayandHanzo Dev 9626ca6c2f Bump consensus v1.35.37 -> v1.36.1 + rip node-side view-change plumbing
v1.36 upstreamed round-scoped view-change INTO the engine (internal prevote/POL
in attestation/reconcile/cert), dropping the external hooks config.Parameters.
ViewChange + Runtime.HandleIncomingPrevote. Ripped the node's now-redundant
external plumbing: the ViewChange enable block (LUX_CONSENSUS_VIEW_CHANGE gate),
the quorumKindPrevote gossip routing + HandleIncomingPrevote call, the dead
BroadcastPrevote gossiper method, and the quorumKindPrevote envelope kind. The
engine owns view-change natively now (fail-secure halt on 2a-n>f unchanged).

Whole node builds EXIT 0 on v1.36.1; luxd binary compiles (55M).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-10 21:07:38 -07:00
zeekayandHanzo Dev 87a57925f3 components/lux UTXO value tree -> native ZAP (Wave-A Substrate)
The shared UTXO value tree (UTXO/TransferableOutput/TransferableInput/Asset/
UTXOID/BaseTx/Metadata + OutputOwners path) gets native struct-is-wire
Marshal/Unmarshal (new marshal.go), replacing every pcodecs.Manager. Codec
param dropped from utxo_state / atomic_utxos / transferables (Sort* sorts on
inner fx wire Bytes()); flow_checker pcodecs.Errs -> errors.Join.

Node consensus persistence uses luxfi/utxo (unchanged); components/lux is the
tx-builder/#58 surface — encoding-only change, type tree NOT collapsed (#58
respected). Both P and X share the encoding => internally consistent, re-genesis
safe. Whole node builds EXIT 0; 8 components/lux round-trips + P-chain txs/block
tests green.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-10 21:01:11 -07:00
zeekayandHanzo Dev 3b4d16bbc5 indexer p-chain example: block.Parse(b) codec-free — WHOLE NODE builds
Last P-chain codec consumer. go build ./... = EXIT 0. The platformvm codec
(pcodecs/txs.Codec/serialize tags) is fully dead; P-chain tx + block + state are
native ZAP struct-is-wire end to end. Remaining codec surface is X-chain +
proposervm + warp (Wave A), which still carry their own (intact) codecs.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-10 14:35:18 -07:00
zeekayandHanzo Dev 511a804016 Decomplect: CreateChainTx is the sole chain constructor (kill genesis-chains stub)
CreateNetworkTx no longer carries genesis chains — the executor never created
them, so tx.Chains() was a latent stub (declared, verified, silently dropped).
Model A resolves it by decomplection, not by implementing a second chain path:

  - CreateNetworkTx = ∅→Network birth (owner + security.Mode + own validator set).
  - CreateChainTx = the ONE chain constructor. An L1 spawn is a BLOCK of
    CreateNetworkTx + N CreateChainTx (block-atomic, not tx-atomic).
  - Removed NetworkChain, its wire (writeNetworkChains/readNetworkChains/
    ncStride/sliceIDs), the chain error set, MaxNetworkChains, the chains param.
  - managerChainIdx (index into genesis chains) -> managerChainID (direct
    ids.ID), now SYMMETRIC with ConvertNetworkTx's manager ref. ids.Empty =>
    P-Chain-governed; a set chainID => Contract-governed staking-contract host.

Net: less code (write as little as possible), one-and-one-way chain creation,
no stub. platformvm build+vet PASS; SovereignL1/InheritedL2/HybridL2/Convert
round-trips PASS.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-10 14:33:13 -07:00
zeekayandHanzo Dev c44cc2289f platformvm builds GREEN off the codec: executor fold + full consumer flip
vms/platformvm/... build + vet PASS with pcodecs/txs.Codec gone from the VM.

Executor semantics (standard_tx_executor):
- registerOwnSet(): shared primitive — per-validator state.L1Validator with
  native owner blobs (txs.MarshalOwner/UnmarshalOwner, no codec), active-set
  capacity check, EndAccumulatedFee=balance+accruedFees, SetNetToL1Conversion
  manager-authority recording (byte-for-byte legacy tail).
- ConvertNetworkTx: promote endomorphism (owner-authorized, folds old
  ConvertNetworkToL1Tx), gated by security.Mode.Manager.
- CreateNetworkTx: base (AddNet/SetNetOwner) + sovereign path registers own set.
- Deleted CreateSovereignL1Tx + ConvertNetworkToL1Tx.
- txs.UnmarshalOwner added: canonical owner marshal/unmarshal pair, no codec.

All ~13 txs.Visitor impls carry ConvertNetworkTx; gossip/block Parse(b) codec-free;
wallet/network/primary off txs.Codec.

KNOWN GAP (pending design decision, NOT silently green): CreateNetworkTx reads
tx.Chains() only to derive managerChainID — it does NOT create the genesis
chains (no AddChain). Atomic-spawn-with-chains vs decomplect-to-CreateChainTx is
the open call. 17 codec-era _test.go parked as .bak for follow-up rewrite.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-10 14:25:08 -07:00
zeekayandHanzo Dev c73eba739f Flip 4 platformvm consumers off txs.Codec (MarshalOwner + New*Tx + native genesis Bytes)
- txs.MarshalOwner(any): ONE canonical owner encoding (same layout as embedded
  tx owner), lifted to a standalone buffer for lock-owner hash keys in utxo
  handler+verifier. Replaces txs.Codec.Marshal(owner). Re-genesis-safe (ownerID
  only matched within a tx's own consumed/produced sets).
- validators/manager: chain.ChainID -> chain.ChainID() (field->method).
- api/static_service: genesis txs via NewAddValidatorTx / NewAddPermissionless
  ValidatorTx / NewCreateChainTx + codec-free Initialize(); genesis blob via
  native g.Bytes(). Mid-flip: executor + remaining visitors still pending.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-10 13:40:18 -07:00
zeekayandHanzo Dev f1a3e85b8e Rename LuxAddress -> UTXOAddr (decomplect place-from-value)
LuxAddress in airdrop.AirdropClaim + bridgevmroot.SignerLeaf is the NATIVE
20-byte Lux address (ids.ShortID / [20]byte), held beside the EVM common.Address
it disambiguates. 'Lux' prefix is banned place-in-value naming; the canonical
pair is EVMAddr / UTXOAddr. Named by address KIND, not brand.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-10 13:30:17 -07:00
zeekayandHanzo Dev d008eba286 security.Mode: decomplect network security into its own package
Pull the network security model out of the tx wire into a small orthogonal
package (vms/platformvm/security): pure values, stdlib-only, no zap/tx/state
deps. One definition — security.Mode{RestakeParent, Admission, Threshold,
Manager} + Sovereign() + Valid() — composed by tx wire, executor, and state
(Rich Hickey: values not places; Rob Pike: no stutter, small orthogonal pkg).

Two orthogonal axes replace the flat Inherited/Sovereign byte:
  - RestakeParent: lean on parent's validator set
  - own set: Admission(NoOwnSet|Open|Gated) + Manager(PChain|Contract)
Their product spans every mode incl. HYBRID L2 (restake AND additive own set),
which the flat byte could not express. Invariant RestakeParent || own-set on
Mode.Valid(); Sovereign derived, never flagged.

CreateNetworkTx + ConvertNetworkTx carry security.Mode on the wire via shared
setSecurity/readSecurity. Round-trip green incl. new HybridL2 case + Convert
target-mode. Wallet builders pass the explicit restaked-L2 Mode.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-10 12:59:29 -07:00
zeekay b6143b5d53 Block codec → native ZAP (struct-is-wire): builds + round-trips green
Blocks are now the zap buffer: kind + parentID@1 + height@33 + time@41 +
tx-length-list@49 + tx-blob@57 (+ proposal-tx@65). commonZapBlock embedded base
mirrors spendingTx. Parse = zap.Parse + kind dispatch (no codec, no version).
Deleted block/codec.go + block/v0 + lift_v0. Round-trip green for abort/commit/
proposal/standard incl real signed txs. go build + go test ./block/ = green.

FINDING: block.GenesisCodec was double-duty — also serialized NON-block STATE
values (feeState, owners, L1Validator, metadata, chains). That's a THIRD codec
surface (state DB serialization) still to migrate for the full kill.
2026-07-10 12:43:20 -07:00
zeekay 20d36b36aa Restore ConvertNetworkTx as the promote endomorphism (Create ≠ Convert)
Create and Convert are orthogonal arrows: CreateNetworkTx = ∅→Network (birth,
sovereign-or-inherited); ConvertNetworkTx = Network→Network (promote an
existing network: inherited→sovereign, re-anchor parent — L2→L1, L3→L1, with
owner auth). Only CreateSovereignL1 stays folded (it was birth+sovereign, not
a distinct op). Both reuse the NetworkValidator component. Round-trip green
incl L2→L1 promote.
2026-07-10 12:13:15 -07:00
zeekay 4610978bcc Decomplect network creation: one CreateNetworkTx folds Convert+CreateSovereign
CreateNetworkTx{parent, owner, security, validators, chains, manager} creates a
network at ANY level in one tx — parent is the level axis (Primary⇒L1, L1⇒L2,
recurse; level=depth, never stored). security is a coproduct: SecuritySovereign
(own validators+manager) | SecurityInherited (restaked from parent). manager
lives on the Network so an inherited L2 holds local admin. NetworkValidator +
NetworkChain are shared value components (reused by CreateChainTx). Deleted
ConvertNetworkToL1Tx + CreateSovereignL1Tx (folded / migration artifacts).

Round-trip green: sovereign L1 (own validators+chains+manager) AND inherited L2
(parent recorded, no own validators, local manager) both survive. One and one
way to make a network at any depth.
2026-07-10 11:39:12 -07:00
zeekay e869b7bcd1 Consumer flip: fee + wallet field->method accessors + codec-free signing
fee/complexity + static_calculator + wallet/chain/p signer/backend visitors
moved off removed struct fields onto method accessors (tx.Ins->tx.Inputs()
etc). sign() rewritten to the codec-free unsigned‖creds model (tx.Unsigned.
Bytes() + tx.Initialize()). txs/fee + wallet/chain/p/signer build clean.
2026-07-10 11:11:42 -07:00
zeekay bbba24bad6 txs pure-zap wire PROVEN: round-trip green (fix AddBytes element-count bug)
zap.ListBuilder.AddBytes counts BYTES not elements; fixed-stride lists were
storing byte-inflated counts -> stride clamp rejected them. Fixed every write
helper to store the real element count (len(entries)). Round-trip test now
GREEN across the hardest cases: multisig+stakeable outputs/inputs, the nested
ConvertNetworkToL1Validator (NodeID+BLS PoP+2 owners), SovereignL1Chain
manifest, and signed unsigned‖creds. go build ./vms/platformvm/txs = exit 0.

The P-chain tx wire is now struct-IS-wire, codec-free, and verified correct.
2026-07-10 10:57:47 -07:00
zeekay 34cb5c1684 txs package GREEN: pure zap struct-is-wire compiles (Convert + CreateSovereign done)
All 21 real P-chain tx types are now the zap buffer — no codec, no marshal/
unmarshal, no Manager, no version, decompounded, on luxfi/zap. ConvertNetworkToL1Tx
+ CreateSovereignL1Tx hand-written with a shared ConvertNetworkToL1Validator
nested encoder (fixed-stride records + shared NodeID/addr pools) and a
SovereignL1Chain encoder. go build ./vms/platformvm/txs/ = exit 0.

Next: round-trip correctness test, then the external consumer flip.
2026-07-10 10:53:14 -07:00
zeekay e288b67008 Decomplect P-chain tx set: rip Slash + P-chain CreateAsset/Operation, restore staker ifaces
- SlashValidatorTx ripped (unwired stub; Avalanche has no slashing; consensus
  only detects, never enforces). Type+executor+tests+Visitor surface removed.
- CreateAssetTx + OperationTx ripped from P-CHAIN (X-chain/AVM types; LP-0130:
  asset creation + fx ops are X-chain money-rail, not P staking-rail; no P
  producer). xvm/avm untouched.
- Staker interfaces (StakerTx/ValidatorTx/DelegatorTx/ScheduledStaker) restored
  on the 5 validator types via pure accessors + FxID-on-stake preserved.
- Remaining real complex types: ConvertNetworkToL1Tx + CreateSovereignL1Tx.
2026-07-10 10:36:35 -07:00
zeekay f0a00167e9 node v1.36.0 — Nova/Quasar export-frontier bridge; consensus v1.36.0
Wires the consensus EXPORT (Quasar, two-thirds-stake) frontier into the C-Chain VM so
the EVM finalized/safe tags and the warp cross-chain gate resolve to the Quasar tip.
Consensus v1.35.38 -> v1.36.0.
2026-07-10 09:44:14 -07:00
zeekay 33e005c869 fix register_l1 verifyBaseTx (return-form) 2026-07-10 09:18:19 -07:00
zeekay 949210730d Pure zap tx: 18 type files converted (parallel) + verifyBaseTx reconciled
Batches 1-3 (proposal + spending + validator-family) landed pure: struct is
the buffer, New*Tx builds, accessors read, SyntacticVerify via verifyBaseTx.
Remaining package-internal: 5 complex types (Convert/CreateSovereign/CreateAsset/
Operation/Slash) + staker interface methods + consumer flip.
2026-07-10 09:17:15 -07:00
zeekay 8d2fb750b6 Remove stale codec tests (codec deleted) 2026-07-10 09:08:31 -07:00
zeekay 1bc07cb1ca Pure zap tx: pure Tx (Parse=wrap+split, Sign=build) + delete reflection codec
tx.go: Tx holds Unsigned (zap-backed) + Creds; Parse wraps signed bytes and
splits unsigned/creds at the self-delimiting boundary; Sign builds unsigned‖creds;
no codec.Manager param anywhere. Deleted codec.go (V0/V1/V2 reflection Manager)
and codec_activation.go. WIP: consumer sites that passed txs.Codec / called the
old Parse(codec, bytes) flip next.
2026-07-10 09:08:15 -07:00
zeekay 1dede2e3d2 Pure zap tx: Parse kind-dispatch + native credential wire (no codec)
Parse wraps the buffer zero-copy and dispatches on the 1-byte kind. Signed =
unsigned ‖ creds (both self-delimiting), so unsigned is a byte-prefix of
signed. writeCredsBuf/parseCredsBuf encode credentials natively (shared 65B
sig-blob array). No Marshal/Unmarshal/Manager. WIP: references the 24 pure
type constructors (5 hand-written + 18 in parallel conversion + 1 template).
2026-07-10 09:07:13 -07:00
zeekay 636944a63a Pure zap tx: spendingTx embedded base (envelope surface for all types) 2026-07-10 09:02:58 -07:00
zeekayandHanzo Dev 9c2468671e chains: bridge the consensus EXPORT (Quasar) frontier into the VM; drop dead view-change braid (v1.36)
Wire the two-tier consensus export boundary through the node so the EVM `finalized`/`safe` tags and
the warp export gate track the ⅔-stake Quasar tip, never the reorgable Nova/accept tip:

- Set NetworkConfig.QuasarObserver to push each EXPORT (Quasar) frontier advance into the raw inner
  VM (SetLastQuasarFinalized) — the eth/warp backends live there, not on the proposervm wrapper.
  Interface-gated: only a VM exposing the export sink (the C-Chain EVM) participates.
- On boot, re-seed the consensus export frontier from the VM's DURABLE Quasar height
  (SyncQuasarFrontier) so GetQuasarTip/QuasarHeight do not regress on restart.

Also remove the node's dead references to the v1.36-deleted Tendermint braid (the consensus engine
dropped it in 174af3c31), which no longer compile against the v1.36 engine:
- the LUX_CONSENSUS_VIEW_CHANGE opt-in (params.ViewChange is gone — Nova is the sole decider),
- the quorumKindPrevote gossip kind + BroadcastPrevote + HandleIncomingPrevote routing (no prevotes;
  the ⅔ Quasar attestation rides the ordinary accept-vote gossip). Keep the braid dead.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-10 04:59:06 -07:00
zeekay 7750ca1829 gofmt spending.go 2026-07-10 01:43:56 -07:00
zeekayandHanzo Dev c526c0aaa4 Pure zap tx: reusable delta encoders (owner/auth/validator/signer/idlist)
Shared delta-field encoders every non-proposal tx composes on the spending
envelope: owner (fx.Owner), auth (*secp256k1fx.Input), inline Validator (44B)
+ Signer (145B: kind+BLS pubkey+PoP), id lists, extra spending lists. Built on
luxfi/zap. With spending.go, the full reusable foundation — type files are now
pure compositions.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-10 01:42:43 -07:00
zeekayandHanzo Dev 7354ce84b9 Pure zap tx: shared spending wire (envelope + Output/Input/owner/creds)
The envelope (NetworkID/BlockchainID/Outs/Ins/Memo) + multisig/stakeable
Output/Input entries + shared owner-address/sig-index arrays, built on
luxfi/zap generic primitives — no codec, no zap_native package. writeSpending/
setEnvelope build inside New*Tx; readEnvelope reads lazily. Polymorphism
(TransferOutput/LockOut, TransferInput/LockIn) handled in explode/assemble.
Foundation every non-proposal tx composes.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-10 01:40:27 -07:00
zeekayandHanzo Dev 8e7253a3c5 Pure zap tx: kind dispatch + AdvanceTimeTx template (struct IS the wire)
kind.go: 1-byte discriminator @ object offset 0 = the whole dispatch. No
codec, no version, no slot map. AdvanceTimeTx converted to the pure model:
holds *zap.Message, Time() is an offset read, NewAdvanceTimeTx builds once,
Bytes() returns the buffer. No marshal/unmarshal anywhere.

Template for the remaining 21 types. Package migrates atomically (codec.go +
all types + Parse/Sign together), so it compiles green again only when the
whole set + concentrated consumers (executor/builder/fee/api, ~63 New sites)
are converted. WIP.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-10 01:24:10 -07:00
zeekayandHanzo Dev 67ec31a344 Rip the marshal/unmarshal bridge — it was a codec by another name
ZAP has no serialization step. The bridge copied fields between plain txs.*
structs and the buffer, which is exactly the codec ZAP deletes. Removed.
The right way: tx type IS the zap buffer (accessors, Parse=wrap, Bytes=buffer),
no codec / manager / compat / version. Wire primitives stay in luxfi/zap.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-10 01:13:02 -07:00
zeekayandHanzo Dev 77389092c2 LP-023: bridge 5 more tx types (extra-list/bytes/idlist deltas)
Shared extra-out/in list helpers (Import/Export second spending set), id-list
+ BLS-PoP encoders. Bridges TransferChainOwnership, RegisterL1Validator,
Import, Export, CreateChain. Round-trip green (13/22 P-tx types native).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-10 01:03:37 -07:00
zeekayandHanzo Dev 13cca30a71 LP-023: bridge 5 delta tx types (owner/auth/scalar deltas)
Shared delta-encoders (owner=fx.Owner->OutputOwners, auth=verify.Verifiable->
secp256k1fx.Input, fixed-id helpers). Bridges IncreaseL1ValidatorBalance,
SetL1ValidatorWeight, DisableL1Validator, RemoveChainValidator, CreateNetwork
= spending envelope + fixed-offset delta fields. Round-trip green (8/22 P-tx
types now native: proposal x2 + BaseTx + these 5).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-10 01:00:42 -07:00
zeekayandHanzo Dev a93e5cdd9d chains: size-chunk GetContext responses so a behind validator resyncs under heavy load (1/3)
Benchmark-proven live mainnet bug (250-trader DEX load): a validator that falls behind cannot
resync the C-Chain. GetContext (the catch-up "context response", wire = Ancestors) bounded the
response ONLY by block COUNT (maxContextBlocks=256). Under heavy DEX load 256 blocks summed to
3.4-5.7 MB, exceeding the 2 MB peer message cap (the zstd compressor refuses uncompressed input
above constants.DefaultMaxMessageSize to prevent a decompression bomb), so msgCreator.Ancestors
FAILED to build and the behind validator received NOTHING — permanently stuck while the tip
advanced (luxd-3 stuck at 256 while tip went 293→300, looping empty-block builds).

Fix (piece 1/3 of the layered design — defense in depth): GetContext now ALSO bounds the
response by serialized SIZE. It stops adding blocks before the accumulated payload would exceed
byteBudget (cap − 128 KiB envelope margin), but ALWAYS includes at least one block so a behind
node makes progress every round; the requester re-requests the remaining gap (context fetch is
already a multi-round oldest-first fill). A single block that alone exceeds the budget is still
served (best-effort) so the walk never deadlocks — the forthcoming trust-tiered validator cap
(pieces 2/3) gives such a block the send headroom; a stranger's tight cap correctly rejects it.

Tests (chains/context_chunk_test.go): TestGetContext_ChunksBySize_FitsUnderCap (100×150 KiB
chain → 12 blocks / 1.84 MB, under budget, vs ~15 MB packed by count — fail-on-old);
TestGetContext_SingleOversizeBlock_StillServed (one oversize block still served, no deadlock).

Pieces 2/3 to follow: trust-tiered message cap (validator peers get headroom, strangers keep
2 MB) + confirm the inbound throttler/benchlist penalizes oversized-message senders.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-09 23:30:40 -07:00
zeekayandHanzo Dev ae3e3a45f5 LP-023: native spending envelope + full component converters
Shared spending envelope (NetworkID/BlockchainID/Outs/Ins/Memo) + polymorphic
converters bridging the txs struct graph to proto/zap_native primitives with
zero reflection: TransferOutput/stakeable.LockOut, TransferInput/stakeable.LockIn,
secp256k1fx.Credential. BaseTx (TxKindBaseFull) bridged; creds travel in the
separate creds buffer of the unsigned‖creds envelope.

Round-trip GREEN: 2-of-3 multisig + owner-locktime output, stakeable.LockOut,
stakeable.LockIn input, memo, AND signed secp256k1 credentials all survive
Marshal->Unmarshal with exact field equality; unsigned stays a byte-prefix of
signed. Every embedding tx type now composes this envelope + its delta fields.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-09 21:39:14 -07:00
zeekayandHanzo Dev 83c1a83765 v1.34.29: consensus v1.35.37 -> v1.35.38 (behind-node self-heal via cert-triggered ancestor catch-up)
Fixes the 'restart-recovery exhausted' mainnet killer: a slipped validator
logged 'cert REFUSED (behind; fetch and retry)' but NEVER fetched, because
HandleIncomingCert only triggered a catchup when the cert's OWN block was
untracked — never for a missing INTERMEDIATE ancestor. v1.35.38 (3c09ecb94)
surfaces the specific missing ancestor as a typed error and the cert handler
fires exactly one requestCatchup for it. The node-layer fetch machinery
(networkCatchup.RequestAncestors -> requestContext -> GetAncestors ->
AcceptCatchupBlock) was already fully wired — it was just never called on this
path. With --skip-bootstrap=true (which disables the beacon-quorum frontier
backstop) this cert-trigger was the ONLY self-heal path, so its absence was
fatal: a behind node couldn't rejoin -> effective 4/5 -> any flap -> stall.

Consensus-side only, no node code change. -race: launch-gate invariant PASS
(rejoin 2.75s, no fork); self-heal + typed-error tests PASS, no data races.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-09 21:28:06 -07:00
zeekayandHanzo Dev be18fce176 LP-023 keystone: native-ZAP tx Manager (drop-in, zero reflection)
Adds nativeManager implementing pcodecs.Manager by bridging txs structs to
proto/zap_native buffers — no reflection, no serialize-tag walk, no version
dispatch. Signed wire = unsigned_zap_buffer ‖ creds_zap_buffer (ZAP buffers
are self-delimiting), preserving tx.go's unsigned-is-prefix-of-signed
invariant with no tx.go change. AdvanceTimeTx + RewardValidatorTx bridged
and round-trip green (build->Marshal->Unmarshal field equality, byte-stable,
prefix invariant, non-ZAP reject). Added alongside the reflection Codec;
flip + reflection deletion lands once all registered types are bridged.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-09 16:53:16 -07:00
zeekayandHanzo Dev 74be89840c v1.34.28: consensus v1.35.36 -> v1.35.37 (F1 stale-alias prevote-lock rebase)
Third-layer mainnet finality fix. v1.34.27 (consensus v1.35.36) proved the
dynamic committee (alpha=4 n=5) and topology.go cert-receive canonical resolve
work live — consensus climbed past 1085755 to round-height 1085761 — but hit the
next layer: a stale-alias prevote-lock. viewForLocked seeds lockBlock from a
pre-canonical-fix durable committedSlot (an OUTER proposervm-wrapper id); a
round-0 lock on an outer alias makes prevoteTarget compare the stale outer id
against the inner-canonical winner by raw id, mismatch forever, no POL, freeze.

v1.35.37 (742696baf): stepViewChange rebases a stale-alias lock onto the inner
canonical winner IFF it resolves (via the same vmCanonicalResolver the cert
rebase uses) to exactly winnerCanon — a genuinely different inner (real fork) or
unresolvable lock is left fail-closed frozen, never merged. lockRound preserved,
2a-n>f untouched, alpha inner-precommits were always inner (CanonicalVoteMessage
binds inner, excludes outer). Full engine/chain green (356s, 0 fail); -race
clean; RED safety gate proves divergent inners still refused.

Roll note: luxd-4 first releases its lock (log 'vc stale-alias lock REBASED');
finality past 1085755 needs >=4 nodes on this build, so height climbs as 3->2->
1->0 come up. EVM stays v1.104.7 (head-pin).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-09 12:05:51 -07:00
zeekayandHanzo Dev 9a0800065f v1.34.27: consensus v1.35.33 -> v1.35.36 (EVM-accept unstick + dynamic committee + snowman purge)
Unsticks mainnet C-Chain finality. Root cause (consensus-layer, proven): under
pChainHeight=0 anyone-can-propose, each validator wraps the same inner block in
its own outer proposervm envelope; votes are canonical-keyed so an α-of-K cert
forms network-wide, but HandleIncomingCert did a strict envelope-id lookup — a
node holding a different alias of the same inner block missed it and requested
catchup instead of finalizing the local wrapper it held, so VM.Accept was never
called and the EVM head froze at 1085755 while consensus logged 'finalized
voters=4'. v1.35.34 (66438a23b) resolves the local wrapper by canonical id and
rebases the verified cert (outer ids unsigned; α votes verify unchanged), no
fork (per-height gate keys on canonical id, idempotent).

Also: dynamic committee 1→N proven (v1.35.35, effectiveCommittee sizes cert +
view-change from live validator count; n=1→1/1,2→2/2,3→3/3,4→3/4,5→4/5 — the
BFT-safe ⌊2n/3⌋+1); view-change safety gate realigned to the effective
committee (RED#2); snowman comment-purge (Quasar/Nova); dynamic committee logs.
Node commits: 4e71814e58 (proposervm→EVM Accept-cascade test, RED#1),
c13681108f (purge), a02611413e (logs). EVM stays v1.104.7 (head-pin). Full
engine/chain suite green under -race (369s, 0 fail/0 race); RED adversarial
suite proves scale-invariance 1→1M + RLP seam byte-exact on real mainnet state.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-09 11:11:58 -07:00
zeekayandHanzo Dev 4e71814e58 vms/proposervm: prove proposervm-finalize → inner-EVM Accept propagation (RED #1, choke #4)
The mainnet 1085755 freeze had consensus finalizing an outer proposervm block while the
inner EVM lastAccepted stayed frozen. The consensus engine's ledger advancing on VM.Accept
is proven in engine/chain; this proves the OTHER half at the NODE layer — that VM.Accept on
a proposervm wrapper cascades to the inner block's Accept:

  postForkBlock.Accept → acceptOuterBlk → acceptInnerBlk → Tree.Accept(innerBlk) →
  innerBlk.Accept  ⇒ inner VM lastAccepted advances

- TestAcceptCascade_InnerHeadAdvances_AtScale: 1000 heights through the REAL Accept path;
  the inner EVM head + proposervm head advance in lock-step at every height.
- TestAcceptCascade_SiblingStorm_WinnerAdvancesInnerOnce: five distinct outer envelopes wrap
  ONE inner block (the anyone-can-propose alias set); accepting the finalized wrapper advances
  the shared inner exactly once.

Confirms the propagation was sound, not a node-layer bug — the freeze was the consensus-layer
storm-alias resolution gap (fixed in consensus v1.35.34). The real luxfi/evm RLP-import→produce
→tip byte-exactness is proven separately in evm/core rlp_seam_red_test.go; composed, they cover
the full engine→proposervm→EVM Accept path.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-09 10:57:31 -07:00
zeekayandHanzo Dev c13681108f node: purge 'snowman' comment references — Quasar/Nova terminology (comment-only, no behavior change)
Removes the forbidden Avalanche term from code comments in chains/manager.go,
chains/quorum.go, and vms/proposervm/proposer/windower_determinism_test.go. Reworded to
preserve exact meaning; comments only, no identifier/behavior change.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-09 10:50:27 -07:00
zeekayandHanzo Dev a02611413e chains: view-change ENABLED log prints the PRESET, not the committee (#5 dynamic logs)
The "round-scoped view-change ENABLED for chain K=21 alpha=15" line read as if K=21/α=15
were the finality committee — misleading. It is the Snowman SAMPLE preset. The α-of-K
cert and the view-change POL/precommit are sized to the LIVE validator set at runtime
(effectiveCommittee/bftCommittee; 5 validators → K=5/α=4), and the engine already logs
the effective (K,α) on each committee re-clamp. Relabel the fields presetK/presetAlpha
and add a note pointing at the runtime committee-clamp log. Log-only; no behavior change.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-09 10:30:30 -07:00
zeekayandHanzo Dev 7720d00baa v1.34.26: consensus v1.35.29 -> v1.35.33 (finality committee sizing) + quorum ValidatorCount
Restores mainnet C-Chain finality. The prior stall: with 5 live validators but
MainnetParams K=21/alpha=15, BOTH finality gates (assembleCertLocked cert-alpha
AND view-change POL) required 15 distinct votes — impossible from 5 validators,
so the chain froze (safe, no fork). consensus v1.35.33 sizes both gates from the
live validator count via effectiveCommittee (alpha=4 for n=5), inheriting the
minBFTCommittee K=4/alpha=3 floor (1085013 self-finality guard preserved);
Snowman K=21 sample untouched. node chains/quorum.go adds
validatorStakeSource.ValidatorCount (height-indexed, deterministic). EVM stays
v1.104.7 (head-state pin). Test: 5-validator MainnetParams control freezes,
fix converges in 0.35s.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-09 08:59:42 -07:00
zeekayandHanzo Dev 439f2768e0 docs/postmortem: mark residual #1 (proposer-preference restart loop) FIXED
BuildBlock last-accepted fallback landed in 2dc15620e4 (ships v1.34.26).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-08 23:27:42 -07:00
zeekayandHanzo Dev 2dc15620e4 vms/proposervm: BuildBlock falls back to last-accepted on unheld preferred
Closes postmortem residual #1 (proposer-preference restart loop / mute-voter
wedge). Build-side companion to the defect #1 SetPreference validate-before-
assign hardening.

vm.preferred is only adopted after a successful getBlock, but a block fetchable
at preference time can later become unfetchable: an unaccepted sibling consensus
dropped, or a never-persisted outer block referenced after heavy sibling churn.
The old BuildBlock returned after the single getBlock(preferred) miss with
"failed to fetch preferred block", so every attempt failed in a tight loop and
the node's voter went mute (~170 err/s). Quasar cert-finality has no re-converge
poll, so the fleet never recovered on its own — the liveness wedge behind the
mainnet 1082879->1085755 window (heartbeat-pause + full-fleet-restart churn).

Fix: on an unfetchable preferred, build the child on last-accepted (always held:
committed state). The node keeps producing on a valid tip while the catch-up
path pulls the gap; a later SetPreference(held tip) re-advances the preference.
Only surface the original error when last-accepted is itself the unfetchable id.

Tests (hermetic, mirror vm_rejoin_wedge_test.go): spy inner VM asserts the fetch
sequence — BuildBlock consults last-accepted on an unheld preferred (old code
never did), and does not spuriously fetch when no distinct fallback exists.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-08 23:20:41 -07:00
zeekayandHanzo Dev 2bbe067f60 docs: postmortem — C-Chain accepted-head state GC eviction (mainnet freeze)
Failure mode, preconditions (pruning + state-history 32 + commit-interval 4096
+ idle), affected (evm ≤v1.104.6 / node v1.34.14-23) and fixed versions (evm
v1.104.7 / node v1.34.25), the head-state-pin invariant, the recovery recipe
(restart-as-recovery, healthy-peer PVC restore at replicas=0, repair-cchain
rewind), and the two residual restart-liveness follow-ups.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-08 06:10:11 -07:00
zeekayandHanzo Dev ae15850e1a v1.34.25: proper semver — EVM pin v1.104.7 (head-state-pin fix), drop stale replace-cascade comment
Same content as v1.34.24 (v1.104.3-headpin == v1.104.7, retagged per semver
policy: real monotonic patch versions, no suffix tags). go.mod has no replace
directives; removes the leftover comment from an already-completed cascade.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-07 21:39:48 -07:00
zeekayandHanzo Dev 092ed0459e v1.34.24: EVM v1.104.3-hotfix -> v1.104.3-headpin (durable idle-freeze fix)
Pins luxfi/evm v1.104.3-headpin: dedicated unbalanced GC pin on the accepted
head state root (transferred head-to-head in Accept + startup + direct
setters) so the head can never be evicted by the tipBuffer/Cap while idle —
the durable fix for the 1085200 'missing trie node at head' freeze. Also
carries vm v1.2.6 parity. Base: v1.34.22 (mainnet matcher lineage), no other
changes.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-07 20:33:36 -07:00
zeekayandHanzo Dev aa884628a0 v1.34.22: consensus v1.35.28 -> v1.35.29 (complete fix: self-finality safety + canonical cert aggregation + clone-resign inner->outer)
The COMPLETE mainnet reliability build. v1.35.29 adds FIX #1 (self-finality floor — a
transient bftCommittee count can never self-finalize without quorum; the 1085016 accept-
without-quorum safety hole) + FIX #3 (canonical vote aggregation — sibling wrappers of the
same inner block combine into ONE cert; the block-288 wrapper-split cert-termination stall)
+ the v4 clone-resign inner->outer fix (my v1.35.28 re-sign silently no-op'd on real wrapped
blocks). Matcher EVM v1.104.3-hotfix + Bug B UNCHANGED — execution-identical to mainnet.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-07 12:33:23 -07:00
zeekayandHanzo Dev 3d65b1c0c2 v1.34.22: MAINNET FINAL — matcher+Reference EVM (v1.104.3-hotfix) + consensus v1.35.28
Bundles BOTH reliability fixes on the mainnet-compatible line (the live test proved they
COMPOUND — Bug A catch-up alone can't reconcile the tip-loss Bug B causes on restart):
- Bug A (consensus v1.35.28, merged to consensus main): catch-up #1/#2/#3/#5 + vote-guard clone re-sign.
- Bug B (EVM v1.104.3-hotfix): head-state Reference one-liner — EXECUTION-IDENTICAL to mainnet
  (golden roots identical RED vs GREEN), matcher EVM (precompile v0.19.0), NO settle-only.
go.mod delta vs mainnet v1.34.14 = consensus-only; EVM = matcher+Reference (the only exec delta is
the proven-identical GC pin). STAGED for mainnet recovery — NOT rolled (gated on RED + canary).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-07 11:00:24 -07:00
zeekayandHanzo Dev a8971630b5 v1.34.21: bump consensus v1.35.24 -> v1.35.28 (#2 steer-guard + vote-guard clone fix)
MAINNET-COMPATIBLE node-only patch: the v1.34.14 line + catch-up/preference fixes + vote-
guard, EVM PLUGIN UNCHANGED (EVM_VERSION=v1.104.3, precompile v0.19.0, chains v1.7.2, vm
v1.2.5 — byte-identical to mainnet; NO settle-only, NO state-transition change). go.mod delta
vs v1.34.14 = consensus ONLY. Consensus v1.35.28 touches vote/cert LIVENESS only (engine is
byte-identical across the lines); it carries #2 (GetBlock-guarded build-tip steer) + the
vote-guard clone-artifact re-sign fallback. Cherry-picked #1/#3/#5 (proposervm validate-
before-assign, EmptyNodeID->real peers, have-block!=not-behind cert-fetch) on the prior commit.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-05 20:02:08 -07:00
zeekayandHanzo Dev 09ab817ee5 node: cure the lagging-validator rejoin wedge (proposervm #1 + peer-select #3)
The recurring freeze: a validator that fell behind could never rejoin, spamming
"sentTo=0" and "failed to fetch preferred block" until bounced. Two node-layer
defects, both BFT-safe (compared against ava avalanchego proposervm):

#1 proposervm.SetPreference (vms/proposervm/vm.go): assigned vm.preferred BEFORE
   fetching, so a build-tip the node doesn't hold POISONED vm.preferred forever
   (BuildBlock errors on every attempt; Quasar cert-finality has no re-converge
   poll). Now: getBlock (both post-fork + inner stores) FIRST, delegate to inner,
   adopt the preference only on success; on a total miss keep the last held tip
   and stay live. Identical to ava in every case its single-store invariant
   produces; only adds recovery for Lux's build-tip steering (never bricks).
   SetPreference is not an acceptance gate, so safety/agreement is untouched.

#3 peer selection (chains/manager.go): consensus signals "fetch a certified-but-
   untracked block" via ids.EmptyNodeID; the old code Add(EmptyNodeID)+Send, so
   GetAncestors went to ZERO peers and the cert-verified gap was never fetched.
   Now: when nodeID is Empty, sample real connected peers that track this chain's
   network (same selection pollFrontierOnce uses); the cert already gated the ask.

Adds vm_rejoin_wedge_test.go. (Defect #2 — the engine build-tip steering in
consensus/engine/chain — is the upstream root cause, tracked separately.)

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-05 19:57:53 -07:00
zeekayandHanzo Dev 08465c2f63 deps: consensus v1.35.26 -> v1.35.27 (comment-trim tip, logic-identical)
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-04 00:16:24 -07:00
zeekayandHanzo Dev ba14e55f2f chains: self-disarming stale-lock apply (apply:<floor>) — consensus v1.35.26
The migration env now requires the operator's observed floor target:
LUX_CONSENSUS_MIGRATE_STALE_LOCKS=apply:<decidedFloor> (1082879 for the
mainnet one-shot). A bare "apply" is refused loudly. Once the chain
recovers and the floor advances, a lingering env self-disarms (Skipped,
no write) instead of degrading into an every-boot prune of genuine
crash locks. Consensus v1.35.26 carries the paired red-review fixes:
self-disarm floor target, durable-finality cert gate, migrated-view
invalidation, height-bound resolver.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-03 23:53:53 -07:00
zeekayandHanzo Dev d84b5397ed chains: wire stale-lock migration + view-change gossip fail-fast; bump consensus v1.35.25
Consensus v1.35.25 adds the boot-time stale outer-wrapper lock migration that
unfreezes mainnet C-Chain 1082880 (the durable 3/2 split-lock the canonical=inner
roll left behind — locks stored by outer wrapper id, never re-canonicalized). This
wires it into chain creation:

- LUX_CONSENSUS_MIGRATE_STALE_LOCKS one-shot (K>1 view-change chains), run AFTER
  NewRuntime seeds the durable locks and BEFORE Start drives any view:
    "inspect" -> InspectLocks: prints the per-height trace table, mutates nothing
                 (the per-pod audit gate operators run before any write);
    "apply"   -> MigrateStaleLocks: idempotent, safety-gated canonicalize/prune
                 ABOVE the decided floor (finalized + vote-guard state <=floor and
                 the floor itself preserved verbatim); STOP -> fail-closed error.
  logStaleLockReport renders the auditable trace table in both modes.
- DEFENSIVE view-change hygiene: refuse to start a view-change chain whose gossiper
  is not a QuorumGossiper or that has no VoteSigner (prevotes/precommits would be
  emitted into the void -> silent finality stall) — fail loud, not frozen.

Offline cross-pod audit uses consensus cmd/voteguard (CGO-free vote-guard decoder).
Keeps the v1.34.14 canonical fix (Part A proposervm + Part B consensus) intact.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-03 22:51:08 -07:00
zeekayandHanzo Dev 98911842db docker: guard all mandatory chain-VM plugins, not just bridgevm
The chain-VM plugins are built with '|| echo WARN' (optional) but hard-COPY'd in
the runtime stage (a build miss -> cryptic COPY failure). Replace the single
bridgevm test-s check with a guard over every mandatory plugin so a missing/empty
binary fails loudly at the build stage with the real cause.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-03 21:26:08 -07:00
zeekayandHanzo Dev 0f185bd813 proposervm: implement canonicalCommitter — inner id is the finality canonical (Part A of 1082879 unfreeze)
The consensus engine's finality object is VotePosition.CanonicalID (the signed
accept vote binds it and EXCLUDES the outer envelope id). It reads it via a
structural canonicalCommitter assertion (engine/chain canonicalIDOf); until now
NO block type implemented it, so canonicalIDOf fell back to the OUTER proposervm
envelope id. On the stuck C-Chain the designated proposer re-wraps the SAME inner
block each slot -> 758 distinct outer ids over ONE inner block 25Q837Lw -> 758
distinct canonicals -> votes scatter -> no alpha-of-K cert -> frozen at 1082879.

postForkCommonComponents now returns the inner block's id as CanonicalID and the
inner parent's id as ParentCanonicalID, so every outer wrapper of one inner block
(and a forked parent) collapses to ONE consensus object. Enables the consensus
v1.35.23/24 canonical-representative determinism fix to actually engage; neither
part alone unfreezes mainnet.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-03 21:26:08 -07:00
zeekayandHanzo Dev a2f5c6c37b build: node canary v1.34.13 — consensus v1.35.24 (fix BLS double-register panic)
v1.34.12 crash-looped on boot:
  panic: threshold: scheme BLS already registered

consensus <= v1.35.22 blank-imported the OLD crypto/threshold/bls in
quasar.go while node imports the NEW threshold/scheme/bls; both register
BLS into the crypto/threshold registry -> panic. consensus v1.35.24
repoints quasar.go to threshold/scheme/bls.

Verified on the FULL node binary dep closure (go list -deps ./main, 1028
pkgs): luxfi/crypto/threshold/bls = 0, luxfi/threshold/scheme/bls = 1 —
exactly one BLS registration path, the panic cannot recur.

Pins: consensus v1.35.24, chains v1.7.2, evm v1.104.3.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-03 21:14:17 -07:00
zeekayandHanzo Dev b3e92ce979 build: node canary v1.34.12 — chains v1.7.2 (pulsar-path fix) + consensus v1.35.22
Supersedes the v1.34.11 image build, which failed in the Docker plugin
stage: chains v1.7.1 pulled consensus v1.25.35, whose polaris.go imports
the removed pulsar/ref/go/pkg/pulsar path — the quantumvm + mpcvm plugins
could not build and their hard COPY failed.

- chains v1.7.1 -> v1.7.2 (consensus bumped to v1.35.22 there; consistent
  pulsar graph; all 10 chain VM plugins verified building standalone).
- Dockerfile CHAINS_REF v1.7.1 -> v1.7.2.
- consensus v1.35.21 -> v1.35.22 (Pike-clean of the ProposalKey liveness
  fix; logic identical to v1.35.21).

Pins: consensus v1.35.22, chains v1.7.2, evm v1.104.3. Off origin/main,
no WIP.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-03 20:50:05 -07:00
zeekayandHanzo Dev 0428445890 build: node canary v1.34.11 — buildable image with decomplected consensus
IMAGE/BUILD change (packaging only — the consensus behavior change ships
separately as luxfi/consensus v1.35.21). Two edits make the deployable
node image build with the fixed consensus and the current chain plugins:

- consensus pin v1.35.20 -> v1.35.21 (ProposalKey decomplection, commit
  776a5119c: outer block IDs are transport aliases, proposal identity is
  ProposalKey{ParentID,Height} — proposervm envelope churn can no longer
  spawn own sibling candidates that split votes; the mainnet 1082880 fix).
- Dockerfile ARG CHAINS_REF v1.7.0 -> v1.7.1. v1.7.0 still shipped the
  pre-rename `thresholdvm` dir, so the Dockerfile's `cd /tmp/chains/mpcvm`
  plugin build produced nothing (swallowed by `|| echo WARN`) and the hard
  COPY of the mpcvm plugin (tGVBwRxp...) failed — the v1.34.10 Docker build
  break. v1.7.1 carries the mpcvm rename; the mpcvm plugin builds clean
  (verified standalone, 16MB) and all 10 chain plugins are present.

Pins verified for the canary image: consensus v1.35.21 (=776a5119c),
chains v1.7.1, evm v1.104.3, cevm v0.19.0, dex v1.5.15. Built off
origin/main — no WIP from any other worktree.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-03 19:38:25 -07:00
zeekayandHanzo Dev 86a342ae7b deps: bump to released graph — ids v1.3.1, constants v1.6.1, genesis v1.16.1, chains v1.7.1, consensus v1.35.20, threshold v1.12.1
Pins node to the published tags carrying this session's LP-0130 work:
ids/constants add M/F chain identifiers (drop T-Chain), genesis adds
M/FChainGenesis, chains has the mpcvm rename, consensus has the
mempool-churn pre-build gate + threshold scheme/bls repoint.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-03 19:08:20 -07:00
zeekay 52fd2cef35 Merge branch 'feat/thresholdvm-decomplect' into integrate/node-release-plus-mpcvm 2026-07-03 16:20:21 -07:00
zeekayandHanzo Dev 89bb0e718c build: pin EVM_VERSION v1.104.1 -> v1.104.3 (ZAP VM-state serialization fix)
v1.34.9 = v1.34.8 + the C-Chain EVM plugin rebuilt from evm v1.104.3
(= evm v1.104.1 + luxfi/vm v1.2.6). The only change is the plugin's
ZAP RPC server now serializes VM state calls, fixing the concurrent-map
crash (chain.State.verifiedBlocks) that killed the EVM subprocess under a
concurrent ParseBlock storm and froze Lux mainnet C-Chain at block 1082879.
Node binary, consensus, genesis, and all other plugins are unchanged;
block output is byte-identical, so v1.34.9 is consensus-compatible with the
running v1.34.8 fleet (verified on devnet/testnet before the mainnet roll).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-03 15:15:10 -07:00
zeekayandHanzo Dev 6c6b256418 deps: repoint BLS threshold scheme registration to luxfi/threshold/scheme/bls
The BLS Scheme impl moved from crypto/threshold/bls to
luxfi/threshold/scheme/bls (all threshold impls now live in one module;
the interface stays in crypto/threshold). One-line blank-import path
change; no behavior change.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-03 14:14:39 -07:00
zeekayandHanzo Dev c7e0eef6f6 vms/types/fee: quantumvm moved to NoUserTxPolicy (LP-0130 §6)
Doc-comment + LLM.md updated to reflect chains/quantumvm feegate flip.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-03 12:32:41 -07:00
zeekayandHanzo Dev b4720c9f57 node v1.34.8: consensus v1.35.16 -> v1.35.17 (VC liveness) + proposervm slot-snap
Pairs the proposervm stale-parent slot-snap churn-fix (f840336167) with consensus
v1.35.17 (view-change abandonment must never silence a height). Together they close the
distributed VC liveness stall that keeps mainnet C-Chain safe-but-paused: slot-snap kills
the stale-parent rebuild sibling explosion at the source (one stable candidate per slot),
and v1.35.17 keeps the round machinery driving the height to convergence instead of going
silent after rePoll's 8-attempt cap. Liveness-only; decided-floor / no-double-finalize
safety invariant untouched. Gated on the live idle-inclusive devnet re-storm + RED before
any testnet/mainnet roll.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-03 11:57:47 -07:00
zeekayandHanzo Dev f840336167 fix(proposervm): slot-snap child timestamp — kill stale-parent rebuild churn (VC liveness)
Off a stale parent (idle past the proposer window → anyone-can-propose), buildChild
was stamping a raw wall-clock timestamp (Time().Truncate(1s)), so every rebuild of the
same inner block produced a DISTINCT envelope id. That is an unbounded, ever-growing
sibling set at one height: the round-scoped view-change can never gather α aligned
prevotes on a single candidate (no proof-of-lock forms) and finality stalls fleet-wide
(the distributed liveness stall).

Snap the child timestamp DOWN to the parent-anchored proposer-window grid
(parentTimestamp + slot*WindowDuration, slot = TimeToSlot(parent, now)) so all rebuilds
WITHIN one slot are byte-identical → one stable candidate the view-change converges on.

Safe by construction: TimeToSlot(parent, snapped) == slot == TimeToSlot(parent, now), so
the block's slot — hence ExpectedProposer's proposer-window / signed-unsigned verdict and
the derived epoch — is unchanged. Only slot>0 snapped, so a fresh in-window child keeps
its exact timestamp (zero live-chain change), and a snapped time is strictly > parent
(monotonic) and <= now (not advanced). Liveness-only; no safety surface touched.

Extracted as slotSnappedChildTimestamp + unit-proven (idempotence, slot-invariance,
monotonicity). Full proposervm suite green.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-03 10:52:51 -07:00
zeekayandHanzo Dev d4323da799 node: rename thresholdvm → mpcvm; split M/F registries (LP-0130)
- vms/thresholdvm/ → vms/mpcvm/ (dir + inner files + package decl).
- node/vms.go optional-VM registry: drop ThresholdVMID row, add
  MPCVMID and FHEVMID rows (matching the new split).
- genesis/builder/registry.go: T-Chain entry → M-Chain (MPCVMID) +
  F-Chain (FHEVMID); registry_test.go parity assertions updated.
- genesis/builder/builder.go: TChainAliases → MChainAliases +
  FChainAliases; ThresholdVMID switch case split to MPCVMID / FHEVMID;
  optIn chain slice uses config.MChainGenesis + config.FChainGenesis
  (T-Chain slot retired).
- LLM.md: point at LP-0130 as canonical topology + fee spec; correct
  the quantumvm posture to service-only per LP-0130 §6 (Q-Chain has
  no user-payable blockspace).

Build + vet clean on chains/mpcvm/... + node/{vms/mpcvm,genesis,node}/...

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-03 01:23:03 -07:00
zeekayandHanzo Dev 97d9745ea9 fix(platformvm): guard validator-set cache + decouple TrackedChains snapshot (multi-validator data races)
Two concurrent-map races on the consensus-adjacent validator-set path,
both surfaced by the 5-node storm re-gate, both RED-verified fixed:

1. m.caches concurrent map write (getValidatorSetCache check-then-insert,
   reached via proposervm windower.ExpectedProposer across chains during
   buildBlocksLocked): guarded with a dedicated cachesLock sync.Mutex
   (atomic check-create-store; plain Mutex, no lost-update).
2. TrackedChains concurrent map read/write: getValidatorSetCache read the
   network's runtime-mutable cfg.TrackedChains lock-free (the admin
   setTrackedChains RPC .Add's it under peersLock). Decoupled via an
   immutable startup snapshot (set.Of(cfg.TrackedChains.List()...) in
   NewManager); the false 'immutable after startup' comment is corrected.
   Snapshot is correct (caching is pure-perf; cache.Empty recomputes),
   and the construction clone provably happens-before any writer (P-chain
   init is synchronous, admin API serves only post-node.Dispatch()).

Gates multi-validator restart-storm crash-freedom (before validator #2).
-race regression tests reproduce both races pre-fix, clean post-fix
(-count=5). Tracked follow-up (same class, network's live alias, admin-API
default-off, NOT this path): config/internal.go:120-121 + health.go:30 —
fix before ever enabling the admin API.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-02 23:03:25 -07:00
zeekay 6e7f0b3d7e Merge tag 'v1.34.5' into blue/bootstrap-finality-split
node v1.34.5: EVM plugin v1.104.1 (0x9999 genesis-injection fix), forward-integrated on v1.34.4
2026-07-02 17:50:31 -07:00
zeekayandHanzo Dev 6a87c69010 node: signed-checkpoint bootstrap anchor (INVARIANT 4) + consensus v1.35.16
Harden the bootstrap checkpoint — the one path that bypasses the beacon quorum —
into a cryptographically SIGNED weak-subjectivity anchor.

- Checkpoint carries an authority Signature over a domain-separated canonical
  (id,height); CheckpointVerifier (injected, backed by a proven primitive —
  Ed25519/BLS, never custom crypto) authenticates it. The policy stays crypto-free
  exactly like AncestrySource/heightOf.
- AcceptsFrontier trusts a pinned checkpoint ONLY when the configured authority
  signed that exact (id,height). Present-but-unsigned, signed-by-a-non-authority,
  signature-transplanted-to-another-(id,height), and no-verifier-wired all FAIL
  CLOSED. A compromised flag cannot inject a false sync anchor without also forging
  the authority's signature. It is a cryptographic vouch, NEVER a ⅔-live-stake tally.
- No production path pins a checkpoint yet (opt-in operator escape hatch), so the
  invariant is enforced for when it is activated; nothing to wire now.

Folds consensus v1.35.16 (single ⅔ formula + leaf-lock doctrine) into the node.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-02 17:49:54 -07:00
zeekayandHanzo Dev 3db595a637 node v1.34.5: bump C-Chain EVM plugin pin v1.104.0 -> v1.104.1
Picks up the DEX 0x9999 genesis-injection fix (evm v1.104.1): 0x9999's
EXTCODESIZE marker is no longer baked into pre-2025 genesis state, so the
archived Lux mainnet (0x3f4fa2a0), Lux testnet (0x1c5fe377) and Zoo mainnet
(0x7c548af4) RLPs re-import byte-identical. 0x9999 activates at its protocol
timestamp (2025-12-25) during replay. rpcChainVM protocol 42 unchanged.

Forward-integrated on v1.34.4 (bootstrap/finality split, consensus v1.35.15) —
v1.34.4 was already taken and still pinned evm v1.104.0. This is a minimal
patch on top: EVM pin only.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-02 17:49:22 -07:00
zeekayandHanzo Dev 15ff32fbdc node: bootstrap/finality type split (FinalityQuorum vs BootstrapTrust) + consensus v1.35.15
Make the bootstrap-trust vs live-finality boundary a HARD API split so neither
can impersonate the other — the mass-recovery deadlock was bootstrap reusing the
finality quorum (>2/3 of CURRENT stake), which is unsatisfiable when the
recovery targets are themselves down validators.

- FinalityQuorum.HasFinality(weight,total): the live rule, strict >2/3 of current
  stake (unchanged). Renamed from ConsensusQuorum to the reviewer's exact spec.
- BootstrapTrust.AcceptsFrontier(replies): selects a weak-subjective sync anchor
  from AUTHENTICATED CONFIGURED beacons — a response FLOOR (MinResponses) plus a
  supermajority over the RESPONDERS, NEVER over the whole set. Distinct type, so
  bootstrap cannot call the finality predicate (INVARIANT 3: acceptance != finality;
  the node re-executes every synced block before re-entering consensus).
- Contrast proven: 3-of-5 bootstrap ACCEPTS a frontier, yet HasFinality(3/5) is
  false and STAYS false after sync — bootstrap is not a finality bypass.

Folds consensus v1.35.15 (the global unlock-before-call-out invariant + the
n=1..10 quorum acceptance matrix) into the node binary.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-02 16:49:47 -07:00
zeekayandHanzo Dev ec5cf94145 node: v1.34.3 — decomplected single-validator DECIDE fix + evm accept-empty pin
consensus v1.35.13 -> v1.35.14 (decomplect: removed the selfVoter shortcut that
self-deadlocked on t.mu.RLock — the live n=1 freeze; K==1 now finalizes solely via the
inline finalizer, one path). EVM_VERSION v1.103.0 -> v1.104.0 (= v1.103.0 + accept-empty:
a consensus-finalized empty block Accepts, so VM.Accept can never fail-closed on an empty
first block — the stale-evm-pin gap). Unblocks single-validator Zoo mainnet/testnet +
Hanzo (all frozen at block 0).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-02 15:29:50 -07:00
zeekayandHanzo Dev 5e44cd67dc node: bump consensus v1.35.11 -> v1.35.13 — LIVE single-validator DECIDE deadlock fix (v1.34.2)
The v1.34.1 n=1 fix passed unit tests but the LIVE single-validator chains still
froze (Zoo mainnet/testnet, Hanzo, Pars, all block-0). Root cause (neo's live repro,
confirmed through the full runtime path): a reentrant-RWMutex self-deadlock — the
single-node selfVoter called engine.ReceiveVote synchronously while buildBlocksLocked
held t.mu, so ReceiveVote's t.mu.RLock deadlocked on the same goroutine (node hangs at
'single-node mode: self-voting', inline finalize never runs, block never decides).

consensus v1.35.13 dispatches the selfVoter's ReceiveVote on a fresh goroutine (no
reentrant lock) — the block now DECIDES through the real NewRuntime path (proven by
TestBlue_SingleValidator_DecidesThroughFullRuntimePath: hangs pre-fix, passes post-fix).
The bump also carries the n=1 synthesize fix (v1.35.11) and the dormant 1→N
decentralization guard (v1.35.12). EVM_VERSION unchanged (v1.103.0) — single-validator
blocks are tx-driven / non-empty.

Gates Zoo mainnet + Zoo testnet + Hanzo (all single-validator).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-02 15:01:00 -07:00
zeekayandHanzo Dev 1b6487e0b7 node: bump consensus v1.35.8 -> v1.35.11 (n=1 single-validator DECIDE stall fix) — v1.34.1
Prod-line (v1.34.x finality-admission) bump carrying the consensus fix for the
single-validator (K=1) DECIDE stall that wedged all three sovereign
single-validator chains (Zoo 200200, Hanzo 36963, Pars 494949): a K==1 block now
finalizes even when its self-vote is unverifiable against a not-yet-resolvable
single-validator set (synthesized 1-of-1 cert; per-height FinalizeBranch gate is
the safety). n>1 C-Chains (mainnet/testnet) unaffected — they never hit the K()==1
branch.

The bump also carries the (dormant) bounded phantom-floor reconcile
(v1.35.9/v1.35.10) — additive engine methods that this prod-line node never calls
(manager.go on main has no ReconcilePhantomFloor wiring), so behavior is unchanged.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-02 13:55:06 -07:00
Hanzo Dev 67272bc7c9 chore: OSS attribution — avalanchego (BSD-3-Clause) + go-ethereum (LGPL-3.0) NOTICE + retained notices 2026-07-02 12:51:01 -07:00
zeekay 2e98019144 Merge branch 'deploy/dex-v114-integer-wire' 2026-07-02 11:03:04 -07:00
zeekayandHanzo Dev 6424671eb7 deploy: bump DEX stack to fork-safe integer wire (chains v1.7.0, evm v1.103.0, precompile v0.19.0)
Deploys the launch-blocker fixes: F1-F5 cross-arch execRoot fork (exact-integer
ZAP wire, no float64 in consensus) + F9 infinite-money unbacked-deposit (authority
gate, fail-closed). Rebuilds the bundled DexVM + EVM plugins on the reconciled
dep chain (dex v1.14.0 lx→dex fold). Cross-arch execRoot proven identical.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-02 11:03:04 -07:00
zeekay 29c623880c go.mod: bump consensus v1.35.7 -> v1.35.8 (fail-closed finalize->VM-accept, fix 2b diagnostic) 2026-07-02 09:23:31 -07:00
zeekayandHanzo Dev ab4201301e chains: opt-in round-scoped view-change enablement (LUX_CONSENSUS_VIEW_CHANGE) + consensus v1.35.7
Adds the per-deployment opt-in flag: when LUX_CONSENSUS_VIEW_CHANGE=true and K>1, sets
consensusParams.ViewChange=true so devnet/testnet activate the round-scoped view-change
without a mainnet default (mainnet owner-gated). The engine fail-secure HALTS if the committee
fails 2a-n>f, so enabling never weakens safety. Bumps consensus v1.35.6->v1.35.7 (removes dead
safeConfig dup). This is what makes v1.32.13's prevote transport actually activatable.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-02 08:20:09 -07:00
zeekay 29a263eb14 merge: view-change prevote gossip + consensus v1.35.6 (v1.32.13) 2026-07-02 08:07:52 -07:00
zeekay c3f037c737 go.mod: bump consensus v1.35.5 -> v1.35.6 (round-scoped view-change) 2026-07-02 08:07:52 -07:00
zeekayandHanzo Dev d1400a4c65 chains: wire round-scoped view-change PREVOTE gossip (node side of v1.32.13)
Adds the p2p transport for the consensus round-scoped view-change:
- quorumKindPrevote (kind 3) in the quorum-gossip envelope + decodeQuorumGossip accepts it.
- blockHandler.Gossip demux routes kind 3 -> engine.HandleIncomingPrevote(payload) (the engine
  decodes+verifies height/round/canonical/sig from the payload, domain LUX/chain/prevote/v1,
  and tallies toward a POL).
- networkGossiper.BroadcastPrevote(chainID,networkID,height,round,canonical,voteBytes) frames
  kind 3 + broadcasts to ALL validators, mirroring BroadcastVote.

Type-checks against the round-scoped consensus (local replace, chains pkg builds clean).
Requires: consensus tag (with ViewChange + HandleIncomingPrevote + BroadcastPrevote) -> node
go.mod bump -> ARC build -> node v1.32.13 (also carries v1.32.12 evm v1.101.3 descent fix +
isBootstrapped truth fix). ViewChange is opt-in per network (default off); enable on devnet/
testnet only, NO mainnet.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-02 08:00:31 -07:00
zeekayandHanzo Dev bd996c41b1 node v1.32.12: bake evm v1.101.3 (stateless ParseBlock — fixes C-Chain bootstrap descent stall)
Bootstrap descent no longer stalls on ancestry blocks ahead of the accepted height:
evm v1.101.3 decomplects BlockGasCost from ParseBlock (parse decodes; Verify enforces).
Carries the v1.32.11 isBootstrapped-truth + ancestry-fetch hardening.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-02 03:23:28 -07:00
zeekayandHanzo Dev feef2bcb1e diag: bake evm v1.101.3-diag (PARSEBLOCK_DIAG logging) for descent root-cause
Temporary diagnostic node build. To be reverted after the evm ParseBlock fix.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-02 03:07:08 -07:00
zeekay 99c690f307 Revert "BSDIAG: instrument C-Chain bootstrap ancestry send+serve (temporary, for root-cause)"
This reverts commit 5921a24dd9.
2026-07-02 03:00:24 -07:00
zeekay 00fa804fc0 Revert "BSDIAG2: parse served batch (wantInBatch + parsedIDs/heights) + served block ids"
This reverts commit 65efff0628.
2026-07-02 03:00:24 -07:00
zeekayandHanzo Dev 65efff0628 BSDIAG2: parse served batch (wantInBatch + parsedIDs/heights) + served block ids
Definitive probe: does the descent's served ancestry batch contain the requested
tip (want), and what are the parsed ids vs the serve-side ids — pins whether the
tip survives the Bytes()->ParseBlock round-trip (proposervm wrapping).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-02 02:32:36 -07:00
zeekayandHanzo Dev 5921a24dd9 BSDIAG: instrument C-Chain bootstrap ancestry send+serve (temporary, for root-cause)
INFO-level logging on both sides of the descent so we can isolate send-vs-serve:
- SEND: sampleAncestorBeacons (weights/connected/ahead/sample), Ancestors SENT
  (sampleSize/sentTo), GOT batch / TIMEOUT / msgBuild-fail.
- SERVE: GetContext RECV, walk-miss (which block + i), SERVED 0 (firstFound),
  SERVING (containers).
- ROUTING: deliverBootstrapAncestors (dataLen/decodedBlocks/chFound/delivered).

To be reverted once the root cause is pinned. Devnet diagnostic build only.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-02 02:12:18 -07:00
zeekayandHanzo Dev 7397da43ae docs(bootstrap): note the two latent invariants red flagged (X-Chain DAG-sync gate; monotonic bootstrapped state)
Documentation-only; no behavior change (v1.32.11 image unaffected). Red review of
the v1.32.11 diff: 0 critical/high/medium, recommendation SHIP.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-02 00:15:02 -07:00
zeekayandHanzo Dev a63a18bf71 node v1.32.11: C-Chain restart/recovery serving hardening (isBootstrapped truth + wipe-path ancestry)
Two node/bootstrap-layer fixes for a clean rolling validator upgrade. Consensus
engine untouched (v1.35.5 boot-seed from v1.32.10 carried forward).

[HIGH — correctness] info.isBootstrapped(C) tracks REAL state, not premature true.
  manager.IsBootstrapped(id) returned true the instant a chain merely EXISTED in
  m.chains (set right after createChain launched the async, possibly-stalling
  bootstrap goroutine) — so a C-Chain stalled at genesis (head 0x0) reported
  info.isBootstrapped(C)=true, masking the stall from any wait-for-healthy gate.
  Now keys on the SAME sb.Bootstrapped signal the readiness health check uses
  (m.Nets.IsChainBootstrapped), set only after runInitialSync reaches the named
  frontier and the VM goes to normal operation (head advanced, eth-RPC live).
  GetChains().Bootstrapped fixed identically. New per-chain query on nets.Net +
  chains.Nets (nil-safe). Regression test TestIsBootstrappedTracksRealConvergence.

[MEDIUM — robustness] WIPE-path GetAncestors fetch hardened for ≥2 peers at genesis.
  Applying the proven avalanchego contract (studied in snowman/bootstrap +
  getter): (a) Ancestors buffers the whole sample and SKIPS empty batches,
  returning the first NON-EMPTY one — a size-1 channel let a fast empty reply from
  a genesis peer win the race and starve a peer that actually held the ancestry;
  (b) sampleAncestorBeacons PREFERS beacons the frontier round found genuinely
  ahead (they hold the ancestry) — ava's PeerTracker "ask a prover" bias sourced
  from the replies we already have, no separate tracker. Tests:
  TestNodeBootstrap_WipePath_MixedGenesisPeers_ObtainsAncestry,
  TestSampleAncestorBeacons_PrefersAheadBeacons.

Build: main+chains+nets+service/info compile CGO_ENABLED=0 (ARC/Dockerfile path);
full chains+nets suites green.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-02 00:05:36 -07:00
zeekayandHanzo Dev 363a5a8591 bump(consensus): v1.35.2 -> v1.35.5 — boot-seed decidedFloor from vm.LastAccepted (clean in-place v1->v2 upgrade)
consensus v1.35.5 is a patch-only fix (go.mod byte-identical to v1.35.2..v1.35.4): the
decided-height sign-gate floor is now seeded DIRECTLY from vm.LastAccepted at engine Start
(before signing) and in SyncState, so a node upgrading IN PLACE from a legacy v1 vote-guard
file (floor 0) has a real decided floor from the first instant of boot — closing the
mainnet v1->v2 upgrade window where the floor was 0 until the first post-upgrade finalize.
Sign-gate-only (never enters byHeight/ledger.Height; PART-A intact; only refuses more).

Supersedes the un-rolled node v1.32.9 (consensus v1.35.4) for the mainnet-first roll. No
node code change (fix is internal to the consensus engine). go mod tidy drops an orphaned
luxfi/bft indirect (pre-existing). Full node builds clean on linux/amd64 (cgo off).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-01 23:58:52 -07:00
zeekayandHanzo Dev 51a304804c chore: migrate luxd HTTP routes /ext -> /v1
Drop the Avalanche-heritage /ext prefix; /v1 is the single canonical route
surface (one way, no backward compat). The node's baseURL is the source of
truth; clients, SDKs, CLI, indexer, maker, genesis, netrunner, and the
k8s/compose/gateway/explorer configs are updated to match.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-01 11:40:13 -07:00
441 changed files with 15335 additions and 21108 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ permissions:
jobs:
buf-lint:
runs-on: ubuntu-latest
runs-on: lux-build-amd64
steps:
- uses: actions/checkout@v4
- name: Check for .proto files
+1 -1
View File
@@ -9,7 +9,7 @@ on:
jobs:
push:
runs-on: ubuntu-latest
runs-on: lux-build-amd64
steps:
- uses: actions/checkout@v4
- uses: bufbuild/buf-setup-action@v1.31.0
+7 -7
View File
@@ -50,7 +50,7 @@ jobs:
TIMEOUT: ${{ env.TIMEOUT }}
CGO_ENABLED: '0'
Fuzz:
runs-on: ubuntu-latest
runs-on: lux-build-amd64
env:
GOPRIVATE: github.com/luxfi/*
GONOSUMDB: github.com/luxfi/*
@@ -71,7 +71,7 @@ jobs:
# e2e_pre_etna, e2e_post_etna, e2e_existing_network, Upgrade
# These will be re-enabled once tmpnet is properly configured
Lint:
runs-on: ubuntu-latest
runs-on: lux-build-amd64
env:
GOPRIVATE: github.com/luxfi/*
GONOSUMDB: github.com/luxfi/*
@@ -96,7 +96,7 @@ jobs:
run: scripts/actionlint.sh
buf-lint:
name: Protobuf Lint
runs-on: ubuntu-latest
runs-on: lux-build-amd64
steps:
- uses: actions/checkout@v4
- name: Install buf
@@ -111,7 +111,7 @@ jobs:
run: buf lint proto
check_generated_protobuf:
name: Up-to-date protobuf
runs-on: ubuntu-latest
runs-on: lux-build-amd64
continue-on-error: true
env:
GOPRIVATE: github.com/luxfi/*
@@ -143,7 +143,7 @@ jobs:
run: .github/workflows/check-clean-branch.sh
check_mockgen:
name: Up-to-date mocks
runs-on: ubuntu-latest
runs-on: lux-build-amd64
continue-on-error: true
env:
GOPRIVATE: github.com/luxfi/*
@@ -163,7 +163,7 @@ jobs:
run: .github/workflows/check-clean-branch.sh
go_mod_tidy:
name: Up-to-date go.mod and go.sum
runs-on: ubuntu-latest
runs-on: lux-build-amd64
env:
GOPRIVATE: github.com/luxfi/*
GONOSUMDB: github.com/luxfi/*
@@ -180,7 +180,7 @@ jobs:
run: .github/workflows/check-clean-branch.sh
test_build_image:
name: Image build
runs-on: ubuntu-latest
runs-on: lux-build-amd64
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
+1 -1
View File
@@ -24,7 +24,7 @@ on:
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
runs-on: lux-build-amd64
permissions:
actions: read
contents: read
-127
View File
@@ -1,127 +0,0 @@
name: Docker (GPU variant)
# Per-arch NATIVE build of the GPU-accelerated node image (DEXVM_GPU=1).
#
# The standard image (docker.yml) is pure-Go (CGO_ENABLED=0) and cross-compiles
# arm64 on an amd64 runner — correct, because nothing native is linked. The GPU
# variant is different: its D-Chain dexvm plugin links the per-arch native
# liblux_gpu (lux_gpu_dex_match_order), and a native GPU lib CANNOT be
# cross-linked. So each arch is built on the arcd pool that owns the matching
# silicon and the matching liblux_gpu artifact from lux-private/gpu-kernels:
#
# arm64 → spark (GB10, CUDA) → lux-gpu-linux-arm64.tar.gz
# amd64 → evo (ROCm, native-linux personality) → lux-gpu-linux-amd64.tar.gz
#
# Each job emits ghcr.io/luxfi/node:<tag>-gpu-<arch>; the manifest job fuses
# them into ghcr.io/luxfi/node:<tag>-gpu. The plain (CPU) manifest is untouched.
#
# This is opt-in and separate from docker.yml on purpose: an operator that wants
# GPU matching pulls :<tag>-gpu; everyone else pulls the portable CPU image.
on:
workflow_dispatch:
inputs:
tag:
description: 'version tag to build as the GPU variant, e.g. v1.33.1'
required: true
lux_gpu_version:
description: 'lux-private/gpu-kernels release providing the per-arch liblux_gpu'
required: false
default: 'v0.1.0'
permissions:
contents: read
packages: write
id-token: write
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- arch: arm64
runner: spark-lux-linux # GB10 — CUDA host, native arm64
platform: linux/arm64
- arch: amd64
runner: evo-lux-linux # Strix Halo — ROCm host, native amd64
platform: linux/amd64
name: node:gpu-${{ matrix.arch }} (native)
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.tag }}
# NATIVE only — no QEMU, no cross. The GPU lib is per-arch; assert the
# runner arch matches the target before building.
- name: Assert native arch
run: |
set -euo pipefail
host="$(uname -m)"
case "${{ matrix.arch }}" in
arm64) [ "$host" = "aarch64" ] || [ "$host" = "arm64" ] || { echo "::error::arm64 target on $host"; exit 1; } ;;
amd64) [ "$host" = "x86_64" ] || { echo "::error::amd64 target on $host"; exit 1; } ;;
esac
echo "OK native ${{ matrix.arch }} on $host"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver: docker-container
driver-opts: network=host
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Resolve cross-repo PAT for private luxfi/* deps
id: pat
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
UNIVERSE_PAT: ${{ secrets.UNIVERSE_PAT }}
run: |
tok="$GH_TOKEN"; [ -z "$tok" ] && tok="$UNIVERSE_PAT"
[ -n "$tok" ] || { echo "::error::no GH_TOKEN/UNIVERSE_PAT for private luxfi deps"; exit 1; }
echo "::add-mask::$tok"
{ echo "token<<EOF"; echo "$tok"; echo "EOF"; } >> "$GITHUB_OUTPUT"
# Single-arch, native build. DEXVM_GPU=1 + CGO_ENABLED=1 make the dexvm
# plugin link the per-arch liblux_gpu fetched inside the Dockerfile from
# the lux-gpu release. BUILDPLATFORM == TARGETPLATFORM (native) so the
# Dockerfile's cross-compile branch is never taken.
- name: Build & push GPU variant (native ${{ matrix.arch }})
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile
platforms: ${{ matrix.platform }}
push: true
build-args: |
CGO_ENABLED=1
DEXVM_GPU=1
LUX_GPU_VERSION=${{ github.event.inputs.lux_gpu_version }}
secrets: |
ghtok=${{ steps.pat.outputs.token }}
tags: ghcr.io/luxfi/node:${{ github.event.inputs.tag }}-gpu-${{ matrix.arch }}
manifest:
needs: build
runs-on: [self-hosted, linux, amd64]
steps:
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Fuse per-arch GPU images into one manifest
run: |
set -euo pipefail
T="ghcr.io/luxfi/node:${{ github.event.inputs.tag }}-gpu"
docker manifest create "$T" "$T-amd64" "$T-arm64"
docker manifest push "$T"
echo "published $T (amd64 + arm64)"
+1 -1
View File
@@ -9,7 +9,7 @@ permissions:
jobs:
fuzz:
runs-on: ubuntu-latest
runs-on: lux-build-amd64
env:
GOPRIVATE: github.com/luxfi/*
GONOSUMDB: github.com/luxfi/*
+1 -1
View File
@@ -11,7 +11,7 @@ permissions:
jobs:
MerkleDB:
runs-on: ubuntu-latest
runs-on: lux-build-amd64
env:
GOPRIVATE: github.com/luxfi/*
GONOSUMDB: github.com/luxfi/*
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
permissions:
contents: read
issues: write
runs-on: ubuntu-latest
runs-on: lux-build-amd64
steps:
- uses: actions/checkout@v4
- uses: crazy-max/ghaction-github-labeler@548a7c3603594ec17c819e1239f281a3b801ab4d #v6.0.0
+1 -1
View File
@@ -4,7 +4,7 @@ on:
- cron: '0 0 * * 0' # Run every day at midnight UTC on Sunday
jobs:
stale:
runs-on: ubuntu-latest
runs-on: lux-build-amd64
steps:
- uses: actions/stale@v10
with:
+2 -2
View File
@@ -15,7 +15,7 @@ env:
jobs:
test-zapdb-replay:
runs-on: ubuntu-latest
runs-on: lux-build-amd64
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -87,7 +87,7 @@ jobs:
--api-admin-enabled=true 2>&1 | grep -E "(genesis-db|Genesis)" || true
test-database-factory:
runs-on: ubuntu-latest
runs-on: lux-build-amd64
steps:
- name: Checkout code
uses: actions/checkout@v4
+29 -62
View File
@@ -257,7 +257,7 @@ RUN . ./build_env.sh && \
# failed ValidateState, and BRICKED the node. Proven on-node: real swap → kill -9 →
# clean reboot, state intact. v1.99.40 = v1.99.39 + deps to latest. consensus v1.25.21 =
# stake-weighted alpha-of-K quorum finality + per-height single-finalize + epoch-bound certs.
ARG EVM_VERSION=v1.101.2
ARG EVM_VERSION=v1.104.8
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
@@ -281,7 +281,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
# byte-identity; DEX big.Rat + determinism). Pin chains v1.4.8 (warp
# consolidated to one luxfi/warp helper; graphvm genesis-last-accepted fix)
# to match the chain-VM plugin stage (CHAINS_REF) below.
go mod edit -require=github.com/luxfi/chains@v1.4.8 && \
go mod edit -require=github.com/luxfi/chains@v1.7.0 && \
find /tmp/evm -name go.sum -exec sed -i -E '/^github.com\/(luxfi|hanzoai)\//d' {} + && \
GOARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) \
CGO_ENABLED=0 GOFLAGS=-mod=mod \
@@ -302,16 +302,16 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
# oraclevm -> r5m1ujrmXxVcQetG3CQfuDLHp2RHKh6vCDaFgBRQfUcTZh7eS
# quantumvm -> ry9Sg8rZdT26iEKvJDmC2wkESs4SDKgZEhk5BgLSwg1EpcNug
# relayvm -> sP6dLqrrBR9w3soP18fbJ3YzZecZdD7DDdfH2cFhhLq7Hy9bz
# thresholdvm -> tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t
# mpcvm -> tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t
# zkvm -> vv3qPfyTVXZ5ArRZA9Jh4hbYDTBe43f7sgQg4CHfNg1rnnvX9
# MUST track node's go.mod luxfi/chains (the D-Chain dexvm + 10 VM plugins).
# Bump with every chains release or the bundled VM plugins go stale vs node's deps.
# v1.4.7 == node go.mod's luxfi/chains pin: warp consolidated to ONE luxfi/warp
# helper (bridgevm/zkvm/thresholdvm), graphvm genesis-last-accepted fix, built on
# helper (bridgevm/zkvm/mpcvm), graphvm genesis-last-accepted fix, built on
# evm v1.99.48 + precompile v0.16.0 (enable-everything builder surface). Keeps the
# baked VM plugins in lockstep with the host node.
ARG CHAINS_REF=v1.4.8
ARG CHAINS_REF=v1.7.5
RUN --mount=type=cache,target=/root/.cache/go-build \
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' {} +
@@ -345,13 +345,25 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
-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/mpcvm && CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
-o /luxd/build/plugins/tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t ./cmd/plugin ) || echo "WARN: mpcvm plugin build skipped" ; \
( 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; } && \
for p in \
juFxSrbCM4wszxddKepj1GWwmrn9YgN1g4n3VUWPpRo9JjERA \
kMhHABHM8j4bH94MCc4rsTNdo5E9En37MMyiujk4WdNxgXFsY \
nZQm4Dmg1rjX18rb8maL9gamYyXPf1xCvF7ymWzxp6a1nSQTt \
oR6tnZHezwogyf9fRnomNXC9ojwCEBAU6jdUzpgy2PB1tD7fM \
pJJCSV7hHYVY6TUZwR8qUPAfuhX8JLb2C1AzNSezrYNbgau8M \
r5m1ujrmXxVcQetG3CQfuDLHp2RHKh6vCDaFgBRQfUcTZh7eS \
ry9Sg8rZdT26iEKvJDmC2wkESs4SDKgZEhk5BgLSwg1EpcNug \
sP6dLqrrBR9w3soP18fbJ3YzZecZdD7DDdfH2cFhhLq7Hy9bz \
tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t \
vv3qPfyTVXZ5ArRZA9Jh4hbYDTBe43f7sgQg4CHfNg1rnnvX9 ; do \
test -s /luxd/build/plugins/$p \
|| { echo "FATAL: required chain-VM plugin $p missing/empty — its build failed above (see the matching WARN line); the runtime-stage hard COPY would otherwise fail cryptically. Surface & fix the real Go build error, or remove the plugin from BOTH the build list and the runtime COPY."; exit 1; } ; \
done && \
rm -rf /tmp/chains
# ============= Native D-Chain DEX VM Plugin Stage ================
@@ -363,18 +375,11 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
# 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. The STANDARD node builds this plugin pure-Go (CGO=0),
# so its matcher is lx.MatchOrderCPU — the pure-Go oracle that works on every
# arch with no native deps. GPU acceleration is OPT-IN via DEXVM_GPU=1 (below):
# pkg/lx's single orderbook_gpu.go links the unified lux-gpu (liblux_gpu) and
# runtime-selects CUDA/HIP/Metal, falling back to MatchOrderCPU when no device is
# present — so the two paths are byte-equal by contract (orderbook_gpu_test.go).
# Because lux-gpu is a per-arch native lib, the DEXVM_GPU variant CANNOT be
# cross-compiled: it must be built on the matching arcd GPU pool (see the
# per-arch GPU-variant build in .github/workflows/docker-gpu.yml). v1.5.10 is
# the first tag whose cmd/dchain builds CGO=0
# 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)
# node HTTP router (VM.CreateHandlers -> /v1/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);
@@ -386,54 +391,20 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
# 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
# /v1/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
# GPU-accelerated D-Chain matcher (opt-in). DEXVM_GPU=1 fetches the per-arch
# unified lux-gpu (built natively on the arcd GPU pools by
# lux-private/gpu-kernels' liblux-gpu.yml — CUDA/arm64 on spark, HIP/amd64 on
# evo, Metal on the mac) and builds the dexvm plugin with CGO_ENABLED=1 so
# pkg/lx/orderbook_gpu.go links liblux_gpu (lux_gpu_dex_match_order +
# lux_gpu_backend_name). Default 0 = the portable pure-Go CPU matcher, unchanged.
# The fetch is per-arch and best-effort in the same spirit as lux-accel above,
# but for DEXVM_GPU=1 a MISSING lib is FATAL: an operator asking for the GPU
# variant must get a GPU-linked plugin, not a silent CPU one. Do NOT set
# DEXVM_GPU=1 in a cross-arch (BUILDPLATFORM != TARGETPLATFORM) build — a native
# GPU lib cannot be cross-linked; build the GPU variant on the matching pool.
ARG DEXVM_GPU=0
ARG LUX_GPU_VERSION=v0.1.0
RUN --mount=type=secret,id=ghtok,required=false \
if [ "${DEXVM_GPU}" = "1" ]; then \
ARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) && \
AUTH=""; [ -s /run/secrets/ghtok ] && AUTH="--header=Authorization: Bearer $(cat /run/secrets/ghtok)"; \
wget -q ${AUTH:+"$AUTH"} \
"https://github.com/lux-private/gpu-kernels/releases/download/${LUX_GPU_VERSION}/lux-gpu-linux-${ARCH}.tar.gz" \
-O /tmp/lux-gpu.tar.gz \
&& tar -xzf /tmp/lux-gpu.tar.gz -C /usr/local \
&& rm /tmp/lux-gpu.tar.gz \
&& ldconfig 2>/dev/null || true; \
test -f /usr/local/lib/pkgconfig/lux-gpu.pc \
|| { echo "FATAL: DEXVM_GPU=1 but lux-gpu ${LUX_GPU_VERSION} (${ARCH}) unavailable — cannot build the GPU dexvm variant"; exit 1; }; \
else \
echo "DEXVM_GPU=0: dexvm builds pure-Go CPU matcher (no lux-gpu link)"; \
fi
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 && \
# DEXVM_GPU=1 → CGO on, orderbook_gpu.go links lux-gpu (pkg-config finds the
# per-arch lib fetched above). Default → CGO off, portable pure-Go matcher.
if [ "${DEXVM_GPU}" = "1" ]; then DEX_CGO=1; else DEX_CGO=0; fi && \
export PKG_CONFIG_PATH="/usr/local/lib/pkgconfig:${PKG_CONFIG_PATH:-}" && \
GOARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) \
CGO_ENABLED=${DEX_CGO} GOFLAGS=-mod=mod \
CGO_ENABLED=0 GOFLAGS=-mod=mod \
go build -ldflags="-s -w" \
-o /luxd/build/plugins/mDVT5EWMumBp3LCqvKwuyZQeY1VXr1jvjGNAt8nL4UFiXvqXr ./cmd/dchain && \
chmod +x /luxd/build/plugins/mDVT5EWMumBp3LCqvKwuyZQeY1VXr1jvjGNAt8nL4UFiXvqXr && \
@@ -464,12 +435,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
curl ca-certificates git \
&& rm -rf /var/lib/apt/lists/*
# Native GPU libraries (optional). /usr/local/lib exists (empty) on the builder
# for the standard CGO_ENABLED=0 / DEXVM_GPU=0 image, so this COPY is a no-op
# there. For the DEXVM_GPU=1 variant it carries the per-arch liblux_gpu.so that
# the D-Chain dexvm plugin dynamically links; ldconfig then makes it resolvable.
# Pure-Go fallbacks are used whenever the library is absent.
COPY --from=builder /usr/local/lib/ /usr/local/lib/
# GPU crypto library (optional -- only present when built with CGO_ENABLED=1 + luxcpp).
# Pure Go fallbacks are used when the library is absent.
RUN ldconfig 2>/dev/null || true
# Maintain compatibility with previous images.
+1 -1
View File
@@ -52,7 +52,7 @@ Target: validate all consensus, EVM, and staking behavior with K=11 validators.
- [ ] Verify all precompile activation timestamps are after 2025-12-25
- [ ] Deploy via PaaS (platform.hanzo.ai), not manual kubectl
- [ ] Verify all 11 pods reach Running state
- [ ] Verify all 11 nodes report healthy via `/ext/health/liveness`
- [ ] Verify all 11 nodes report healthy via `/v1/health/liveness`
### 1.2 Bootstrap and Connectivity
+21
View File
@@ -10,3 +10,24 @@ For the canonical Lux IP and licensing strategy, see:
For commercial inquiries that go beyond BSD-3 (e.g. private moat
acceleration kernels), contact `licensing@lux.network`.
## Upstream attribution
See [NOTICE](NOTICE) for the full attribution. In summary:
- **avalanchego** (Ava Labs, Inc.) — this repository is derived from
[ava-labs/avalanchego](https://github.com/ava-labs/avalanchego), licensed
under the **BSD 3-Clause License** (© 2019 Ava Labs, Inc.). BSD-3 is
permissive; the Lux additions here are likewise BSD-3-Clause.
- **go-ethereum** (The go-ethereum Authors) — EVM support derives from
[go-ethereum](https://github.com/ethereum/go-ethereum). It is **not**
vendored in-tree; it is consumed as the external Go module
`github.com/luxfi/geth`, which retains go-ethereum's original licenses:
the library is **LGPL-3.0-or-later** and the command-line tools are
**GPL-3.0**.
**Copyleft flag:** the LGPL-3.0/GPL-3.0 terms of the go-ethereum-derived code
(via `github.com/luxfi/geth`) are **not** superseded by this repository's
BSD-3-Clause license. Distributing compiled node binaries must honor LGPL-3.0
for the linked geth library (published source of that code and its
modifications at <https://github.com/luxfi/geth>, and user ability to relink).
+38 -15
View File
@@ -65,10 +65,10 @@ selection, and EVM contract auth.
### Where to look for X
- Profile resolve at boot: `node/node.go:initSecurityProfile`
- Profile RPC + REST + metrics: `service/security/`
- JSON-RPC namespace: `security` at `POST /ext/security`
- JSON-RPC namespace: `security` at `POST /v1/security`
(methods `securityProfile`, `blockSecurity`)
- REST sidecars: `GET /ext/security/profile`, `GET /ext/security/block/{n}`
- Prometheus gauges: `/ext/metrics` under the `security_*` family
- REST sidecars: `GET /v1/security/profile`, `GET /v1/security/block/{n}`
- Prometheus gauges: `/v1/metrics` under the `security_*` family
- Peer scheme gate: `network/peer/scheme_gate.go`
- Classical-compat registry: `vms/txs/auth/policy.go`
- Mempool gate (P-Chain): `vms/platformvm/mempool/*.go`
@@ -85,6 +85,29 @@ selection, and EVM contract auth.
## FeePolicy — canonical user-tx fee gate
> **Topology + UTXO ownership + cross-chain fee model** are normatively
> specified by [**LP-0130** (Chain Topology, UTXO Ownership, and Fee
> Model)](https://github.com/luxfi/lps/blob/main/LPs/lp-0130-chain-topology-utxo-ownership-and-fee-model.md).
> Read that LP before touching any VM's fee/settlement path or any
> cross-chain import/export flow. In particular:
>
> - Only **P** and **X** are canonical UTXO state machines (LP-0130 §2).
> - **X** is the money rail; **P** is the staking/reward rail; **LUX**
> is the fee currency everywhere (LP-0130 §3, §5).
> - **Q-Chain has no user-payable blockspace** — finality is a
> validator obligation paid via P (LP-0130 §6). `quantumvm` MUST
> use `NoUserTxPolicy{}` — enforced in chains/quantumvm/feegate.go as of 2026-07-03.
> - **M-Chain fees are service fees** deducted from the originating
> chain's fee pool, not a user M-balance (LP-0130 §7). `mpcvm`
> already runs `NoUserTxPolicy{}` — correct.
> - **B-Chain fees** are deducted from the bridged amount (LP-0130 §8).
> - Every non-P/X chain settles worker rewards to X (asset payouts) or
> P (staker rewards) via epoch fee roots reconciled at Q finality
> (LP-0130 §4, §11).
> - **Σ-escrow invariant** (LP-0130 I-8): `Σ non-P/X fee balances ==
> Σ X-side fee escrow` at every Q checkpoint. Drift is a
> finality-blocking fault.
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.
@@ -97,14 +120,14 @@ no per-VM bespoke fee structs.
| 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, ...}` |
| bridgevm | B-Chain | user-tx | `FlatPolicy{Fee: MinTxFeeFloor, ...}` (deducted from bridged amount, LP-0130 §8) |
| quantumvm | Q-Chain | **service-only** (LP-0130 §6) | `NoUserTxPolicy{}` — validator obligation, no user-payable blockspace |
| identityvm | I-Chain | user-tx | `FlatPolicy{Fee: MinTxFeeFloor, ...}` |
| thresholdvm | M-Chain | service-only | `NoUserTxPolicy{}` |
| mpcvm | M-Chain | service-only (LP-0130 §7) | `NoUserTxPolicy{}` — fees pulled from originating chain's fee pool |
| 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) |
| evm | C-Chain | user-tx | native EVM gas (gas * gasPrice >= 0 enforced upstream); balance is X-imported LUX (LP-0130 §10) |
| platformvm | P-Chain | user-tx | native `TxFee` field on Config |
| avm | X-Chain | user-tx | native `TxFee` field on Config |
@@ -230,7 +253,7 @@ Located in `/vms/`:
- **platformvm**: Staking, validation, network management
- **xvm**: Asset transfers, UTXO model
- **dexvm**: DEX with order book, perpetuals, AMM
- **thresholdvm**: Threshold MPC and FHE for confidential computing
- **mpcvm**: Threshold MPC and FHE for confidential computing
- **quantumvm**: PQ consensus coordination (ML-DSA, Corona)
- **identityvm**: Decentralized identity (DID, verifiable credentials)
- **keyvm**: Post-quantum key management (ML-KEM, ML-DSA)
@@ -320,11 +343,11 @@ github.com/luxfi/genesis (JSON config) → github.com/luxfi/node/genesis/build
### CGO Dependencies
These require CGO for full functionality (graceful fallback when disabled):
- `consensus/quasar` - GPU NTT acceleration
- `vms/thresholdvm/fhe` - GPU FHE operations
- `vms/mpcvm/fhe` - GPU FHE operations
- `x/blockdb` - zstd compression
### FHE (Fully Homomorphic Encryption)
Located in `vms/thresholdvm/fhe/`:
Located in `vms/mpcvm/fhe/`:
- Uses `github.com/luxfi/lattice/multiparty` for DKG
- Lattice-based cryptography only (no fallbacks)
- Threshold decryption via Warp messaging
@@ -639,7 +662,7 @@ strings.Contains(errStr, "not found") // parent block not in local state
### Known CGO Stubs
When CGO disabled, these use CPU fallbacks:
- `consensus/quasar/gpu_ntt_nocgo.go`
- `vms/thresholdvm/fhe/gpu_fhe_nocgo.go`
- `vms/mpcvm/fhe/gpu_fhe_nocgo.go`
- `vms/zkvm/accel/accel_mlx.go`
### 8. ZAP CreateHandlers for VM HTTP Endpoints
@@ -659,7 +682,7 @@ When CGO disabled, these use CPU fallbacks:
```bash
curl -s -X POST -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}' \
http://localhost:9640/ext/bc/C/rpc
http://localhost:9640/v1/bc/C/rpc
# Returns: {"jsonrpc":"2.0","id":1,"result":"0x17870"}
```
@@ -668,7 +691,7 @@ curl -s -X POST -H "Content-Type: application/json" \
**Behavior**:
- **GET /**: Returns JSON node information (nodeId, networkId, version, chains, endpoints)
- **POST /**: Proxies JSON-RPC requests directly to C-chain `/ext/bc/C/rpc`
- **POST /**: Proxies JSON-RPC requests directly to C-chain `/v1/bc/C/rpc`
- **OPTIONS /**: Returns CORS preflight headers
**Files Modified**: `server/http/router.go`, `server/http/server.go`
@@ -735,7 +758,7 @@ if s.validators.NumNets() != 0 {
**Verification**:
```bash
curl -s http://localhost:9650/ext/health | jq '.checks.bls'
curl -s http://localhost:9650/v1/health | jq '.checks.bls'
# Should show: "message": "node has the correct BLS key"
```
@@ -769,7 +792,7 @@ Testing conducted on a single Lux validator node (testnet mode, macOS):
**Benchmark Command:**
```bash
cd ~/work/lux/benchmarks
NODE_ENDPOINT="http://localhost:9640/ext/bc/C/rpc" \
NODE_ENDPOINT="http://localhost:9640/v1/bc/C/rpc" \
PRIVATE_KEY="<funded_key>" \
./bin/bench tps --chains=lux --duration=60s --concurrency=5
```
+1 -1
View File
@@ -212,7 +212,7 @@ run-testnet: build-fips init-chains
node-status:
@echo "$(GREEN)Checking node status...$(NC)"
@curl -s -X POST --data '{"jsonrpc":"2.0","id":1,"method":"info.isBootstrapped","params":{}}' \
-H 'content-type:application/json;' http://localhost:9630/ext/info | jq
-H 'content-type:application/json;' http://localhost:9630/v1/info | jq
stop-node:
@echo "$(YELLOW)Stopping Lux node...$(NC)"
+38
View File
@@ -0,0 +1,38 @@
Lux Node
Copyright (c) 2019-2025 Lux Industries Inc.
This product includes software from avalanchego by Ava Labs, Inc.
(https://github.com/ava-labs/avalanchego), licensed under the BSD 3-Clause
License:
Copyright (C) 2019, Ava Labs, Inc.
Lux Node is derived from avalanchego. The Lux additions and modifications in
this repository are licensed under the BSD 3-Clause License (see the LICENSE
file). The BSD 3-Clause terms of the upstream avalanchego code are retained;
this NOTICE preserves the required Ava Labs copyright attribution at the
repository level.
--------------------------------------------------------------------------
Ethereum Virtual Machine support is derived from go-ethereum by The
go-ethereum Authors (https://github.com/ethereum/go-ethereum). In this
repository that code is NOT vendored in-tree; it is consumed as an external
Go module through the Lux fork github.com/luxfi/geth, which retains
go-ethereum's original licenses:
* The go-ethereum library packages are licensed under the GNU Lesser
General Public License, version 3 (LGPL-3.0-or-later).
* The go-ethereum command-line tools are licensed under the GNU General
Public License, version 3 (GPL-3.0).
Copyright (C) The go-ethereum Authors
COPYLEFT NOTICE: The LGPL-3.0/GPL-3.0 terms continue to apply to the
go-ethereum-derived portions provided via github.com/luxfi/geth, and are NOT
superseded by the BSD 3-Clause license of this repository. Distribution of
compiled Lux Node binaries must honor LGPL-3.0 for the linked go-ethereum
library code (source availability for that code and its modifications, and
the ability for users to relink against a modified library). The luxfi/geth
source, including Lux modifications, is published at
https://github.com/luxfi/geth.
+1 -1
View File
@@ -3066,7 +3066,7 @@ This version is backwards compatible to [v1.9.0](https://github.com/luxfi/node/r
- Fixed `x/merkledb.ChangeProof#getLargestKey` to correctly handle no changes
- Added test for `xvm/txs/executor.SemanticVerifier#verifyFxUsage` with multiple valid fxs
- Fixed CPU + bandwidth performance regression during vertex processing
- Added example usage of the `/ext/index/X/block` API
- Added example usage of the `/v1/index/X/block` API
- Reduced the default value of `--consensus-optimal-processing` from `50` to `10`
- Updated the year in the license header
+1 -1
View File
@@ -145,7 +145,7 @@ Production implementations live in `lux/crypto/` and `lux/lattice/`. Formal veri
| `audits/2025-12-30-other-vms-audit.md` | Secondary VMs |
| `audits/2025-12-30-platformvm-audit.md` | PlatformVM (P-Chain) |
| `audits/2025-12-30-proposervm-evm-audit.md` | ProposerVM and EVM integration |
| `audits/2025-12-30-thresholdvm-audit.md` | ThresholdVM (T-Chain) |
| `audits/2025-12-30-mpcvm-audit.md` | ThresholdVM (T-Chain) |
| `audits/2025-12-30-warp-audit.md` | Warp cross-chain messaging |
| `audits/2025-12-30-zkvm-audit.md` | ZKVM (Z-Chain) |
+63 -16
View File
@@ -157,8 +157,27 @@ func (b *blockHandler) sampleAncestorBeacons() (set.Set[ids.NodeID], bool) {
if len(connected) == 0 {
return nil, false
}
start := int(b.bsRotor.Add(1)-1) % len(connected)
// Prefer beacons that reported an ahead tip in the last frontier round — they HOLD the
// ancestry the descent needs, so asking them (rather than a rotated peer that may be at
// genesis) is what lets a re-bootstrapping node obtain ancestry even when ≥2 peers are still
// at genesis. This is ava's PeerTracker "ask a prover" bias sourced from the frontier replies
// we already have (no separate tracker). Fill any remaining slots from the rotated full set so
// the sample never shrinks below what blind rotation would pick (defense against a stale/empty
// ahead-set). Empty ahead-set ⇒ pure rotation, identical to prior behavior.
b.bsMu.Lock()
ahead := b.bsAheadBeacons
b.bsMu.Unlock()
sample := set.NewSet[ids.NodeID](bootstrapAncestorSample)
for _, id := range connected {
if sample.Len() >= bootstrapAncestorSample {
break
}
if ahead != nil && ahead.Contains(id) {
sample.Add(id)
}
}
start := int(b.bsRotor.Add(1)-1) % len(connected)
for i := 0; i < len(connected) && sample.Len() < bootstrapAncestorSample; i++ {
sample.Add(connected[(start+i)%len(connected)])
}
@@ -318,7 +337,7 @@ func (b *blockHandler) FrontierTip(ctx context.Context) (ids.ID, chainbootstrap.
switch {
case err == nil:
// A configured-beacon quorum named a safe sync anchor (NOT a finality cert — the loop
// re-executes the descent and re-enters consensus, where ConsensusQuorum alone governs).
// re-executes the descent and re-enters consensus, where FinalityQuorum alone governs).
// With the P-ready gate above, this judgement is over the TRUE FULL staked set, so a tip the
// quorum actively names is a real frontier (when it equals our own held tip, a ⅔-of-responders
// supermajority is AT our height, so we ARE at the network frontier — the loop's Accepted()-shortcut
@@ -538,6 +557,17 @@ func (b *blockHandler) collectFrontierReplies(ctx context.Context, connected []i
replies := make([]BeaconReply, 0, len(connected))
seen := make(map[ids.NodeID]struct{}, len(connected))
// ahead = beacons reporting a tip this node has NOT accepted (they hold blocks we lack, so
// they can SERVE the ancestry the descent needs). sampleAncestorBeacons prefers them so the
// GetAncestors sample targets peers that can serve — never a peer still at genesis (our own
// tip). Recorded per round; publishes into b.bsAheadBeacons before returning.
ahead := set.NewSet[ids.NodeID](len(connected))
record := func() []BeaconReply {
b.bsMu.Lock()
b.bsAheadBeacons = ahead
b.bsMu.Unlock()
return replies
}
deadline := time.After(bootstrapFrontierWindow)
for {
select {
@@ -551,13 +581,16 @@ func (b *blockHandler) collectFrontierReplies(ctx context.Context, connected []i
}
seen[rep.nodeID] = struct{}{}
replies = append(replies, BeaconReply{NodeID: rep.nodeID, Tip: rep.tip, Weight: w})
if _, accepted := b.acceptedHeight(rep.tip); !accepted {
ahead.Add(rep.nodeID) // reported a tip we have not accepted → genuinely ahead
}
if len(seen) >= len(connected) {
return replies // every connected beacon answered — resolve now, do not wait the window
return record() // every connected beacon answered — resolve now, do not wait the window
}
case <-deadline:
return replies
return record()
case <-ctx.Done():
return replies
return record()
}
}
}
@@ -581,7 +614,15 @@ func (b *blockHandler) Ancestors(ctx context.Context, blockID ids.ID, maxBlocks
requestID := b.requestIDCounter
b.contextRequestMu.Unlock()
ch := make(chan [][]byte, 1)
// Buffer the whole sample so EVERY sampled beacon's reply can queue — the loop
// then skips the EMPTY ones (a beacon that lacks the requested block, e.g. a peer
// still at genesis) and returns the first NON-EMPTY batch. With a size-1 channel a
// fast empty reply won the race and starved a slower peer that actually held the
// ancestry, so a re-bootstrapping node with ≥2 peers at genesis could keep drawing
// empties and stall. This mirrors the proven avalanchego contract: an empty Ancestors
// reply means "this peer can't serve — take another's," never "done" (getter serves an
// EXPLICIT empty batch when it lacks the block; see GetContext).
ch := make(chan [][]byte, bootstrapAncestorSample)
b.bsMu.Lock()
b.bsAncestorCh[requestID] = ch
b.bsMu.Unlock()
@@ -597,13 +638,19 @@ func (b *blockHandler) Ancestors(ctx context.Context, blockID ids.ID, maxBlocks
}
b.net.Send(msg, sample, b.networkID, 0)
select {
case blocks := <-ch:
return blocks, nil
case <-time.After(bootstrapAncestorsTimeout):
return nil, nil
case <-ctx.Done():
return nil, ctx.Err()
deadline := time.After(bootstrapAncestorsTimeout)
for {
select {
case blocks := <-ch:
if len(blocks) == 0 {
continue // this beacon can't serve the block — wait for a peer that can
}
return blocks, nil
case <-deadline:
return nil, nil // no beacon in the sample served — the loop re-samples (rotated)
case <-ctx.Done():
return nil, ctx.Err()
}
}
}
@@ -816,7 +863,7 @@ func (b *blockHandler) BootstrapFailure() error {
// is correctly failing safe DOWN (VM in Bootstrapping, serving nothing as head) and waiting for the
// quorum to return; the network cannot make progress without it. monitorBootstrap's no-progress
// watchdog polls this so it does NOT force-STOP a node that is deliberately waiting — which, given
// the K8s probes only poll the always-green /ext/health/liveness, would be a permanent brick. It is
// the K8s probes only poll the always-green /v1/health/liveness, would be a permanent brick. It is
// the discriminator between "stuck on a served gap" (a real stall → stop) and "waiting for the
// quorum" (self-heal → keep waiting). Distinct from BootstrapFailed (a terminal/structural fail).
func (b *blockHandler) BootstrapConnecting() bool { return b.bootstrapConnecting.Load() }
@@ -921,7 +968,7 @@ func (b *blockHandler) runBootstrapThenPoll(ctx context.Context) {
// no-progress watchdog treats it as a deliberate WAIT, not a stall: the node stays in Bootstrapping
// (serving nothing as head, NEVER live at the stale height) and CONVERGES the instant the quorum
// returns — the in-process self-heal the K8s probes do NOT provide (they poll the always-green
// /ext/health/liveness, so a fail-safe-DOWN node is never restarted). A STRUCTURAL failure (deep gap
// /v1/health/liveness, so a fail-safe-DOWN node is never restarted). A STRUCTURAL failure (deep gap
// → state-sync) or an exhausted attempt bound returns false WITHOUT going Ready, bootstrapFailed
// recording the reason so monitorBootstrap surfaces it. The node NEVER false-completes at its stale
// height, and a transient outage NEVER becomes a permanent brick.
@@ -963,7 +1010,7 @@ func (b *blockHandler) runInitialSync(ctx context.Context) bool {
// (eclipse / partition / a majority co-restart still in flight) is RE-ATTEMPTED — the node stays
// in Bootstrapping (engine alive, VM serving nothing as head, never live at the stale height)
// and CONVERGES the instant the quorum returns. This is the recovery the K8s probes do NOT
// provide (they all poll the always-green /ext/health/liveness, so a fail-safe-DOWN node is
// provide (they all poll the always-green /v1/health/liveness, so a fail-safe-DOWN node is
// never restarted). bootstrapMaxAttempts ≤ 0 ⇒ retry until the quorum returns or shutdown; a
// test pins it to 1 to assert the single-attempt terminal fail-safe. A STRUCTURAL failure (deep
// gap → state-sync) is NOT retried — a retry cannot fix it; it is surfaced for the operator.
+90 -6
View File
@@ -217,6 +217,13 @@ type bsBeaconNet struct {
serveAncestors bool // beacons serve ancestry (false models name-only beacons)
ancestorsEmpty bool // beacons REPLY to GetAncestors but serve an EMPTY batch (cross-version / withholding)
// emptyResponders models a MIXED fleet on a WIPE-path recovery: the named beacons REPLY to
// GetAncestors with an EMPTY batch (they lack the block — e.g. still at genesis after a
// simultaneous wipe), while the rest serve the real ancestry. When set, the mock delivers the
// empties FIRST, proving the fetcher (blockHandler.Ancestors) skips them and still returns the
// non-empty batch — never starved by a peer that can't serve. Requires serveAncestors=true.
emptyResponders set.Set[ids.NodeID]
// tipFor optionally overrides the tip a specific beacon reports (models DISAGREEMENT —
// beacons connected but split across tips, so no ⅔ quorum forms → FrontierNoQuorum).
tipFor map[ids.NodeID]ids.ID
@@ -334,6 +341,23 @@ func (n *bsBeaconNet) Send(msg message.OutboundMessage, nodeIDs set.Set[ids.Node
n.bh.deliverBootstrapFrontier(n.malicious, n.forgedTip)
}
case "ancestors":
if n.emptyResponders != nil {
// Mixed fleet: emptyResponders reply EMPTY (they lack the block), the rest serve.
// Deliver the empties FIRST so the test proves Ancestors skips them and returns the
// slower non-empty batch (the WIPE-path ≥2-at-genesis case).
var servers []ids.NodeID
for id := range nodeIDs {
if n.emptyResponders.Contains(id) {
n.bh.deliverBootstrapAncestors(m.requestID, nil)
} else {
servers = append(servers, id)
}
}
for range servers {
n.bh.deliverBootstrapAncestors(m.requestID, n.frame(m.blockID))
}
return nil
}
if n.serveAncestors {
n.bh.deliverBootstrapAncestors(m.requestID, n.frame(m.blockID))
} else if n.ancestorsEmpty {
@@ -512,6 +536,66 @@ func TestNodeBootstrap_EmptyNodeConvergesViaTransport(t *testing.T) {
require.True(t, bh.Accepted(ctx, chain[N].id), "node must hold the tip after sync")
}
// TestNodeBootstrap_WipePath_MixedGenesisPeers_ObtainsAncestry is the regression guard for the
// WIPE-path ≥2-at-genesis stall (deliverable 3). A re-bootstrapping node samples a fleet where
// SOME beacons reply to GetAncestors with an EMPTY batch (they lack the block — still at genesis
// after a simultaneous wipe) while the rest serve the real ancestry, and the empties arrive FIRST.
// With the pre-fix size-1 reply channel + first-reply-wins, the empty reply won the race and the
// good peer's non-empty batch was dropped, so the descent got nothing every round and stalled. The
// fix buffers the whole sample and SKIPS empty batches, returning the first non-empty one — so the
// node obtains ancestry from the peers that CAN serve, even when ≥2 peers are at genesis.
func TestNodeBootstrap_WipePath_MixedGenesisPeers_ObtainsAncestry(t *testing.T) {
const N = 30
chain, byID := buildBSChain(N, -1)
vm := newBSVM(chain)
bh, chainID, beacons := newBSHandlerAndEngine(t, vm, 5)
// 2 of 5 beacons are "still at genesis" — they reply EMPTY to GetAncestors. The mock
// delivers those empties FIRST so a first-reply-wins fetcher would be starved.
empties := set.NewSet[ids.NodeID](2)
empties.Add(beacons[0], beacons[1])
bh.net = &bsBeaconNet{
bh: bh, chainID: chainID, connected: beacons, byID: byID, tip: chain[N],
serveAncestors: true, emptyResponders: empties,
}
bh.msgCreator = bsMsgBuilder{}
ctx := context.Background()
require.NoError(t, runBS(t, bh), "node must converge despite ≥2 peers serving empty ancestry")
last, _ := vm.LastAccepted(ctx)
require.Equal(t, chain[N].id, last, "node must sync to the tip via the peers that CAN serve")
require.True(t, bh.Accepted(ctx, chain[N].id))
}
// TestSampleAncestorBeacons_PrefersAheadBeacons proves change 2: the ancestry sample PREFERS
// beacons the frontier round found genuinely AHEAD (they hold the ancestry), so a re-bootstrapping
// node asks peers that can serve rather than wasting the bounded sample on genesis peers. With more
// connected beacons than the sample size, blind rotation would periodically EXCLUDE any given
// beacon; the preference guarantees the recorded ahead-beacon is ALWAYS sampled.
func TestSampleAncestorBeacons_PrefersAheadBeacons(t *testing.T) {
const numBeacons = 8 // > bootstrapAncestorSample (4), so rotation alone would sometimes miss one
chain, byID := buildBSChain(1, -1)
vm := newBSVM(chain)
bh, chainID, beacons := newBSHandlerAndEngine(t, vm, numBeacons)
bh.net = &bsBeaconNet{bh: bh, chainID: chainID, connected: beacons, byID: byID, tip: chain[1]}
bh.msgCreator = bsMsgBuilder{}
// Record ONE beacon as ahead (the frontier round's output). It must appear in EVERY sample.
ahead := beacons[numBeacons-1]
bh.bsMu.Lock()
bh.bsAheadBeacons = set.Of(ahead)
bh.bsMu.Unlock()
for i := 0; i < 2*numBeacons; i++ {
sample, ok := bh.sampleAncestorBeacons()
require.True(t, ok)
require.LessOrEqual(t, sample.Len(), bootstrapAncestorSample)
require.True(t, sample.Contains(ahead),
"the recorded ahead-beacon must be preferred into every ancestry sample (call %d)", i)
}
}
// TestRED_FrozenVMLastAccepted_ConvergesOffFinalizedLedger is the regression guard for red
// HIGH-1: the convergence-recognition must ride the IN-PROCESS consensus finalized ledger, NOT
// the VM's LastAccepted cache — which the real ZAP client FREEZES at the boot snapshot for the
@@ -775,9 +859,9 @@ func TestNodeBootstrap_NoBeaconSet_ReportsNoBeacons(t *testing.T) {
func TestNodeBootstrap_FreshNet_SelfVoteUnderFullConnectivity_CaughtUp(t *testing.T) {
const N = 5
chain, byID := buildBSChain(N, -1)
vm := newBSVM(chain) // the node is at genesis (lastAccepted = genesis)
vm := newBSVM(chain) // the node is at genesis (lastAccepted = genesis)
bh, chainID, beacons := newBSHandlerAndEngine(t, vm, 2) // 2 equal-weight (100) beacons
bh.selfNodeID = beacons[0] // THIS node is beacon 0 — it is itself a validator
bh.selfNodeID = beacons[0] // THIS node is beacon 0 — it is itself a validator
bh.msgCreator = bsMsgBuilder{}
// FULL connectivity: the ONE other beacon is connected and reports GENESIS (a fresh net — every
@@ -824,7 +908,7 @@ func TestNodeBootstrap_SelfVote_PeerAheadDefeatsCaughtUp(t *testing.T) {
func TestNodeBootstrap_SelfVote_PartialConnectivityFailsSafe(t *testing.T) {
const N = 5
chain, byID := buildBSChain(N, -1)
vm := newBSVM(chain) // node at genesis
vm := newBSVM(chain) // node at genesis
bh, chainID, beacons := newBSHandlerAndEngine(t, vm, 3) // self + 2 others
bh.selfNodeID = beacons[0]
bh.msgCreator = bsMsgBuilder{}
@@ -1528,7 +1612,7 @@ func TestRED_TipHolderCoRestartGoesReadyAtOwnTip(t *testing.T) {
// It must NOT go live at its stale height (safety) and must NOT permanently give up (liveness): it
// stays in Bootstrapping, RE-ATTEMPTS (bootstrapMaxAttempts ≤ 0 ⇒ until the quorum returns), and
// CONVERGES the instant the quorum comes back — all IN-PROCESS, with no pod restart (the K8s probes
// poll the always-green /ext/health/liveness and would never restart it). This is the bounded
// poll the always-green /v1/health/liveness and would never restart it). This is the bounded
// re-bootstrap retry that closes the "permanent brick" RED flagged.
func TestRED_MajorityOutageSelfHealsWhenQuorumReturns(t *testing.T) {
const N, K = 30, 16 // stale at N; the live frontier (once the quorum returns) is N+K
@@ -1852,8 +1936,8 @@ func TestBootstrap_AcceptedHeight_StoreVsAcceptance(t *testing.T) {
const M = 30
const Top = 40
chain, _ := buildBSChain(Top, -1)
vm := newBSVMAt(chain, M) // accepted 0..M
vm.store(chain[M+1 : Top+1]...) // M+1..Top GOSSIPED into the store but UNACCEPTED
vm := newBSVMAt(chain, M) // accepted 0..M
vm.store(chain[M+1 : Top+1]...) // M+1..Top GOSSIPED into the store but UNACCEPTED
bh := &blockHandler{logger: log.NewNoOpLogger(), vm: vm}
ctx := context.Background()
+63 -10
View File
@@ -12,7 +12,7 @@
// to recover from. Bootstrap trust was braided into consensus finality, and finality's ⅔ rule
// is mathematically unsatisfiable during a mass outage.
//
// The fix is a type split, NOT a renamed threshold. ConsensusQuorum decides FINALITY
// The fix is a type split, NOT a renamed threshold. FinalityQuorum decides FINALITY
// (> ⅔ of CURRENT stake — UNCHANGED). BootstrapTrust decides whether a fetched frontier is
// SAFE TO BEGIN SYNC FROM: a quorum of AUTHENTICATED CONFIGURED beacons that RESPOND, gated by
// a response FLOOR (MinResponses) and an agreement threshold over the RESPONDERS (not over the
@@ -24,6 +24,7 @@ package chains
import (
"bytes"
"context"
"encoding/binary"
"errors"
"fmt"
"math"
@@ -37,30 +38,30 @@ import (
// BootstrapTrust is not a consensus-finality oracle.
// It selects a weak-subjective sync frontier from authenticated configured beacons.
// Live block acceptance remains governed exclusively by ConsensusQuorum.
// Live block acceptance remains governed exclusively by FinalityQuorum.
type BootstrapTrust interface {
// AcceptsFrontier returns the block an empty/behind node may BEGIN SYNCING FROM, selected
// from the authenticated configured beacons' frontier replies — or an error
// (ErrInsufficientBootstrapResponses / ErrNoBootstrapQuorum) when no trusted frontier can be
// named this round. The returned Frontier is a sync ANCHOR, never a consensus certificate
// (see the type comment): the node must still re-execute every block it descends to before
// re-entering live consensus, where ConsensusQuorum alone governs acceptance.
// re-entering live consensus, where FinalityQuorum alone governs acceptance.
AcceptsFrontier(ctx context.Context, replies []BeaconReply) (*Frontier, error)
}
// ConsensusQuorum decides FINALITY: whether a weight is a finalizing supermajority (> ⅔) of the
// FinalityQuorum decides FINALITY: whether a weight is a finalizing supermajority (> ⅔) of the
// CURRENT validator set. This is the live-consensus rule; bootstrap does NOT change it. It is a
// SEPARATE named type from BootstrapTrust precisely so the distinction is explicit and testable:
// a frontier that AcceptsFrontier admits is "safe to sync from", and in general it does NOT
// satisfy HasFinality (3 of 5 responders is a valid sync anchor; 3 of 5 stake is not finality).
type ConsensusQuorum interface {
type FinalityQuorum interface {
HasFinality(weight, total StakeWeight) bool
}
// StakeWeight is validator stake in the units the validator manager reports (Weight/Light).
type StakeWeight = uint64
// twoThirdsFinality is the production ConsensusQuorum: > ⅔ of the CURRENT total stake, exactly
// twoThirdsFinality is the production FinalityQuorum: > ⅔ of the CURRENT total stake, exactly
// the rule the live cert-gate uses (consensusconfig.TwoThirdsStakeFloor). Defined here only to
// give the live rule a name to CONTRAST bootstrap trust against — it is not wired into the live
// path (that already enforces ⅔ inside consensus), and bootstrap never calls it to ACCEPT.
@@ -70,10 +71,10 @@ func (twoThirdsFinality) HasFinality(weight, total StakeWeight) bool {
return weight > consensusconfig.TwoThirdsStakeFloor(total)
}
// DefaultConsensusQuorum returns the live ⅔-of-current-stake finality rule — the thing bootstrap
// DefaultFinalityQuorum returns the live ⅔-of-current-stake finality rule — the thing bootstrap
// trust is explicitly NOT. Used by the test suite to prove a bootstrap-accepted frontier does
// not constitute finality.
func DefaultConsensusQuorum() ConsensusQuorum { return twoThirdsFinality{} }
func DefaultFinalityQuorum() FinalityQuorum { return twoThirdsFinality{} }
var (
// ErrInsufficientBootstrapResponses: fewer than MinResponses configured beacons answered.
@@ -127,9 +128,48 @@ type AncestrySource interface {
// Checkpoint is an operator-pinned (id, height) the recovering node may anchor to when too few
// beacons respond to form a quorum — the EXPLICIT override for INVARIANT 2's "1 of N reachable"
// case. Absent (nil) ⇒ the default policy REJECTS rather than trusting a captured minority.
//
// INVARIANT 4 (a checkpoint is a SIGNED weak-subjectivity anchor, not a bare config value): the
// checkpoint carries a cryptographic Signature by the configured checkpoint AUTHORITY over its
// (id, height), and AcceptsFrontier trusts it ONLY when CheckpointVerifier authenticates that
// signature. A (id,height) present in a flag/config but UNSIGNED — or signed by a non-authority key
// — is REJECTED (fail closed). This is the crucial hardening: the checkpoint is the one path that
// bypasses the beacon quorum, so a compromised flag must NOT be able to inject a false sync anchor
// without ALSO forging the authority's signature. It is a cryptographic vouch, NEVER a ⅔-live-stake
// tally (that conflation is the very deadlock BootstrapTrust exists to avoid).
type Checkpoint struct {
ID ids.ID
Height uint64
// Signature is the checkpoint authority's signature over this checkpoint's canonical (id,height)
// bytes. Verified by CheckpointVerifier before the anchor is trusted; an empty signature is
// never accepted.
Signature []byte
}
// CheckpointVerifier authenticates a Checkpoint's Signature against the configured checkpoint
// AUTHORITY key(s). The node injects a real implementation backed by a PROVEN primitive (Ed25519 /
// BLS — never custom crypto); the policy stays free of any crypto dependency, exactly like
// AncestrySource and heightOf. A nil verifier means no signed anchor is configured, so any
// Checkpoint is untrusted and the below-floor case fails closed.
type CheckpointVerifier interface {
// VerifyCheckpoint reports whether sig is a valid signature over (id, height) by the configured
// checkpoint authority. It MUST reject an empty signature and be signature-safe (constant-time
// compare on the primitive). It is the sole authority on whether a pinned anchor may be trusted.
VerifyCheckpoint(id ids.ID, height uint64, sig []byte) bool
}
// CanonicalCheckpointMessage is the exact byte string a checkpoint authority signs and
// CheckpointVerifier authenticates: a domain-separated, fixed-layout encoding of (id, height) so a
// signature can never be transplanted from another context. 8-byte big-endian height after the
// 32-byte id, under a distinct domain tag.
func CanonicalCheckpointMessage(id ids.ID, height uint64) []byte {
const domain = "lux-bootstrap-checkpoint-v1\x00"
msg := make([]byte, 0, len(domain)+len(id)+8)
msg = append(msg, domain...)
msg = append(msg, id[:]...)
var h [8]byte
binary.BigEndian.PutUint64(h[:], height)
return append(msg, h[:]...)
}
// Ratio is an exact rational threshold (e.g. 2/3, 3/4). A value clears it iff
@@ -152,7 +192,7 @@ func (r Ratio) floorOf(whole uint64) uint64 {
}
// BootstrapPolicy is the default BootstrapTrust: a CONFIGURED-BEACON quorum with a response
// FLOOR and an agreement threshold over the RESPONDERS — a SEPARATE object from ConsensusQuorum
// FLOOR and an agreement threshold over the RESPONDERS — a SEPARATE object from FinalityQuorum
// with a SEPARATE threat model. It does NOT pass "reachable stake" into the ⅔-of-current-stake
// finality rule (that conflation IS the mass-recovery deadlock). It reuses the ancestor-tolerant
// common-ancestor tally only for HOW to find the agreed frontier; the ACCEPTANCE gate is the
@@ -197,8 +237,12 @@ type BootstrapPolicy struct {
// network, or a fleet unanimously AT the tip).
MinFrontierHeight uint64
// Checkpoint is the OPTIONAL operator override for the below-floor case (INVARIANT 2). nil ⇒
// reject below the floor.
// reject below the floor. When set, it is trusted ONLY if CheckpointVerifier authenticates its
// signature (INVARIANT 4).
Checkpoint *Checkpoint
// CheckpointVerifier authenticates the Checkpoint's authority signature (INVARIANT 4). nil ⇒ a
// configured Checkpoint is NOT trusted (fail closed) — a bare (id,height) is never enough.
CheckpointVerifier CheckpointVerifier
// NamingWindow bounds the ancestry fetched per anchor; MaxAnchors bounds how many distinct
// reported tips are resolved. Both default to the package constants when zero.
NamingWindow int
@@ -330,6 +374,15 @@ func (p *BootstrapPolicy) AcceptsFrontier(ctx context.Context, replies []BeaconR
// operator explicitly pinned a checkpoint to anchor from.
if !p.floorMet(responders, responderWeight) {
if p.Checkpoint != nil {
// INVARIANT 4: the checkpoint bypasses the beacon quorum, so trust it ONLY when the
// configured authority SIGNED this exact (id,height). A checkpoint present in config but
// unsigned, or signed by a non-authority key, is REJECTED (fail closed) — a compromised
// flag cannot inject a false sync anchor without also forging the authority's signature.
if p.CheckpointVerifier == nil || len(p.Checkpoint.Signature) == 0 ||
!p.CheckpointVerifier.VerifyCheckpoint(p.Checkpoint.ID, p.Checkpoint.Height, p.Checkpoint.Signature) {
return nil, fmt.Errorf("%w: a checkpoint is pinned but its authority signature did not verify",
ErrInsufficientBootstrapResponses)
}
return &Frontier{
ID: p.Checkpoint.ID,
Height: p.Checkpoint.Height,
+100 -12
View File
@@ -16,6 +16,7 @@ package chains
import (
"context"
"crypto/ed25519"
"testing"
"github.com/stretchr/testify/require"
@@ -302,7 +303,7 @@ func TestBootstrapTrust_F_SplitReachableAncestrySelectsCommonAncestor(t *testing
// TestBootstrapTrust_G_FinalityUnchanged proves INVARIANT 3: a bootstrap-accepted frontier is NOT
// finality. The SAME 3-of-5 support that AcceptsFrontier admits as a sync anchor does NOT satisfy
// ConsensusQuorum.HasFinality — live block acceptance still requires > ⅔ of CURRENT validator
// FinalityQuorum.HasFinality — live block acceptance still requires > ⅔ of CURRENT validator
// stake (4 of 5 here). The bootstrap quorum cannot finalize a block.
func TestBootstrapTrust_G_FinalityUnchanged(t *testing.T) {
const w uint64 = 100
@@ -321,39 +322,76 @@ func TestBootstrapTrust_G_FinalityUnchanged(t *testing.T) {
require.Equal(t, frontier, f.ID)
require.Equal(t, StakeWeight(3*w), f.Weight, "the frontier is backed by exactly the 3 responders")
// ConsensusQuorum says that SAME 3-of-5 weight is NOT finality — the decisions are different
// FinalityQuorum says that SAME 3-of-5 weight is NOT finality — the decisions are different
// objects with different thresholds. Finality is unchanged: it still needs > ⅔ (4 of 5).
cq := DefaultConsensusQuorum()
cq := DefaultFinalityQuorum()
require.False(t, cq.HasFinality(3*w, total),
"INVARIANT 3: a bootstrap-accepted frontier (3 of 5) is NOT a finalizing supermajority")
require.True(t, cq.HasFinality(4*w, total),
"finality UNCHANGED: > ⅔ of current stake (4 of 5) still finalizes")
require.False(t, cq.HasFinality(f.Weight, total),
"the bootstrap quorum's own backing weight cannot finalize a block")
// AFTER-SYNC (the owner's "bootstrap is not a finality bypass"): the node has now SYNCED to the
// frontier via BootstrapTrust and re-entered live consensus. A NEW block that collects the SAME
// 3-of-5 stake STILL does not finalize — bootstrap admitted a sync ANCHOR, it did not lower the
// finality bar. Live acceptance returns to strict > ⅔ of CURRENT stake, exactly as before any
// bootstrap. HasFinality is stateless in the bootstrap outcome, which is the whole point: there
// is no code path by which "we bootstrapped from 3/5" leaks into the finality decision.
require.False(t, cq.HasFinality(3*w, total),
"AFTER syncing from a 3-of-5 bootstrap frontier, live finality STILL needs > ⅔ (4 of 5) — no bypass")
}
// ----- checkpoint override (complements B) ----------------------------------
// edCheckpointAuthority is a test checkpoint authority backed by Ed25519 — a PROVEN primitive, no
// custom crypto. It signs a checkpoint's canonical (id,height) message and verifies against its own
// public key, rejecting an empty signature and any key that is not the configured authority.
type edCheckpointAuthority struct {
priv ed25519.PrivateKey
pub ed25519.PublicKey
}
func newEdCheckpointAuthority(t *testing.T) *edCheckpointAuthority {
t.Helper()
pub, priv, err := ed25519.GenerateKey(nil)
require.NoError(t, err)
return &edCheckpointAuthority{priv: priv, pub: pub}
}
func (a *edCheckpointAuthority) sign(id ids.ID, height uint64) []byte {
return ed25519.Sign(a.priv, CanonicalCheckpointMessage(id, height))
}
// VerifyCheckpoint implements CheckpointVerifier: authenticate against the authority's public key.
func (a *edCheckpointAuthority) VerifyCheckpoint(id ids.ID, height uint64, sig []byte) bool {
return len(sig) != 0 && ed25519.Verify(a.pub, CanonicalCheckpointMessage(id, height), sig)
}
// TestBootstrapTrust_CheckpointOverride: below the response floor (1 of 5), the DEFAULT is reject
// (test B), but an operator who pins a checkpoint gets the explicit override — the node anchors to
// the pinned (id,height) instead of trusting the lone beacon. This is the sanctioned escape hatch
// for a deeply-partitioned node, NEVER an open-ended ≥1-beacon acceptance.
// (test B), but an operator who pins a SIGNED checkpoint gets the explicit override — the node
// anchors to the authenticated (id,height) instead of trusting the lone beacon. This is the
// sanctioned escape hatch for a deeply-partitioned node, NEVER an open-ended ≥1-beacon acceptance,
// and (INVARIANT 4) NEVER a bare unsigned config value.
func TestBootstrapTrust_CheckpointOverride(t *testing.T) {
beacons := nodeIDs(5)
authority := newEdCheckpointAuthority(t)
ckptID := ids.GenerateTestID()
const ckptHeight = uint64(1_082_796)
policy := &BootstrapPolicy{
TrustedBeacons: equalBeacons(beacons, equalStake),
MinResponses: 3,
Checkpoint: &Checkpoint{ID: ckptID, Height: 1_082_796},
TrustedBeacons: equalBeacons(beacons, equalStake),
MinResponses: 3,
Checkpoint: &Checkpoint{ID: ckptID, Height: ckptHeight, Signature: authority.sign(ckptID, ckptHeight)},
CheckpointVerifier: authority,
}
// 1 reachable beacon — below the floor — but a checkpoint is pinned.
// 1 reachable beacon — below the floor — but a SIGNED checkpoint is pinned.
f, err := policy.AcceptsFrontier(context.Background(), []BeaconReply{
reply(beacons[0], ids.GenerateTestID(), equalStake),
})
require.NoError(t, err)
require.True(t, f.FromCheckpoint, "below the floor with a pinned checkpoint → anchor to the checkpoint")
require.True(t, f.FromCheckpoint, "below the floor with a SIGNED checkpoint → anchor to the checkpoint")
require.Equal(t, ckptID, f.ID)
require.Equal(t, uint64(1_082_796), f.Height)
require.Equal(t, ckptHeight, f.Height)
// Without the checkpoint the same 1-of-5 is rejected (the default — never trust the lone beacon).
policy.Checkpoint = nil
@@ -363,6 +401,56 @@ func TestBootstrapTrust_CheckpointOverride(t *testing.T) {
require.ErrorIs(t, err, ErrInsufficientBootstrapResponses)
}
// TestBootstrapTrust_CheckpointMustBeSigned is INVARIANT 4: a checkpoint that is present but not
// AUTHENTICATED is REJECTED (fail closed). A compromised flag/config that pins a false (id,height)
// cannot inject a sync anchor without the authority's signature. Four rejection modes, one accept.
func TestBootstrapTrust_CheckpointMustBeSigned(t *testing.T) {
beacons := nodeIDs(5)
authority := newEdCheckpointAuthority(t)
attacker := newEdCheckpointAuthority(t) // a DIFFERENT key — not the configured authority
ckptID := ids.GenerateTestID()
const h = uint64(500_000)
lone := []BeaconReply{reply(beacons[0], ids.GenerateTestID(), equalStake)} // 1-of-5, below floor
base := func() *BootstrapPolicy {
return &BootstrapPolicy{TrustedBeacons: equalBeacons(beacons, equalStake), MinResponses: 3, CheckpointVerifier: authority}
}
// (1) UNSIGNED checkpoint (empty signature) → rejected even with a verifier wired.
p := base()
p.Checkpoint = &Checkpoint{ID: ckptID, Height: h}
_, err := p.AcceptsFrontier(context.Background(), lone)
require.ErrorIs(t, err, ErrInsufficientBootstrapResponses, "an UNSIGNED checkpoint must be rejected")
// (2) signed by a NON-AUTHORITY (attacker) key → rejected.
p = base()
p.Checkpoint = &Checkpoint{ID: ckptID, Height: h, Signature: attacker.sign(ckptID, h)}
_, err = p.AcceptsFrontier(context.Background(), lone)
require.ErrorIs(t, err, ErrInsufficientBootstrapResponses, "a checkpoint signed by a non-authority key must be rejected")
// (3) authority signature over a DIFFERENT (id,height) — replay onto a forged anchor → rejected.
p = base()
forgedID := ids.GenerateTestID()
p.Checkpoint = &Checkpoint{ID: forgedID, Height: h, Signature: authority.sign(ckptID, h)} // sig binds ckptID, not forgedID
_, err = p.AcceptsFrontier(context.Background(), lone)
require.ErrorIs(t, err, ErrInsufficientBootstrapResponses, "a signature transplanted to a different (id,height) must be rejected")
// (4) NO verifier configured → any checkpoint is untrusted (fail closed).
p = base()
p.CheckpointVerifier = nil
p.Checkpoint = &Checkpoint{ID: ckptID, Height: h, Signature: authority.sign(ckptID, h)}
_, err = p.AcceptsFrontier(context.Background(), lone)
require.ErrorIs(t, err, ErrInsufficientBootstrapResponses, "no verifier ⇒ even a validly-signed checkpoint is untrusted")
// (accept) authority signs the exact pinned (id,height) → trusted.
p = base()
p.Checkpoint = &Checkpoint{ID: ckptID, Height: h, Signature: authority.sign(ckptID, h)}
f, err := p.AcceptsFrontier(context.Background(), lone)
require.NoError(t, err)
require.True(t, f.FromCheckpoint)
require.Equal(t, ckptID, f.ID)
}
// ----- safety guard for the global ancestor-tolerant tally ------------------
// TestBootstrapTrust_ForkAtSharedGenesisFailsSafe is the load-bearing guard for the
+24
View File
@@ -46,6 +46,30 @@ func (s *Nets) GetOrCreate(chainID ids.ID) (nets.Net, bool) {
return chain, true
}
// IsChainBootstrapped reports whether the given chain has finished initial sync
// (reached the network frontier and transitioned its VM to normal operation) on
// this node — Bootstrapped(chainID) was called for it in its validation net. A
// chain that is merely tracked (its sync goroutine launched) but has NOT converged
// reads false. This is the per-chain truth manager.IsBootstrapped / info.isBootstrapped
// key on, replacing the mere-existence test that returned true the instant a chain
// was added to the manager (the premature-true masking bug: a C-Chain stalled at
// genesis reported bootstrapped=true). A chainID is added to exactly one net's
// tracking, so the first net that reports it bootstrapped is authoritative.
func (s *Nets) IsChainBootstrapped(chainID ids.ID) bool {
if s == nil {
return false // no net tracking wired ⇒ nothing has been marked bootstrapped
}
s.lock.RLock()
defer s.lock.RUnlock()
for _, chain := range s.chains {
if chain.IsChainBootstrapped(chainID) {
return true
}
}
return false
}
// Bootstrapping returns the chainIDs of any chains that are still
// bootstrapping.
func (s *Nets) Bootstrapping() []ids.ID {
+139
View File
@@ -0,0 +1,139 @@
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// context_chunk_test.go — the heavy-block self-heal fix: GetContext must bound its Ancestors
// response by SERIALIZED SIZE (not just block COUNT) so it stays under the peer message cap.
//
// Benchmark-proven live bug: under 250-trader DEX load, heavy blocks made a 256-block context
// response sum to 3.4-5.7 MB > the 2 MB compressor cap; msgCreator.Ancestors FAILED to build,
// so a behind validator received NOTHING and could never resync (permanently stuck while the tip
// advanced). This pins the fix: the response is chunked to fit the budget, always serving at
// least one block so the behind node makes progress every round.
package chains
import (
"context"
"errors"
"testing"
"time"
consensuschain "github.com/luxfi/consensus/engine/chain"
consensusblock "github.com/luxfi/consensus/engine/chain/block"
"github.com/luxfi/constants"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/node/message"
)
// heavyBlock is a consensuschain.Block of a controlled byte size; only the methods GetContext
// touches (ID/Parent/Bytes) are overridden — the rest of the interface is embedded and unused.
type heavyBlock struct {
consensusblock.Block
id, parent ids.ID
bytes []byte
}
func (b *heavyBlock) ID() ids.ID { return b.id }
func (b *heavyBlock) Parent() ids.ID { return b.parent }
func (b *heavyBlock) Bytes() []byte { return b.bytes }
// sizeStubVM serves heavyBlocks by id (only GetBlock is called by GetContext).
type sizeStubVM struct {
consensuschain.BlockBuilder
blocks map[ids.ID]consensusblock.Block
}
func (v *sizeStubVM) GetBlock(_ context.Context, id ids.ID) (consensusblock.Block, error) {
b, ok := v.blocks[id]
if !ok {
return nil, errors.New("not found")
}
return b, nil
}
// sizeRecMsg records the containers GetContext hands to Ancestors, so the test can measure the
// assembled response size (the input the real zstd compressor would reject above the cap).
type sizeRecMsg struct {
message.OutboundMsgBuilder
containers [][]byte
}
func (m *sizeRecMsg) Ancestors(_ ids.ID, _ uint32, containers [][]byte) (message.OutboundMessage, error) {
m.containers = containers
return nil, nil
}
func TestGetContext_ChunksBySize_FitsUnderCap(t *testing.T) {
// A 100-block chain of ~150 KiB blocks. Packed by COUNT alone (up to maxContextBlocks=256,
// i.e. all 100), the response is ~15 MB — 7x over the 2 MB cap, so the old handler's message
// failed to build. Bounded by SIZE, it serves only as many as fit.
const nBlocks = 100
const blkSize = 150 * 1024
vm := &sizeStubVM{blocks: map[ids.ID]consensusblock.Block{}}
parent := ids.Empty
var tip ids.ID
for i := 0; i < nBlocks; i++ {
id := ids.GenerateTestID()
vm.blocks[id] = &heavyBlock{id: id, parent: parent, bytes: make([]byte, blkSize)}
parent = id
tip = id
}
msg := &sizeRecMsg{}
bh := &blockHandler{
logger: log.NewNoOpLogger(),
vm: vm,
msgCreator: msg,
net: &redStubNet{},
chainID: ids.GenerateTestID(),
networkID: ids.GenerateTestID(),
maxContextBlocks: 256,
}
if err := bh.GetContext(context.Background(), ids.GenerateTestNodeID(), 1, time.Now().Add(time.Second), tip); err != nil {
t.Fatalf("GetContext: %v", err)
}
total := 0
for _, c := range msg.containers {
total += len(c)
}
budget := constants.DefaultMaxMessageSize - 128*1024 // the handler's byteBudget
if len(msg.containers) == 0 {
t.Fatal("must serve at least one block (a behind node must always make progress)")
}
if total > budget {
t.Fatalf("context response %d bytes exceeds budget %d — the real Ancestors compressor would REJECT it "+
"(cap %d), stranding a behind validator (the live bug)", total, budget, constants.DefaultMaxMessageSize)
}
if len(msg.containers) >= nBlocks {
t.Fatalf("expected SIZE truncation (fewer than %d blocks), got %d — response not chunked", nBlocks, len(msg.containers))
}
t.Logf("size-chunked: %d blocks, %d payload bytes (budget %d, cap %d) — fits, so the compressor accepts it",
len(msg.containers), total, budget, constants.DefaultMaxMessageSize)
}
// A single heavy block is ALWAYS served even if it alone exceeds the budget — the walk must never
// deadlock (the trust-tiered validator cap gives such a block the send headroom; a stranger's
// tight cap correctly rejects it downstream).
func TestGetContext_SingleOversizeBlock_StillServed(t *testing.T) {
oversize := constants.DefaultMaxMessageSize + 1<<20 // > the cap on its own
id := ids.GenerateTestID()
vm := &sizeStubVM{blocks: map[ids.ID]consensusblock.Block{
id: &heavyBlock{id: id, parent: ids.Empty, bytes: make([]byte, oversize)},
}}
msg := &sizeRecMsg{}
bh := &blockHandler{
logger: log.NewNoOpLogger(), vm: vm, msgCreator: msg, net: &redStubNet{},
chainID: ids.GenerateTestID(), networkID: ids.GenerateTestID(), maxContextBlocks: 256,
}
if err := bh.GetContext(context.Background(), ids.GenerateTestNodeID(), 1, time.Now().Add(time.Second), id); err != nil {
t.Fatalf("GetContext: %v", err)
}
if len(msg.containers) != 1 {
t.Fatalf("a single (even oversize) block must be served so the walk never deadlocks, got %d blocks", len(msg.containers))
}
}
+193 -28
View File
@@ -160,6 +160,20 @@ var (
_ Manager = (*manager)(nil)
)
// quasarExportVM is the OPTIONAL two-tier-consensus (v1.36) export sink a VM may
// expose: the consensus engine pushes each Quasar (⅔-by-stake) EXPORT-FINAL
// frontier advance in, and re-seeds from the VM's durable height on boot, so the
// VM's `finalized`/`safe` tags and cross-chain export gate track ⅔-stake
// finality instead of the reorgable Nova accept tip. NOT part of chain.ChainVM —
// generic VMs never implement it and run Nova-only. For a plugin VM the concrete
// implementation is in another process; the rpcchainvm client carries these
// across the boundary and reports whether the plugin advertised the capability
// via SupportsQuasarExport (see the wiring in createChain).
type quasarExportVM interface {
SetLastQuasarFinalized(uint64)
LastQuasarHeight() uint64
}
// Manager manages the chains running on this node.
// It can:
// - Create a chain
@@ -219,7 +233,7 @@ type ChainParameters struct {
FxIDs []ids.ID
// Invariant: Only used when [ID] is the P-chain ID.
CustomBeacons validators.Manager
// Name of the chain (used for HTTP routing alias, e.g., /ext/bc/zoo/rpc)
// Name of the chain (used for HTTP routing alias, e.g., /v1/bc/zoo/rpc)
Name string
}
@@ -743,7 +757,7 @@ func (m *manager) createChain(chainParams ChainParameters) {
// plugin shows up later (e.g. via lpm install).
//
// The old behavior (always register a failing health check) made
// /ext/health return 503 for any opted-out chain, which made
// /v1/health return 503 for any opted-out chain, which made
// kubelet liveness probes kill the validator pod. That made
// chain participation effectively all-or-nothing per validator.
// Now: validators participate per-plugin, opt-in.
@@ -849,13 +863,13 @@ func (m *manager) createChain(chainParams ChainParameters) {
chainBase := fmt.Sprintf("bc/%s", chainAlias)
chainIDBase := fmt.Sprintf("bc/%s", chainParams.ID.String())
// AddRoute will build the full path as /ext/<base><endpoint>
// AddRoute will build the full path as /v1/<base><endpoint>
m.Server.AddRoute(handler, chainBase, endpoint)
if chainAlias != chainParams.ID.String() {
m.Server.AddRoute(handler, chainIDBase, endpoint)
}
// Also register with chain name alias for user-friendly routing (e.g., /ext/bc/zoo/rpc)
// Also register with chain name alias for user-friendly routing (e.g., /v1/bc/zoo/rpc)
if chainParams.Name != "" {
nameLower := strings.ToLower(chainParams.Name)
nameBase := fmt.Sprintf("bc/%s", nameLower)
@@ -928,9 +942,9 @@ func (m *manager) createChain(chainParams ChainParameters) {
m.Log.Info("║ VM ID:", log.Stringer("vmID", chainParams.VMID))
m.Log.Info("║ Network ID:", log.Stringer("chainID", chainParams.ChainID))
m.Log.Info("║ Endpoints available at:")
m.Log.Info("║ → /ext/bc/" + chainParams.ID.String())
m.Log.Info("║ → /v1/bc/" + chainParams.ID.String())
if chainAlias != chainParams.ID.String() {
m.Log.Info("║ → /ext/bc/" + chainAlias)
m.Log.Info("║ → /v1/bc/" + chainAlias)
}
m.Log.Info("╚══════════════════════════════════════════════════════════════════╝")
@@ -1162,7 +1176,7 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
vmConfigBytes := m.injectAutominingConfig(chainParams.VMID, chainConfig.Config)
vmConfigBytes = m.injectSecurityProfileConfig(chainParams.VMID, vmConfigBytes)
// CONSENSUS-SAFETY (single-proposer-per-height): re-wrap multi-validator
// linear chains in proposervm so block production follows the Snowman++
// linear chains in proposervm so block production follows the proposervm's
// proposer schedule — exactly ONE validator builds height H, the rest wait
// and vote. Without it every validator's engine calls BuildBlock
// UNCONDITIONALLY at every height off a slightly-different mempool, so two
@@ -1199,6 +1213,10 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
return nil, fmt.Errorf("refusing to start multi-node chain %s with non-BFT consensus params: %w", chainParams.ID, err)
}
}
// v1.36 "Nova": the round-scoped VIEW-CHANGE (prevote/POL/lock) was DELETED from the
// consensus engine (174af3c31). Nova metastable sampling is the sole decider and the ⅔
// Quasar attestation trails it — there is no view-change to opt into, so the former
// LUX_CONSENSUS_VIEW_CHANGE env gate is gone. Keep the braid dead.
_, innerIsDAGNative := vmTyped.(interface {
Linearize(context.Context, ids.ID, chan<- vm.Message) error
})
@@ -1505,7 +1523,7 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
// the proposervm, whose SignedBlock carries the real P-chain height
// (selectChildPChainHeight = max(GetCurrentHeight, parentH)) and exposes
// PChainHeight() — the SAME value the engine's pChainHeightOf reads. That
// is precisely the Snowman++ mechanism newPChainHeightVM was a stand-in
// is precisely the proposervm mechanism newPChainHeightVM was a stand-in
// for, so stacking both would double-stamp the height. We keep
// newPChainHeightVM only for the unwrapped K>1 chains (P-Chain, X-Chain).
if blockBuilder != nil && !wrapInProposerVM {
@@ -1516,8 +1534,56 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
log.Stringer("networkID", networkID))
}
}
// EXPORT-FRONTIER BRIDGE (two-tier consensus, v1.36). VM.Accept now advances the
// local NOVA (bare-majority) accept tip, which is reorgable and MUST NOT be exported.
// Push each EXPORT (Quasar, ⅔-by-stake) frontier advance into the VM so the EVM
// `finalized`/`safe` block tags and the warp cross-chain export gate resolve to the
// Quasar tip, NEVER the Nova tip (the "semantic collapse" the split exists to prevent).
//
// Capability-gated, not just interface-gated: the C-Chain EVM runs as a SEPARATE
// rpcchainvm plugin process, so vmTyped here is the rpcchainvm *Client, which carries
// SetLastQuasarFinalized/LastQuasarHeight for EVERY plugin (they cross the ZAP
// boundary). The client learns from the plugin's Initialize handshake whether the
// concrete VM actually implements the export capability and reports it via
// SupportsQuasarExport — false → the observer stays unwired and this chain is Nova-only
// with no per-finalization cross-process no-op. A VM WITHOUT the probe (an in-process
// VM whose concrete export methods we hold directly) is treated as capable, preserving
// the direct-wire path. Push into the RAW inner VM (vmTyped) — the eth/warp backends
// live there, not on the proposervm wrapper.
//
// Ordering: the observer MUST be set on netCfg BEFORE NewRuntime captures it; the boot
// re-seed needs the constructed engine and so runs after. exportVM (nil unless capable)
// carries the wired/not-wired decision across that split — a value, not a re-derived
// predicate.
var exportVM quasarExportVM
if qvm, ok := vmTyped.(quasarExportVM); ok {
capable := true
if probe, hasProbe := vmTyped.(interface{ SupportsQuasarExport() bool }); hasProbe {
capable = probe.SupportsQuasarExport()
}
if capable {
exportVM = qvm
netCfg.QuasarObserver = func(_ ids.ID, height uint64) {
qvm.SetLastQuasarFinalized(height)
}
m.Log.Info("wired EXPORT-frontier (quasar) bridge into the VM (finalized/safe + warp gate track ⅔-stake finality)",
log.Stringer("chainID", chainParams.ID))
}
}
consensusEngine := consensuschain.NewRuntime(netCfg)
// Re-seed the consensus EXPORT frontier from the VM's DURABLE Quasar height so
// GetQuasarTip / QuasarHeight do not regress on restart (the in-memory frontier resets
// to (Empty,0) until a fresh ⅔-stake cert re-forms; the VM persisted the export height).
// Advance-only; the observer above refines it as new certs land this session.
if exportVM != nil {
if h := exportVM.LastQuasarHeight(); h > 0 {
consensusEngine.SyncQuasarFrontier(ids.Empty, h)
m.Log.Info("re-seeded consensus export (quasar) frontier from the VM's durable height on boot",
log.Stringer("chainID", chainParams.ID), log.Uint64("quasarHeight", h))
}
}
// 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
@@ -2080,7 +2146,7 @@ func (m *manager) monitorBootstrap(engine Engine, h handler.Handler, sb nets.Net
// this true. The no-progress watchdog treats it as a deliberate quorum WAIT, not a stall: it must
// NOT force-STOP a node that is correctly failing safe DOWN and waiting for its quorum to return
// (the network cannot progress without the quorum, and the K8s probes — all polling the
// always-green /ext/health/liveness — would never restart it, so a stop here is a permanent
// always-green /v1/health/liveness — would never restart it, so a stop here is a permanent
// brick). The node stays in Bootstrapping (serving nothing as head) and converges when the quorum
// returns. nil for degenerate handlers (legacy behavior).
type bootstrapConnector interface{ BootstrapConnecting() bool }
@@ -2252,6 +2318,17 @@ func watchBootstrapProgress(
}
}
// IsBootstrapped reports whether [id] has ACTUALLY finished initial sync on this
// node: the chain exists AND its validation net has marked it bootstrapped —
// monitorBootstrap called sb.Bootstrapped(id) only after runInitialSync reached the
// named network frontier and transitioned the VM to normal operation (head advanced
// to the frontier, eth-RPC serving live). The old body returned true the instant the
// chain merely EXISTED in m.chains — set right after createChain launched the (async,
// possibly-stalling) bootstrap goroutine — so a C-Chain stalled at genesis (head 0x0,
// never converged) still reported info.isBootstrapped(C)=true, masking the stall from
// any readiness gate keyed on it. Keying on the SAME sb.Bootstrapped signal the health
// check (m.Nets.Bootstrapping) already uses makes info.isBootstrapped track real state,
// so a hands-off rolling upgrade's wait-for-healthy gate can trust it.
func (m *manager) IsBootstrapped(id ids.ID) bool {
m.chainsLock.Lock()
_, exists := m.chains[id]
@@ -2259,9 +2336,7 @@ func (m *manager) IsBootstrapped(id ids.ID) bool {
if !exists {
return false
}
// Bootstrapped chains start in NormalOp
return true
return m.Nets.IsChainBootstrapped(id)
}
func (m *manager) GetChains() []ChainInfo {
@@ -2271,10 +2346,12 @@ func (m *manager) GetChains() []ChainInfo {
result := make([]ChainInfo, 0, len(m.chains))
for id, info := range m.chains {
result = append(result, ChainInfo{
ID: id,
Name: info.Name,
VMID: info.VMID,
Bootstrapped: true,
ID: id,
Name: info.Name,
VMID: info.VMID,
// Real per-chain convergence, not mere existence (same fix as IsBootstrapped):
// a tracked-but-still-syncing chain reports Bootstrapped=false.
Bootstrapped: m.Nets.IsChainBootstrapped(id),
})
}
return result
@@ -2723,13 +2800,21 @@ type blockHandler struct {
// tells monitorBootstrap's no-progress watchdog this is a deliberate WAIT for the quorum to
// return (the network cannot make progress without it), NOT a stall — so the watchdog does not
// force-STOP a node that is correctly failing safe and waiting, which (given the K8s probes only
// poll the always-green /ext/health/liveness) would otherwise be a permanent brick.
// poll the always-green /v1/health/liveness) would otherwise be a permanent brick.
bootstrapConnecting gatomic.Bool
bsActive gatomic.Bool // true while the bootstrap loop is driving
bsMu sync.Mutex // guards bsFrontierCh + bsAncestorCh
bsMu sync.Mutex // guards bsFrontierCh + bsAncestorCh + bsAheadBeacons
bsFrontierCh chan bsFrontierReply // weighted frontier replies for the current FrontierTip
bsAncestorCh map[uint32]chan [][]byte // requestID -> ancestors reply for the current Ancestors
bsRotor gatomic.Uint32 // round-robins the Ancestors peer sample (M1: no monopoly)
// bsAheadBeacons is the set of beacons whose accepted tip, reported in the most recent
// FrontierTip round, is a block this node does NOT hold — i.e. beacons genuinely AHEAD that
// therefore HAVE the ancestry the descent needs. sampleAncestorBeacons prefers them so a
// re-bootstrapping node asks GetAncestors of peers that can serve, never wasting the sample on
// peers still at genesis (the ava PeerTracker "ask a prover" bias, without a full tracker).
// Recorded in collectFrontierReplies; empty ⇒ sampleAncestorBeacons falls back to the full
// rotated set (identical to prior behavior). Guarded by bsMu.
bsAheadBeacons set.Set[ids.NodeID]
// vmReady transitions the VM to NORMAL OPERATION (vm.Ready → the EVM's
// onNormalOperationsStarted: block building, mempool gossip, validator dispatch).
@@ -2826,7 +2911,7 @@ type blockHandler struct {
// DOWN — never live at a stale height) and CONVERGES the instant the quorum returns. ≤0 ⇒
// UNLIMITED (retry until the quorum returns or shutdown) — the production default, because a
// node without a quorum must keep trying to rejoin and the K8s liveness probe does NOT restart
// it (all luxd probes poll the always-green /ext/health/liveness). A STRUCTURAL failure (deep
// it (all luxd probes poll the always-green /v1/health/liveness). A STRUCTURAL failure (deep
// gap → state-sync) is never retried regardless. Tests pin it to 1 to assert the single-attempt
// terminal fail-safe in isolation.
bootstrapMaxAttempts int
@@ -3094,14 +3179,45 @@ func (b *blockHandler) requestContext(ctx context.Context, nodeID ids.NodeID, bl
return
}
nodeSet := set.NewSet[ids.NodeID](1)
nodeSet.Add(nodeID)
// PEER SELECTION (defect #3). The consensus layer signals "I hold a VERIFIED cert
// for a block I don't track — fetch it" by passing ids.EmptyNodeID (topology.go:
// requestCatchup(cert.Position.BlockID, ids.EmptyNodeID)); picking a real peer is
// the node layer's job. The prior code blindly Add(EmptyNodeID) + Send, so
// GetAncestors went to ZERO peers (the "sentTo=0" spam on the frozen fleet) and the
// certified-but-untracked block was NEVER fetched — the node saw the cert, could not
// finalize, and never recovered. When nodeID is Empty, sample real connected peers
// that track this chain's network (the SAME selection pollFrontierOnce uses); a valid
// cert already gated this request, so asking any network peer is sound (the served
// gap is cert-verified on accept).
nodeSet := set.NewSet[ids.NodeID](frontierPollSample)
if nodeID == ids.EmptyNodeID {
for _, p := range b.net.PeerInfo(nil) {
if p.TrackedChains.Contains(b.networkID) {
nodeSet.Add(p.ID)
if nodeSet.Len() >= frontierPollSample {
break
}
}
}
} else {
nodeSet.Add(nodeID)
}
if nodeSet.Len() == 0 {
// No reachable peer to serve the block. Release the pending slot so a later tick
// (frontier poll → AcceptedFrontier, or a re-gossiped cert) can retry — otherwise
// the block stays pinned unrequestable until the TTL reap.
b.contextRequestMu.Lock()
delete(b.pendingContext, blockID)
b.contextRequestMu.Unlock()
return
}
sentTo := b.net.Send(msg, nodeSet, b.networkID, 0)
b.logger.Info("requested context for missing prerequisites",
log.Stringer("from", nodeID),
log.Stringer("blockID", blockID),
log.Uint32("requestID", requestID),
log.Int("asked", nodeSet.Len()),
log.Int("sentTo", sentTo.Len()))
}
@@ -3191,10 +3307,31 @@ func (b *blockHandler) GetContext(ctx context.Context, nodeID ids.NodeID, reques
log.Stringer("containerID", containerID),
log.Uint32("requestID", requestID))
// Collect context blocks (walk parent chain)
// Collect context blocks (walk parent chain).
//
// SIZE-CHUNKING (the heavy-DEX-block self-heal fix). The response is the Ancestors
// wire message, whose UNCOMPRESSED size the peer compressor refuses above the message
// cap (constants.DefaultMaxMessageSize; the zstd compressor bounds its input to prevent a
// decompression bomb). The old loop bounded ONLY by COUNT (maxContextBlocks=256), so under
// heavy DEX load 256 blocks summed to 3.4-5.7 MB > the 2 MB cap, msgCreator.Ancestors FAILED
// to build, and the behind validator got NOTHING — it could never resync and fell
// permanently behind (the benchmark-proven stall). We now ALSO bound by serialized size:
// stop before the accumulated payload would exceed the budget, but ALWAYS include at least
// one block so a behind node makes progress every round; the requester re-requests for the
// remaining gap (GetAncestors/context is already a multi-round, oldest-first fill). A single
// block that alone exceeds the budget is still served (best-effort) so the walk never
// deadlocks — the trust-tiered validator cap (peer layer) gives such a block the headroom to
// actually send; for a stranger it will be refused by the tight cap, which is correct.
var containers [][]byte
currentID := containerID
// Leave margin under the cap for the p2p envelope (chainID, requestID, per-container length
// prefixes, compression framing) so the assembled message stays comfortably below the limit.
const contextResponseMargin = 128 * 1024 // 128 KiB
byteBudget := constants.DefaultMaxMessageSize - contextResponseMargin
accumulated := 0
truncatedForSize := false
for i := 0; i < b.maxContextBlocks; i++ {
// First check pending blocks (for recently proposed but not yet accepted blocks)
// This is critical: when we propose a block and send PullQuery, other validators
@@ -3225,6 +3362,14 @@ func (b *blockHandler) GetContext(ctx context.Context, nodeID ids.NodeID, reques
certBytes, _ = b.engine.CertForBlock(blk.ID())
}
entry := encodeCatchupEntry(blk.Bytes(), certBytes)
// SIZE GATE: stop before exceeding the budget — but never drop the FIRST block, so a
// behind node always receives at least one block per request and cannot deadlock.
if len(containers) > 0 && accumulated+len(entry) > byteBudget {
truncatedForSize = true
break
}
accumulated += len(entry)
containers = append([][]byte{entry}, containers...)
// Get parent ID for next iteration
@@ -3260,6 +3405,8 @@ func (b *blockHandler) GetContext(ctx context.Context, nodeID ids.NodeID, reques
log.Stringer("to", nodeID),
log.Stringer("containerID", containerID),
log.Int("numBlocks", len(containers)),
log.Int("payloadBytes", accumulated),
log.Bool("truncatedForSize", truncatedForSize),
log.Int("sentTo", sentTo.Len()))
return nil
@@ -3397,15 +3544,28 @@ func (b *blockHandler) AcceptedFrontier(ctx context.Context, nodeID ids.NodeID,
if b.deliverBootstrapFrontier(nodeID, containerID) {
return nil
}
if _, err := b.vm.GetBlock(ctx, containerID); err == nil {
return nil // we already have the peer's tip — not behind
}
if b.engine != nil {
if blk, err := b.vm.GetBlock(ctx, containerID); err == nil {
// HAVE-BLOCK, LACK-FINALIZATION (defect #5). Holding the peer's tip block does NOT
// mean we are caught up. A verified-but-unfinalized block (we voted for it, but the
// α-of-K cert never reached us) leaves us behind on the CERT, not the block bytes.
// The prior check returned "not behind" on GetBlock success, so such a node NEVER
// fetched the missing cert and sat stuck at its unfinalized height forever (the exact
// "verified 288 but no cert" condition). Only "have the block AND it is finalized
// here" is truly not-behind; otherwise fall through to fetch the cert-carrying gap so
// AcceptCatchupBlock can finalize it on its verified cert (no re-vote).
if b.engine == nil {
return nil
}
if fin, ok := b.engine.FinalizedBlockAtHeight(blk.Height()); ok && fin == containerID {
return nil // have the block AND finalized it — truly not behind
}
// have the block but not finalized here → behind on the cert → fetch below
} else if b.engine != nil {
if _, found := b.engine.GetPendingBlock(containerID); found {
return nil // already tracked
return nil // already tracked (pending) — the live path is handling it
}
}
b.requestContext(ctx, nodeID, containerID) // behind → fetch the gap
b.requestContext(ctx, nodeID, containerID) // behind (missing block OR its cert) → fetch the gap
return nil
}
@@ -4105,6 +4265,11 @@ func (g *networkGossiper) BroadcastVote(chainID ids.ID, networkID ids.ID, blockI
return g.net.Gossip(msg, nil, g.networkID, -1, 0, 0).Len()
}
// v1.36 "Nova": BroadcastPrevote was DELETED — the round-scoped view-change (prevote/POL/lock)
// it fed no longer exists in the consensus engine (174af3c31). Nova sampling decides; the ⅔
// Quasar attestation (a plain accept-vote, gossiped via BroadcastVote) trails it. Keep the braid dead.
// GossipCert broadcasts an assembled α-of-K finality cert to ALL validators so
// followers finalize blockID on a verifiable proof (HandleIncomingCert), not a
// fast-follow guess. Best effort: the gossiping node's own finality is already
+8 -2
View File
@@ -123,8 +123,14 @@ func (m *manager) authorizeChainActivation(chainID ids.ID) (authorized bool, rea
// 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.
// bootstrapped on this node. IsBootstrapped now keys on REAL convergence
// (sb.Bootstrapped), not mere presence — safe here because X-Chain is a DAG
// chain marked bootstrapped SYNCHRONOUSLY inside createChain (Engine == nil
// path) before the sequential chain-creator dequeues any re-pushed gated chain,
// so IsBootstrapped(X) is already true when a gated chain re-runs. INVARIANT: if
// X-Chain is ever linearized into an engine chain (async Bootstrapped via
// monitorBootstrap), retryPendingGatedChains must be made to re-drain after X
// converges, else gated chains park forever. If not bootstrapped yet, defer.
if !m.IsBootstrapped(m.XChainID) {
return false, false
}
+24 -5
View File
@@ -9,10 +9,12 @@ import (
"github.com/stretchr/testify/require"
"github.com/luxfi/constants"
"github.com/luxfi/container/buffer"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/math/set"
"github.com/luxfi/node/nets"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/nftfx"
"github.com/luxfi/utxo/secp256k1fx"
@@ -163,8 +165,10 @@ func TestHoldsAuthorizationNFT(t *testing.T) {
// 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.
// installed as the X-Chain's tracked VM AND marked bootstrapped in its net so
// IsBootstrapped(XChainID) is true (the gate consults the X-Chain UTXO set, which
// is only valid once the X-Chain has finished initial sync — mere presence in
// m.chains is no longer sufficient) and xChainUTXOReader() resolves to it.
func newGateManager(
xChainID ids.ID,
stakingAddr ids.ShortID,
@@ -172,17 +176,29 @@ func newGateManager(
critical set.Set[ids.ID],
xChainVM xChainUTXOReader,
) *manager {
netsTracker, err := NewNets(ids.GenerateTestNodeID(), map[ids.ID]nets.Config{
constants.PrimaryNetworkID: {},
})
if err != nil {
panic(err)
}
m := &manager{
chains: make(map[ids.ID]*chainInfo),
gatedAttempts: make(map[ids.ID]int),
}
m.Log = log.NewNoOpLogger()
m.Nets = netsTracker
m.XChainID = xChainID
m.StakingXAddress = stakingAddr
m.ChainAuthorizations = authz
m.CriticalChains = critical
if xChainVM != nil {
m.chains[xChainID] = &chainInfo{Name: "X-Chain", VM: xChainVM}
// The X-Chain has finished initial sync (its UTXO set is valid) — the real
// precondition the gate depends on, now reflected in the net tracking.
sb, _ := netsTracker.GetOrCreate(constants.PrimaryNetworkID)
sb.AddChain(xChainID)
sb.Bootstrapped(xChainID)
}
return m
}
@@ -278,11 +294,14 @@ func TestAuthorizeChainActivation(t *testing.T) {
})
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.
// X-Chain is bootstrapped (in m.chains AND marked bootstrapped in its net, so
// 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{}{}}
sb, _ := m.Nets.GetOrCreate(constants.PrimaryNetworkID)
sb.AddChain(xChainID)
sb.Bootstrapped(xChainID)
authorized, ready := m.authorizeChainActivation(gatedChain)
require.True(t, ready)
require.False(t, authorized)
+63
View File
@@ -177,6 +177,69 @@ func TestIsBootstrapped(t *testing.T) {
require.False(m.IsBootstrapped(chainID))
}
// TestIsBootstrappedTracksRealConvergence is the regression guard for the
// premature-true masking bug: manager.IsBootstrapped must report true ONLY once the
// chain has ACTUALLY finished initial sync (its net marked it Bootstrapped), NOT the
// instant it is merely tracked (added to m.chains with its sync goroutine launched).
// Before the fix, a C-Chain stalled at genesis (head 0x0) reported
// info.isBootstrapped(C)=true, masking the stall from any readiness gate.
func TestIsBootstrappedTracksRealConvergence(t *testing.T) {
require := require.New(t)
chainConfigs := map[ids.ID]nets.Config{
constants.PrimaryNetworkID: {},
}
netsTracker, err := NewNets(ids.GenerateTestNodeID(), chainConfigs)
require.NoError(err)
config := &ManagerConfig{
Log: log.NewNoOpLogger(),
Metrics: metric.NewMultiGatherer(),
VMManager: vms.NewManager(),
ChainDataDir: t.TempDir(),
Nets: netsTracker,
}
m, err := New(config)
require.NoError(err)
mImpl := m.(*manager)
// A native chain validated by the primary network — the C-Chain shape.
chainID := ids.GenerateTestID()
// Simulate createChain's tracking: the chain EXISTS in m.chains and is registered
// as bootstrapping in its validation net — but has NOT converged (initial sync is
// still driving, e.g. stalled at genesis fetching ancestry).
mImpl.chainsLock.Lock()
mImpl.chains[chainID] = &chainInfo{Name: "C-Chain"}
mImpl.chainsLock.Unlock()
sb, _ := netsTracker.GetOrCreate(constants.PrimaryNetworkID)
require.True(sb.AddChain(chainID))
// THE FIX: exists-but-not-converged must be FALSE (was true — the masking bug).
require.False(m.IsBootstrapped(chainID),
"a tracked-but-still-syncing chain must not report bootstrapped")
for _, ci := range m.(*manager).GetChains() {
if ci.ID == chainID {
require.False(ci.Bootstrapped, "GetChains must not report a syncing chain bootstrapped")
}
}
// Initial sync reaches the frontier → monitorBootstrap calls sb.Bootstrapped.
sb.Bootstrapped(chainID)
// Now — and only now — it reports bootstrapped (head advanced to frontier, VM live).
require.True(m.IsBootstrapped(chainID),
"a converged chain must report bootstrapped")
found := false
for _, ci := range m.(*manager).GetChains() {
if ci.ID == chainID {
found = true
require.True(ci.Bootstrapped, "GetChains must report a converged chain bootstrapped")
}
}
require.True(found)
}
// TestToEngineChannelFlow verifies the toEngine channel notification flow
// This tests the goroutine that reads from toEngine and triggers block building
func TestToEngineChannelFlow(t *testing.T) {
+15 -1
View File
@@ -89,7 +89,7 @@ func selectConsensusParams(sybilProtection bool, networkID uint32) consensusconf
// shouldWrapInProposerVM decides whether a linear chain.ChainVM is wrapped in
// proposervm to enforce single-proposer-per-height block production (the
// Snowman++ window). It is the SINGLE policy gate (the manager calls it once);
// proposer window). It is the SINGLE policy gate (the manager calls it once);
// keeping it a pure function makes the policy unit-testable without standing up
// a whole chain. All three conditions must hold:
//
@@ -267,6 +267,16 @@ func (s *validatorStakeSource) TotalStake(height uint64) uint64 {
return total
}
// ValidatorCount implements consensuschain.StakeSource. The number of DISTINCT
// validators in the set IN FORCE AT height — the round-scoped view-change's BFT
// committee size (it sizes its POL/precommit quorum to bftAlpha over this count,
// NOT the oversized sample K). Read from the SAME height-indexed set as
// Weight/TotalStake so every node computes the identical committee and the
// count-quorum matches the ⅔-by-stake set exactly.
func (s *validatorStakeSource) ValidatorCount(height uint64) int {
return len(validatorSetAtHeight(s.state, s.networkID, height))
}
var _ consensuschain.StakeSource = (*validatorStakeSource)(nil)
// --- validator-set-root source (MEDIUM: epoch binding) -----------------------
@@ -364,11 +374,15 @@ func hashValidatorSet(set map[ids.NodeID]*validators.GetValidatorOutput) ids.ID
//
// kind 1 = signed vote (payload = engine encodeSignedVote: nodeID+sig)
// kind 2 = finality cert (payload = engine cert MarshalBinary)
// (round-scoped view-change prevotes are engine-INTERNAL since consensus v1.36 —
// the node no longer frames or routes a prevote kind)
var quorumGossipMagic = [4]byte{'L', 'X', 'Q', 0x01}
const (
quorumKindVote byte = 1
quorumKindCert byte = 2
// kind 3 (prevote) was DELETED with the v1.36 view-change rip-out (174af3c31); Nova sampling
// decides and the ⅔ Quasar attestation rides quorumKindVote. Do not reuse 3 — keep the braid dead.
)
// ErrNotQuorumGossip signals a payload is not a quorum envelope (so the caller
+1 -1
View File
@@ -150,7 +150,7 @@ func (r *ChainHandlerRegistrar) ValidateEndpoint(
}
// Build the full URL
fullURL := fmt.Sprintf("/ext/%s%s", info.Base, endpoint)
fullURL := fmt.Sprintf("/v1/%s%s", info.Base, endpoint)
r.log.Info("Validating endpoint",
log.Stringer("chainID", chainID),
+7 -7
View File
@@ -68,20 +68,20 @@ func (d *DebugTool) DiagnoseEndpoint(chainID ids.ID, alias string) *DiagnosticRe
// getURLPatterns returns all possible URL patterns to test.
func (d *DebugTool) getURLPatterns(chainID ids.ID, alias string) []string {
patterns := []string{
fmt.Sprintf("%s/ext/bc/%s/rpc", d.baseURL, chainID.String()),
fmt.Sprintf("%s/ext/bc/%s/ws", d.baseURL, chainID.String()),
fmt.Sprintf("%s/ext/bc/%s", d.baseURL, chainID.String()),
fmt.Sprintf("%s/v1/bc/%s/rpc", d.baseURL, chainID.String()),
fmt.Sprintf("%s/v1/bc/%s/ws", d.baseURL, chainID.String()),
fmt.Sprintf("%s/v1/bc/%s", d.baseURL, chainID.String()),
}
if alias != "" && alias != chainID.String() {
patterns = append(patterns,
fmt.Sprintf("%s/ext/bc/%s/rpc", d.baseURL, alias),
fmt.Sprintf("%s/ext/bc/%s/ws", d.baseURL, alias),
fmt.Sprintf("%s/ext/bc/%s", d.baseURL, alias),
fmt.Sprintf("%s/v1/bc/%s/rpc", d.baseURL, alias),
fmt.Sprintf("%s/v1/bc/%s/ws", d.baseURL, alias),
fmt.Sprintf("%s/v1/bc/%s", d.baseURL, alias),
)
}
// Also test without /ext prefix (some setups might differ)
// Also test without /v1 prefix (some setups might differ)
patterns = append(patterns,
fmt.Sprintf("%s/bc/%s/rpc", d.baseURL, chainID.String()),
)
+2 -2
View File
@@ -111,7 +111,7 @@ func (m *HandlerManager) RegisterChainHandlers(
log.Err(err))
} else {
m.log.Info("Handler registered successfully",
log.String("route", fmt.Sprintf("/ext/%s%s", base, endpoint)),
log.String("route", fmt.Sprintf("/v1/%s%s", base, endpoint)),
log.Stringer("chainID", chainID))
}
}
@@ -187,7 +187,7 @@ func (m *HandlerManager) getFullRoutes(bases []string, endpoints []string) []str
routes := []string{}
for _, base := range bases {
for _, endpoint := range endpoints {
routes = append(routes, fmt.Sprintf("/ext/%s%s", base, endpoint))
routes = append(routes, fmt.Sprintf("/v1/%s%s", base, endpoint))
}
}
return routes
+2 -2
View File
@@ -80,9 +80,9 @@ func IntegrationExample(
fmt.Printf("\n✅ Chain %s RPC endpoints ready:\n", chainID)
for _, endpoint := range info.Endpoints {
if info.ChainAlias != "" {
fmt.Printf(" %s/ext/bc/%s%s\n", baseURL, info.ChainAlias, endpoint)
fmt.Printf(" %s/v1/bc/%s%s\n", baseURL, info.ChainAlias, endpoint)
}
fmt.Printf(" %s/ext/bc/%s%s\n", baseURL, chainID, endpoint)
fmt.Printf(" %s/v1/bc/%s%s\n", baseURL, chainID, endpoint)
}
fmt.Println()
}
+1 -1
View File
@@ -19,7 +19,7 @@ services:
LUXD_CONSENSUS_QUORUM_SIZE: "1"
LUXD_LOG_LEVEL: "info"
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:9630/ext/health"]
test: ["CMD", "curl", "-sf", "http://localhost:9630/v1/health"]
interval: 30s
timeout: 10s
retries: 5
+5 -6
View File
@@ -24,7 +24,6 @@ import (
"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"
@@ -536,8 +535,8 @@ func TestGetNetConfigsFromFlags(t *testing.T) {
"2Ctt6eGAeo4MLqTmGa7AdRecuVMPGWEX9wSsCLBYrLhX4a394i": {
"consensusParameters": {
"k": 30,
"alphaPreference": 16,
"alphaConfidence": 20
"alphaPreference": 20,
"alphaConfidence": 25
},
"validatorOnly": true
}
@@ -547,8 +546,8 @@ func TestGetNetConfigsFromFlags(t *testing.T) {
config, ok := given[id]
require.True(ok)
require.True(config.ValidatorOnly)
require.Equal(16, config.ConsensusParameters.AlphaPreference)
require.Equal(20, config.ConsensusParameters.AlphaConfidence)
require.Equal(20, config.ConsensusParameters.AlphaPreference)
require.Equal(25, config.ConsensusParameters.AlphaConfidence)
require.Equal(30, config.ConsensusParameters.K)
// must still respect defaults (MainnetParameters.MaxOutstandingItems = 1024)
require.Equal(1024, config.ConsensusParameters.MaxOutstandingItems)
@@ -733,7 +732,7 @@ func TestResolveUTXOAssetID_POnlyFallback(t *testing.T) {
require := require.New(t)
pOnly := &pchaingenesis.Genesis{Chains: nil}
pOnlyBytes, err := pchaingenesis.Codec.Marshal(pchaintxs.CodecVersion, pOnly)
pOnlyBytes, err := pOnly.Bytes()
require.NoError(err)
gotID, err := resolveUTXOAssetID(42, pOnlyBytes)
+5 -5
View File
@@ -133,12 +133,12 @@ deploy:
### Prometheus Metrics
The node exposes metrics at `http://localhost:9630/ext/metrics`
The node exposes metrics at `http://localhost:9630/v1/metrics`
### Health Checks
- Liveness: `http://localhost:9630/ext/health`
- Readiness: `http://localhost:9630/ext/info`
- Liveness: `http://localhost:9630/v1/health`
- Readiness: `http://localhost:9630/v1/info`
### Grafana Dashboard
@@ -178,12 +178,12 @@ docker exec -it luxd /bin/bash
# Check if bootstrapped
curl -X POST -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"info.isBootstrapped","params":{"chain":"C"}}' \
http://localhost:9630/ext/info
http://localhost:9630/v1/info
# Get block number
curl -X POST -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}' \
http://localhost:9630/ext/bc/C/rpc
http://localhost:9630/v1/bc/C/rpc
```
## CI/CD
+4 -4
View File
@@ -287,10 +287,10 @@ echo "📝 Starting with command:"
echo " $CMD"
echo ""
echo "📡 API Endpoints:"
echo " - JSON-RPC: http://$HTTP_HOST:$HTTP_PORT/ext/bc/C/rpc"
echo " - WebSocket: ws://$HTTP_HOST:$HTTP_PORT/ext/bc/C/ws"
echo " - Health: http://$HTTP_HOST:$HTTP_PORT/ext/health"
echo " - Info: http://$HTTP_HOST:$HTTP_PORT/ext/info"
echo " - JSON-RPC: http://$HTTP_HOST:$HTTP_PORT/v1/bc/C/rpc"
echo " - WebSocket: ws://$HTTP_HOST:$HTTP_PORT/v1/bc/C/ws"
echo " - Health: http://$HTTP_HOST:$HTTP_PORT/v1/health"
echo " - Info: http://$HTTP_HOST:$HTTP_PORT/v1/info"
echo ""
# Execute
+1 -1
View File
@@ -346,7 +346,7 @@ export default function HomePage() {
</p>
<pre className="mt-4 overflow-x-auto rounded-lg bg-black/50 p-4">
<code className="text-sm text-green-400">
curl -X POST --data &#39;&#123;&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;method&quot;:&quot;health.health&quot;,&quot;id&quot;:1&#125;&#39; \{"\n"} -H &#39;content-type:application/json&#39; \{"\n"} 127.0.0.1:9650/ext/health
curl -X POST --data &#39;&#123;&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;method&quot;:&quot;health.health&quot;,&quot;id&quot;:1&#125;&#39; \{"\n"} -H &#39;content-type:application/json&#39; \{"\n"} 127.0.0.1:9650/v1/health
</code>
</pre>
</div>
+20 -20
View File
@@ -31,7 +31,7 @@ Or in configuration:
## Endpoint
```
http://localhost:9630/ext/admin
http://localhost:9630/v1/admin
```
## Methods
@@ -54,7 +54,7 @@ curl -X POST --data '{
"chain":"2S53R2ub94CV5vmSRAjqPYmRxvuiFunCb1gN2CAw3DQBfPWghX"
},
"id":1
}' -H 'content-type:application/json;' http://localhost:9630/ext/admin
}' -H 'content-type:application/json;' http://localhost:9630/v1/admin
```
---
@@ -85,7 +85,7 @@ curl -X POST --data '{
"chain":"2S53R2ub94CV5vmSRAjqPYmRxvuiFunCb1gN2CAw3DQBfPWghX"
},
"id":1
}' -H 'content-type:application/json;' http://localhost:9630/ext/admin
}' -H 'content-type:application/json;' http://localhost:9630/v1/admin
```
**Response:**
@@ -114,7 +114,7 @@ curl -X POST --data '{
"method":"admin.getLogLevel",
"params":{},
"id":1
}' -H 'content-type:application/json;' http://localhost:9630/ext/admin
}' -H 'content-type:application/json;' http://localhost:9630/v1/admin
```
**Response:**
@@ -146,7 +146,7 @@ curl -X POST --data '{
"logLevel":"debug"
},
"id":1
}' -H 'content-type:application/json;' http://localhost:9630/ext/admin
}' -H 'content-type:application/json;' http://localhost:9630/v1/admin
```
---
@@ -181,7 +181,7 @@ curl -X POST --data '{
"method":"admin.loadVMs",
"params":{},
"id":1
}' -H 'content-type:application/json;' http://localhost:9630/ext/admin
}' -H 'content-type:application/json;' http://localhost:9630/v1/admin
```
**Response:**
@@ -214,7 +214,7 @@ curl -X POST --data '{
"method":"admin.startCPUProfiler",
"params":{},
"id":1
}' -H 'content-type:application/json;' http://localhost:9630/ext/admin
}' -H 'content-type:application/json;' http://localhost:9630/v1/admin
```
---
@@ -232,7 +232,7 @@ curl -X POST --data '{
"method":"admin.stopCPUProfiler",
"params":{},
"id":1
}' -H 'content-type:application/json;' http://localhost:9630/ext/admin
}' -H 'content-type:application/json;' http://localhost:9630/v1/admin
```
---
@@ -250,7 +250,7 @@ curl -X POST --data '{
"method":"admin.memoryProfile",
"params":{},
"id":1
}' -H 'content-type:application/json;' http://localhost:9630/ext/admin
}' -H 'content-type:application/json;' http://localhost:9630/v1/admin
```
---
@@ -276,7 +276,7 @@ curl -X POST --data '{
"method":"admin.getConfig",
"params":{},
"id":1
}' -H 'content-type:application/json;' http://localhost:9630/ext/admin
}' -H 'content-type:application/json;' http://localhost:9630/v1/admin
```
**Response:**
@@ -311,7 +311,7 @@ curl -X POST --data '{
"method":"admin.shutdown",
"params":{},
"id":1
}' -H 'content-type:application/json;' http://localhost:9630/ext/admin
}' -H 'content-type:application/json;' http://localhost:9630/v1/admin
```
---
@@ -330,7 +330,7 @@ curl -X POST --data '{
"method":"admin.db.commit",
"params":{},
"id":1
}' -H 'content-type:application/json;' http://localhost:9630/ext/admin
}' -H 'content-type:application/json;' http://localhost:9630/v1/admin
```
## Profiling and Debugging
@@ -344,7 +344,7 @@ curl -X POST --data '{
"method":"admin.startCPUProfiler",
"params":{},
"id":1
}' http://localhost:9630/ext/admin
}' http://localhost:9630/v1/admin
# Let it run for 30 seconds
sleep 30
@@ -355,7 +355,7 @@ curl -X POST --data '{
"method":"admin.stopCPUProfiler",
"params":{},
"id":2
}' http://localhost:9630/ext/admin
}' http://localhost:9630/v1/admin
# Analyze profile
go tool pprof cpu.prof
@@ -370,7 +370,7 @@ curl -X POST --data '{
"method":"admin.memoryProfile",
"params":{},
"id":1
}' http://localhost:9630/ext/admin
}' http://localhost:9630/v1/admin
# Analyze
go tool pprof mem.prof
@@ -401,7 +401,7 @@ set_log_level() {
\"method\":\"admin.setLogLevel\",
\"params\":{\"logLevel\":\"$LEVEL\"},
\"id\":1
}" http://localhost:9630/ext/admin
}" http://localhost:9630/v1/admin
}
# Increase verbosity for debugging
@@ -476,7 +476,7 @@ Enable audit logging for admin operations:
# auto_restart.sh
check_health() {
curl -s http://localhost:9630/ext/health | jq -r '.healthy'
curl -s http://localhost:9630/v1/health | jq -r '.healthy'
}
restart_node() {
@@ -488,7 +488,7 @@ restart_node() {
"method":"admin.shutdown",
"params":{},
"id":1
}' http://localhost:9630/ext/admin
}' http://localhost:9630/v1/admin
sleep 10
@@ -514,7 +514,7 @@ CURRENT=$(curl -s -X POST --data '{
"method":"admin.getConfig",
"params":{},
"id":1
}' http://localhost:9630/ext/admin)
}' http://localhost:9630/v1/admin)
echo "Current configuration:"
echo $CURRENT | jq .
@@ -525,7 +525,7 @@ curl -X POST --data '{
"method":"admin.setLogLevel",
"params":{"logLevel":"debug"},
"id":2
}' http://localhost:9630/ext/admin
}' http://localhost:9630/v1/admin
```
## Troubleshooting
+12 -12
View File
@@ -10,7 +10,7 @@ The Health API provides endpoints to monitor the health and readiness of your Lu
## Endpoint
```
http://localhost:9630/ext/health
http://localhost:9630/v1/health
```
## Health Check Types
@@ -20,7 +20,7 @@ http://localhost:9630/ext/health
Get the overall health status of the node:
```bash
curl http://localhost:9630/ext/health
curl http://localhost:9630/v1/health
```
**Response:**
@@ -61,7 +61,7 @@ curl http://localhost:9630/ext/health
Check if the node is ready to serve requests:
```bash
curl http://localhost:9630/ext/health/readiness
curl http://localhost:9630/v1/health/readiness
```
Returns:
@@ -73,7 +73,7 @@ Returns:
Check if the node is alive and running:
```bash
curl http://localhost:9630/ext/health/liveness
curl http://localhost:9630/v1/health/liveness
```
Returns:
@@ -92,7 +92,7 @@ curl -X POST --data '{
"id": 1,
"method": "health.health",
"params": {}
}' -H 'content-type:application/json;' http://localhost:9630/ext/health
}' -H 'content-type:application/json;' http://localhost:9630/v1/health
```
**Response:**
@@ -175,7 +175,7 @@ Configure per-chain health parameters:
Export health metrics in Prometheus format:
```bash
curl http://localhost:9630/ext/metrics | grep health
curl http://localhost:9630/v1/metrics | grep health
```
Metrics:
@@ -208,13 +208,13 @@ spec:
image: luxfi/node:latest
livenessProbe:
httpGet:
path: /ext/health/liveness
path: /v1/health/liveness
port: 9630
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ext/health/readiness
path: /v1/health/readiness
port: 9630
initialDelaySeconds: 60
periodSeconds: 5
@@ -229,7 +229,7 @@ spec:
# health_monitor.sh
while true; do
HEALTH=$(curl -s http://localhost:9630/ext/health | jq -r '.healthy')
HEALTH=$(curl -s http://localhost:9630/v1/health | jq -r '.healthy')
if [ "$HEALTH" != "true" ]; then
echo "ALERT: Node unhealthy at $(date)"
@@ -248,7 +248,7 @@ done
# health_analysis.sh
# Get detailed health
RESPONSE=$(curl -s http://localhost:9630/ext/health)
RESPONSE=$(curl -s http://localhost:9630/v1/health)
# Parse each chain
for CHAIN in P X C Q; do
@@ -271,7 +271,7 @@ done
```
backend lux_nodes
option httpchk GET /ext/health
option httpchk GET /v1/health
http-check expect status 200
server node1 192.168.1.10:9630 check
@@ -289,7 +289,7 @@ upstream lux_nodes {
}
location /health_check {
proxy_pass http://lux_nodes/ext/health;
proxy_pass http://lux_nodes/v1/health;
proxy_connect_timeout 1s;
proxy_read_timeout 1s;
}
+23 -23
View File
@@ -10,7 +10,7 @@ The Info API provides general information about the node and network status. Thi
## Endpoint
```
http://localhost:9630/ext/info
http://localhost:9630/v1/info
```
## Format
@@ -23,7 +23,7 @@ curl -X POST --data '{
"method": "info.<method>",
"params": {...},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
```
## Methods
@@ -41,7 +41,7 @@ curl -X POST --data '{
"method": "info.getNodeVersion",
"params": {},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
```
**Example Response:**
@@ -78,7 +78,7 @@ curl -X POST --data '{
"method": "info.getNodeID",
"params": {},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
```
**Example Response:**
@@ -111,7 +111,7 @@ curl -X POST --data '{
"method": "info.getNodeIP",
"params": {},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
```
**Example Response:**
@@ -140,7 +140,7 @@ curl -X POST --data '{
"method": "info.getNetworkID",
"params": {},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
```
**Example Response:**
@@ -175,7 +175,7 @@ curl -X POST --data '{
"method": "info.getNetworkName",
"params": {},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
```
**Example Response:**
@@ -207,7 +207,7 @@ curl -X POST --data '{
"alias": "P"
},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
```
**Example Response:**
@@ -239,7 +239,7 @@ curl -X POST --data '{
"chain": "P"
},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
```
**Example Response:**
@@ -269,7 +269,7 @@ curl -X POST --data '{
"method": "info.peers",
"params": {},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
```
**Example Response:**
@@ -313,7 +313,7 @@ curl -X POST --data '{
"method": "info.getTxFee",
"params": {},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
```
**Example Response:**
@@ -348,7 +348,7 @@ curl -X POST --data '{
"method": "info.uptime",
"params": {},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
```
**Example Response (Validator):**
@@ -390,7 +390,7 @@ curl -X POST --data '{
"method": "info.getVMs",
"params": {},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
```
**Example Response:**
@@ -424,7 +424,7 @@ curl -X POST --data '{
"method": "info.getUpgrades",
"params": {},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
```
**Example Response:**
@@ -468,7 +468,7 @@ VERSION=$(curl -s -X POST --data '{
"method": "info.getNodeVersion",
"params": {},
"id": 1
}' -H 'content-type:application/json;' $NODE_URL/ext/info | jq -r '.result.version')
}' -H 'content-type:application/json;' $NODE_URL/v1/info | jq -r '.result.version')
echo "Version: $VERSION"
@@ -478,7 +478,7 @@ NODE_ID=$(curl -s -X POST --data '{
"method": "info.getNodeID",
"params": {},
"id": 1
}' -H 'content-type:application/json;' $NODE_URL/ext/info | jq -r '.result.nodeID')
}' -H 'content-type:application/json;' $NODE_URL/v1/info | jq -r '.result.nodeID')
echo "Node ID: $NODE_ID"
@@ -488,7 +488,7 @@ PEERS=$(curl -s -X POST --data '{
"method": "info.peers",
"params": {},
"id": 1
}' -H 'content-type:application/json;' $NODE_URL/ext/info | jq -r '.result.numPeers')
}' -H 'content-type:application/json;' $NODE_URL/v1/info | jq -r '.result.numPeers')
echo "Connected Peers: $PEERS"
@@ -499,7 +499,7 @@ for CHAIN in P X C Q; do
\"method\": \"info.isBootstrapped\",
\"params\": {\"chain\": \"$CHAIN\"},
\"id\": 1
}" -H 'content-type:application/json;' $NODE_URL/ext/info | jq -r '.result.isBootstrapped')
}" -H 'content-type:application/json;' $NODE_URL/v1/info | jq -r '.result.isBootstrapped')
echo "$CHAIN-Chain Bootstrapped: $STATUS"
done
@@ -510,7 +510,7 @@ UPTIME=$(curl -s -X POST --data '{
"method": "info.uptime",
"params": {},
"id": 1
}' -H 'content-type:application/json;' $NODE_URL/ext/info 2>/dev/null | jq -r '.result.rewardingStakePercentage' 2>/dev/null)
}' -H 'content-type:application/json;' $NODE_URL/v1/info 2>/dev/null | jq -r '.result.rewardingStakePercentage' 2>/dev/null)
if [ "$UPTIME" != "null" ] && [ -n "$UPTIME" ]; then
echo "Validator Uptime: $UPTIME%"
@@ -531,7 +531,7 @@ PEERS=$(curl -s -X POST --data '{
"method": "info.peers",
"params": {},
"id": 1
}' -H 'content-type:application/json;' $NODE_URL/ext/info | jq -r '.result.peers[]')
}' -H 'content-type:application/json;' $NODE_URL/v1/info | jq -r '.result.peers[]')
echo "=== Peer Connection Quality ==="
echo "Node ID | Latency (ms) | Uptime % | Version"
@@ -560,7 +560,7 @@ while true; do
\"method\": \"info.isBootstrapped\",
\"params\": {\"chain\": \"$CHAIN\"},
\"id\": 1
}" -H 'content-type:application/json;' $NODE_URL/ext/info | jq -r '.result.isBootstrapped')
}" -H 'content-type:application/json;' $NODE_URL/v1/info | jq -r '.result.isBootstrapped')
if [ "$STATUS" == "true" ]; then
echo "✅ $CHAIN-Chain: Bootstrapped"
@@ -575,7 +575,7 @@ while true; do
"method": "info.peers",
"params": {},
"id": 1
}' -H 'content-type:application/json;' $NODE_URL/ext/info | jq -r '.result.numPeers')
}' -H 'content-type:application/json;' $NODE_URL/v1/info | jq -r '.result.numPeers')
echo ""
echo "Connected Peers: $PEERS"
@@ -588,7 +588,7 @@ while true; do
\"method\": \"info.isBootstrapped\",
\"params\": {\"chain\": \"$CHAIN\"},
\"id\": 1
}" -H 'content-type:application/json;' $NODE_URL/ext/info | jq -r '.result.isBootstrapped')
}" -H 'content-type:application/json;' $NODE_URL/v1/info | jq -r '.result.isBootstrapped')
if [ "$STATUS" != "true" ]; then
ALL_BOOTSTRAPPED=false
+23 -23
View File
@@ -10,7 +10,7 @@ The Platform Chain (P-Chain) is responsible for staking, validators, and chain m
## Endpoint
```
http://localhost:9630/ext/bc/P
http://localhost:9630/v1/bc/P
```
## Format
@@ -23,7 +23,7 @@ curl -X POST --data '{
"method": "platform.<method>",
"params": {...},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
```
## Methods
@@ -41,7 +41,7 @@ curl -X POST --data '{
"method": "platform.getHeight",
"params": {},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
```
**Example Response:**
@@ -73,7 +73,7 @@ curl -X POST --data '{
"addresses": ["P-lux1q8tgunsf7sxpl2qp9fz0xjs36kstm7vdjvpzcc"]
},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
```
**Example Response:**
@@ -114,7 +114,7 @@ curl -X POST --data '{
"limit": 100
},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
```
---
@@ -134,7 +134,7 @@ curl -X POST --data '{
"method": "platform.getCurrentValidators",
"params": {},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
```
**Example Response:**
@@ -190,7 +190,7 @@ curl -X POST --data '{
"method": "platform.getPendingValidators",
"params": {},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
```
---
@@ -212,7 +212,7 @@ curl -X POST --data '{
"height": 365000
},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
```
---
@@ -233,7 +233,7 @@ curl -X POST --data '{
"height": 365000
},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
```
---
@@ -255,7 +255,7 @@ curl -X POST --data '{
"addresses": ["P-lux1q8tgunsf7sxpl2qp9fz0xjs36kstm7vdjvpzcc"]
},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
```
---
@@ -274,7 +274,7 @@ curl -X POST --data '{
"method": "platform.getMinStake",
"params": {},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
```
**Example Response:**
@@ -305,7 +305,7 @@ curl -X POST --data '{
"method": "platform.getTotalStake",
"params": {},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
```
---
@@ -327,7 +327,7 @@ curl -X POST --data '{
"txID": "2nmH8LithVbdjaXsxVQCQfXtzN9hBbmebrsaEYnLM9T32Uy3Y5"
},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
```
---
@@ -345,7 +345,7 @@ curl -X POST --data '{
"method": "platform.getTimestamp",
"params": {},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
```
---
@@ -363,7 +363,7 @@ curl -X POST --data '{
"method": "platform.getBlockchains",
"params": {},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
```
**Example Response:**
@@ -409,7 +409,7 @@ curl -X POST --data '{
"blockID": "vXSY7FK7NR65Y8BrJDKQH4a6vBdJuqAMvVzWj3Zxcap5J4ZE3"
},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
```
---
@@ -431,7 +431,7 @@ curl -X POST --data '{
"txID": "2nmH8LithVbdjaXsxVQCQfXtzN9hBbmebrsaEYnLM9T32Uy3Y5"
},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
```
---
@@ -452,7 +452,7 @@ curl -X POST --data '{
"txID": "2nmH8LithVbdjaXsxVQCQfXtzN9hBbmebrsaEYnLM9T32Uy3Y5"
},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
```
**Example Response:**
@@ -488,7 +488,7 @@ curl -X POST --data '{
"method": "platform.getCurrentSupply",
"params": {},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
```
---
@@ -507,7 +507,7 @@ curl -X POST --data '{
"method": "platform.getChains",
"params": {},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
```
---
@@ -544,7 +544,7 @@ curl -X POST --data '{
"password": "mypassword"
},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
```
---
@@ -694,7 +694,7 @@ curl -s -X POST --data "{
\"nodeIDs\": [\"$NODE_ID\"]
},
\"id\": 1
}" -H 'content-type:application/json;' http://localhost:9630/ext/bc/P | jq '.result.validators[0]'
}" -H 'content-type:application/json;' http://localhost:9630/v1/bc/P | jq '.result.validators[0]'
```
### Monitor Staking Rewards
@@ -711,7 +711,7 @@ STAKE=$(curl -s -X POST --data "{
\"addresses\": [\"$ADDRESS\"]
},
\"id\": 1
}" -H 'content-type:application/json;' http://localhost:9630/ext/bc/P)
}" -H 'content-type:application/json;' http://localhost:9630/v1/bc/P)
echo "Current stake: $(echo $STAKE | jq -r '.result.staked')"
echo "Stakeable: $(echo $STAKE | jq -r '.result.stakeable')"
@@ -210,14 +210,14 @@ curl -X POST --data '{
"jsonrpc":"2.0",
"id": 1,
"method": "health.health"
}' -H 'content-type:application/json;' http://localhost:9630/ext/health
}' -H 'content-type:application/json;' http://localhost:9630/v1/health
# Get node info
curl -X POST --data '{
"jsonrpc":"2.0",
"id": 1,
"method": "info.getNodeVersion"
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
# Check bootstrap status
curl -X POST --data '{
@@ -227,7 +227,7 @@ curl -X POST --data '{
"params": {
"chain": "P"
}
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
```
## Troubleshooting
@@ -308,7 +308,7 @@ curl -X POST --data '{
"jsonrpc":"2.0",
"id": 1,
"method": "health.health"
}' -H 'content-type:application/json;' http://localhost:9630/ext/health
}' -H 'content-type:application/json;' http://localhost:9630/v1/health
```
### Bootstrap Status
@@ -320,7 +320,7 @@ curl -X POST --data '{
"id": 1,
"method": "info.isBootstrapped",
"params": {"chain": "P"}
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
```
### Metrics
@@ -328,7 +328,7 @@ curl -X POST --data '{
Access Prometheus metrics:
```bash
curl http://localhost:9630/ext/metrics
curl http://localhost:9630/v1/metrics
```
Key metrics to monitor:
+17 -17
View File
@@ -58,7 +58,7 @@ curl -X POST --data '{
"jsonrpc":"2.0",
"id": 1,
"method": "platform.getHeight"
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
# Check bootstrap status for all chains
curl -X POST --data '{
@@ -66,7 +66,7 @@ curl -X POST --data '{
"id": 1,
"method": "info.isBootstrapped",
"params": {"chain": "P"}
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
```
### Configure Public IP
@@ -90,7 +90,7 @@ curl -X POST --data '{
"jsonrpc":"2.0",
"id": 1,
"method": "info.getNodeID"
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
```
Response:
@@ -155,7 +155,7 @@ curl -X POST --data '{
"password": "mypassword"
},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/keystore
}' -H 'content-type:application/json;' http://localhost:9630/v1/keystore
# Create P-Chain address
curl -X POST --data '{
@@ -166,7 +166,7 @@ curl -X POST --data '{
"password": "mypassword"
},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
```
### Transfer LUX to P-Chain
@@ -185,7 +185,7 @@ curl -X POST --data '{
"amount": 2000000000000
},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/X
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/X
# Import to P-Chain
curl -X POST --data '{
@@ -197,7 +197,7 @@ curl -X POST --data '{
"sourceChain": "X"
},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
```
## Step 4: Add as Validator
@@ -221,7 +221,7 @@ curl -X POST --data '{
"delegationFeeRate": 10
},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
```
Parameters:
@@ -243,7 +243,7 @@ curl -X POST --data '{
"method": "platform.getPendingValidators",
"params": {},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
# Get current validators
curl -X POST --data '{
@@ -251,7 +251,7 @@ curl -X POST --data '{
"method": "platform.getCurrentValidators",
"params": {},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
```
## Step 5: Monitor Your Validator
@@ -268,7 +268,7 @@ curl -X POST --data '{
"nodeID": "NodeID-5KqnQfaFQxY9rsFBAa68377qWSherYLQ7"
},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
```
### Monitor Performance Metrics
@@ -277,10 +277,10 @@ Key metrics to track:
```bash
# Node health
curl http://localhost:9630/ext/health
curl http://localhost:9630/v1/health
# Prometheus metrics
curl http://localhost:9630/ext/metrics | grep -E "uptime|stake|validator"
curl http://localhost:9630/v1/metrics | grep -E "uptime|stake|validator"
```
Important metrics:
@@ -308,7 +308,7 @@ NODE_URL="http://localhost:9630"
WEBHOOK_URL="your-webhook-url"
# Check if node is responsive
if ! curl -s "$NODE_URL/ext/health" > /dev/null; then
if ! curl -s "$NODE_URL/v1/health" > /dev/null; then
curl -X POST "$WEBHOOK_URL" -d '{"text":"ALERT: Node is not responding!"}'
fi
@@ -318,7 +318,7 @@ UPTIME=$(curl -s -X POST --data '{
"method":"platform.getValidator",
"params":{"nodeID":"NodeID-xxx"},
"id":1
}' "$NODE_URL/ext/bc/P" | jq -r '.result.uptime')
}' "$NODE_URL/v1/bc/P" | jq -r '.result.uptime')
if [ "$UPTIME" -lt "80" ]; then
curl -X POST "$WEBHOOK_URL" -d "{\"text\":\"WARNING: Uptime is $UPTIME%\"}"
@@ -349,7 +349,7 @@ curl -X POST --data '{
"nodeID": "NodeID-5KqnQfaFQxY9rsFBAa68377qWSherYLQ7"
},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
```
## Chain Validation
@@ -373,7 +373,7 @@ curl -X POST --data '{
"weight": 1000
},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
```
### Chain Requirements
+7 -7
View File
@@ -104,14 +104,14 @@ curl -X POST --data '{
"jsonrpc":"2.0",
"id": 1,
"method": "info.getNetworkInfo"
}' -H 'content-type:application/json;' http://localhost:9650/ext/info
}' -H 'content-type:application/json;' http://localhost:9650/v1/info
# Get node ID
curl -X POST --data '{
"jsonrpc":"2.0",
"id": 1,
"method": "info.getNodeID"
}' -H 'content-type:application/json;' http://localhost:9650/ext/info
}' -H 'content-type:application/json;' http://localhost:9650/v1/info
```
## Chain Management
@@ -130,7 +130,7 @@ curl -X POST --data '{
"genesisData": "...",
"netID": "network-id"
}
}' -H 'content-type:application/json;' http://localhost:9650/ext/P
}' -H 'content-type:application/json;' http://localhost:9650/v1/P
```
### Validator Management
@@ -147,7 +147,7 @@ curl -X POST --data '{
"endTime": ...,
"stakeAmount": ...
}
}' -H 'content-type:application/json;' http://localhost:9650/ext/P
}' -H 'content-type:application/json;' http://localhost:9650/v1/P
```
## Configuration Reference
@@ -175,10 +175,10 @@ curl -X POST --data '{
```bash
# Prometheus metrics endpoint
curl http://localhost:9650/ext/metrics
curl http://localhost:9650/v1/metrics
# Health check
curl http://localhost:9650/ext/health
curl http://localhost:9650/v1/health
```
### Logs
@@ -193,7 +193,7 @@ curl -X POST --data '{
"id": 1,
"method": "admin.setLogLevel",
"params": {"logLevel": "debug"}
}' http://localhost:9650/ext/admin
}' http://localhost:9650/v1/admin
```
## Testing
@@ -595,10 +595,10 @@ curl -X POST --data '{
"jsonrpc":"2.0",
"method":"info.peers",
"id":1
}' http://localhost:9630/ext/info
}' http://localhost:9630/v1/info
# Check network metrics
curl http://localhost:9630/ext/metrics | grep network
curl http://localhost:9630/v1/metrics | grep network
# Monitor connections
netstat -an | grep 9631
+9 -9
View File
@@ -39,7 +39,7 @@ scrape_configs:
- job_name: 'lux-node'
static_configs:
- targets: ['localhost:9630']
metrics_path: '/ext/metrics'
metrics_path: '/v1/metrics'
- job_name: 'node-exporter'
static_configs:
@@ -56,7 +56,7 @@ alerting:
### Available Metrics
The Lux node exposes 400+ Prometheus metrics at `http://localhost:9630/ext/metrics`.
The Lux node exposes 400+ Prometheus metrics at `http://localhost:9630/v1/metrics`.
#### Key Metric Categories
@@ -419,11 +419,11 @@ groups:
```bash
# Basic health check
curl http://localhost:9630/ext/health
curl http://localhost:9630/v1/health
# Detailed health with readiness/liveness
curl http://localhost:9630/ext/health/readiness
curl http://localhost:9630/ext/health/liveness
curl http://localhost:9630/v1/health/readiness
curl http://localhost:9630/v1/health/liveness
```
### Custom Health Script
@@ -443,7 +443,7 @@ send_alert() {
}
# Check if node is responsive
if ! curl -s "$NODE_URL/ext/health" > /dev/null; then
if ! curl -s "$NODE_URL/v1/health" > /dev/null; then
send_alert "Node is not responding!"
exit 1
fi
@@ -455,7 +455,7 @@ for CHAIN in P X C Q; do
\"method\": \"info.isBootstrapped\",
\"params\": {\"chain\": \"$CHAIN\"},
\"id\": 1
}" -H 'content-type:application/json;' $NODE_URL/ext/info | jq -r '.result.isBootstrapped')
}" -H 'content-type:application/json;' $NODE_URL/v1/info | jq -r '.result.isBootstrapped')
if [ "$STATUS" != "true" ]; then
send_alert "$CHAIN-Chain is not bootstrapped!"
@@ -468,7 +468,7 @@ PEERS=$(curl -s -X POST --data '{
"method": "info.peers",
"params": {},
"id": 1
}' -H 'content-type:application/json;' $NODE_URL/ext/info | jq -r '.result.numPeers')
}' -H 'content-type:application/json;' $NODE_URL/v1/info | jq -r '.result.numPeers')
if [ "$PEERS" -lt 4 ]; then
send_alert "Low peer count: $PEERS"
@@ -619,7 +619,7 @@ Create runbooks for common issues:
```bash
# Real-time metrics
watch -n 1 'curl -s http://localhost:9630/ext/metrics | grep -E "chain_height|network_peers"'
watch -n 1 'curl -s http://localhost:9630/v1/metrics | grep -E "chain_height|network_peers"'
# Log streaming
tail -f ~/.luxd/logs/*.log | grep --line-buffered ERROR
@@ -26,7 +26,7 @@ else
fi
# Check API responsiveness
if curl -s http://localhost:9630/ext/health > /dev/null 2>&1; then
if curl -s http://localhost:9630/v1/health > /dev/null 2>&1; then
echo "✅ API is responsive"
else
echo "❌ API is NOT responsive"
@@ -53,7 +53,7 @@ PEERS=$(curl -s -X POST --data '{
"jsonrpc":"2.0",
"method":"info.peers",
"id":1
}' -H 'content-type:application/json;' http://localhost:9630/ext/info 2>/dev/null | jq -r '.result.numPeers')
}' -H 'content-type:application/json;' http://localhost:9630/v1/info 2>/dev/null | jq -r '.result.numPeers')
if [ -n "$PEERS" ] && [ "$PEERS" -gt 0 ]; then
echo "✅ Connected peers: $PEERS"
@@ -225,7 +225,7 @@ curl -X POST --data '{
"method":"platform.getCurrentValidators",
"params":{"nodeIDs":["<your-node-id>"]},
"id":1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
# Verify staking transaction
curl -X POST --data '{
@@ -233,7 +233,7 @@ curl -X POST --data '{
"method":"platform.getTx",
"params":{"txID":"<staking-tx-id>"},
"id":1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
```
#### Issue: Low uptime percentage
@@ -275,7 +275,7 @@ curl -X POST --data '{
"jsonrpc":"2.0",
"method":"platform.getHeight",
"id":1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
# Reduce consensus participation
./build/node --consensus-gossip-concurrent=2
@@ -334,7 +334,7 @@ grep "api" ~/.luxd/configs/node-config.json
./build/node --http-host=0.0.0.0
# Check for rate limiting
curl -I http://localhost:9630/ext/info
curl -I http://localhost:9630/v1/info
```
### Staking Issues
@@ -369,7 +369,7 @@ curl -X POST --data '{
"method":"platform.getBalance",
"params":{"addresses":["P-lux1..."]},
"id":1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
# Import funds from X-Chain
curl -X POST --data '{
@@ -381,7 +381,7 @@ curl -X POST --data '{
"sourceChain":"X"
},
"id":1
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
```
## Log Analysis
@@ -518,7 +518,7 @@ curl -X POST --data '{
"jsonrpc":"2.0",
"method":"info.getNodeID",
"id":1
}' http://localhost:9630/ext/info
}' http://localhost:9630/v1/info
```
### Support Channels
@@ -0,0 +1,163 @@
# Postmortem: C-Chain accepted-head state GC eviction (mainnet freeze)
**Status:** Resolved. Mainnet recovered and accepted at 5/5 validators, C-Chain
height 1085412, hash `0xd957eae6cb0bbef37174…`, all validators in agreement,
explorer at tip, treasury and Genesis NFT state verified.
**Severity:** Critical (mainnet C-Chain unable to build blocks; no state loss).
## Accepted permanent invariant
> The accepted C-Chain head's state root must never be GC/pruning eligible —
> across idle windows, duplicate empty-block state roots, cold snapshot/cache
> layers, small state history, and restarts.
>
> accepted head ⇒ accepted head state root is pinned ⇒ GC/pruning cannot evict
> the execution base for H+1.
The specific accepted-head GC eviction failure is **structurally prevented** by
the head-state pin, and production evidence confirms the fleet no longer
exhibits the prior failure signature. (A formal long-idle ritual was not
completed to termination during the incident window; the sign-off rests on the
structural invariant plus production evidence: a head idle for 3h04m was built
on cleanly, multiple 515 minute zero-traffic windows passed with tip state
readable, and zero eviction/materialize canaries appeared fleet-wide after the
fix.)
## Failure mode (exact)
The C-Chain EVM (coreth-lineage, `luxfi/evm`) in pruning mode manages trie
memory with `cappedMemoryTrieWriter` (`core/state_manager.go`):
- Accepted state roots are held in a `tipBuffer` (`BoundedBuffer`) of depth
`state-history` (default **32**); as roots age out of the buffer they are
`Dereference`d.
- Dirty trie nodes are only committed to disk at `commit-interval` boundaries
(default **4096** blocks), with optimistic `Cap` flushes near the boundary.
- The insert-time `triedb.Reference(root, {})` in `writeBlockAndSetHead` is
**refcount-balanced**: it is consumed as the block ages through the
tipBuffer (or via `RejectTrie`). It therefore does not protect an idle head.
On an idle chain, consecutive empty blocks share identical state roots. The
tipBuffer's aging `Dereference` for an old entry then lands on the *live head
root* (duplicate key), dropping its reference count to zero. At any height that
is not a commit boundary the head root has never been persisted, so the next
`Cap`/flush evicts it from the dirty cache. The subsequent `BuildBlock` cannot
open the parent (head) state:
```
failed to materialize parent state for build: … StateAt: missing trie node
<head state root> … is not available, not found
```
and the chain wedges. RPC reads at `latest` fail with the same error (any
`StateAt(root)` caller). Consensus is unaffected — all validators agree on the
head *block*; only the local execution base for H+1 is gone. State is always
deterministically re-derivable from durable blocks, so a restart re-executes
and recovers — **restart is recovery evidence, not a fix**: the head could
still be evicted again in the next idle window.
### Preconditions (all defaults on affected mainnet validators)
- `pruning-enabled: true`
- `state-history: 32`
- `commit-interval: 4096`
- idle or bursty-then-idle traffic (heartbeat pause, low organic flow)
- duplicate empty-block state roots at the tip
- current height not at a commit boundary
Any C-Chain deployment matching these preconditions is exposed on affected
versions — this bug class will recur wherever the EVM runs with pruning, small
state history, long commit intervals, and idle traffic.
### Observed occurrences
1. Mainnet froze at height 1085200 after a ~10 minute heartbeat pause
(2026-07-07). All five validators had the block, none could serve or build
on its state.
2. An earlier fleet-wide variant contributed to the 1082879→1085012 incident
window (mixed with a separate proposervm/consensus issue documented in the
consensus fault-recovery audit).
## Affected / fixed versions
| Component | Affected | Fixed |
|---|---|---|
| `luxfi/evm` (C-Chain plugin) | ≤ v1.104.6 (all pruning-mode deployments; the balanced insert-time Reference in v1.104.3-hotfix was insufficient) | **v1.104.7** |
| `luxfi/node` image | v1.34.14 v1.34.23 (carry affected EVM plugins) | **v1.34.24** (interim), **v1.34.25** (canonical: identical fix, proper semver, clean go.mod) |
Fix commits (`luxfi/evm`, branch `evm-main-bugb`): `9bab20f7a` (pin),
`58b90490c` (non-fatal degradation), `e3781c35c` (vm v1.2.6 parity).
## The fix (structural)
`core/blockchain.go`: a dedicated **unbalanced** GC reference held on the
accepted head's state root — `headStatePinRoot` + `pinAcceptedHead(root)`:
- Transferred head-to-head: `Reference` the new head root first, then
`Dereference` the previous pinned root; exactly one live head pin exists at
all times, on `lastAccepted.Root()`.
- Established in `Accept`, in `SetLastAcceptedBlockDirect`, and on the loaded
head at startup (`loadLastState`), so the invariant holds across restarts
and the restart-then-idle path.
- Skips when the root is unchanged (duplicate empty-block roots keep exactly
one reference) and when state is not yet materialized (bootstrapping /
state-sync; the first `Accept` then establishes it).
- Deliberately **not** balanced against `InsertTrie`/`AcceptTrie`/`RejectTrie`
— its lifetime is "is the accepted head", nothing else.
- Non-fatal on backend error (pathdb `Reference`/`Dereference` are no-ops /
"not supported"): a failed pin degrades to pre-fix behavior with a WARN
rather than wedging Accept or startup.
No archive-mode workaround and no state-sync hack. Disk growth is unchanged
(one extra referenced root).
## Recovery recipe (what actually worked)
1. **Wedged-at-tip (state evicted, DB otherwise consistent):** restart the
node. Boot re-executes from the last committed root and re-materializes the
head state deterministically. Valid as *recovery*; deploy the fixed version
so it cannot recur.
2. **proposervm/EVM height split** (`proposervm finality index … is BEHIND the
inner VM tip`; produced here by crash-churn on affected versions — the
fail-closed guard then correctly refuses to mount): restarts cannot heal a
split. Restore the node's PVC from a `VolumeSnapshot` of a currently
healthy peer. Per-ordinal staking keys are installed by `startup.sh` from
the `luxd-staking` secret, so cross-node volume clones are safe (distinct
NodeIDs).
3. **PVC swaps must happen at StatefulSet `replicas=0`.** A live single-pod
PVC delete/recreate always loses the race to the StatefulSet controller,
which recreates a blank PVC first.
4. If a fleet-consistent EVM rewind is needed instead (no healthy peer):
`evm/cmd/repair-cchain` rewinds the standalone EVM `lastAccepted` to the
proposervm floor; on boot the heightAhead branch self-heals (used in the
1084996 recovery). Zero re-execution; never a re-genesis.
5. Retain evidence snapshots before every destructive step.
## Residual follow-ups (non-blocking; restart/churn liveness, not consensus or state-loss)
1. **Proposer-preference restart loop:** after heavy sibling churn, a node's
proposervm preference can reference a never-persisted outer block; every
`BuildBlock` then fails `not found` in a tight loop and the node's voter
goes mute (observed ~170 err/s). Restart clears it. Fix: fall back to
last-accepted when the preferred parent is not fetchable.
**FIXED (commit `8001bc5179`, branch `ship/node-v1.34.24`, ships in
v1.34.26):** `vms/proposervm/vm.go` `BuildBlock` now builds the child on
last-accepted (always held — committed state) when `vm.preferred` is
unfetchable, instead of hard-erroring; it surfaces the original error only
when last-accepted is itself the unfetchable id. Build-side companion to the
already-shipped defect #1 `SetPreference` validate-before-assign hardening.
Tests: `vms/proposervm/vm_buildblock_fallback_test.go`.
2. **Ancestor-fetch liveness:** the finality guard refuses certs with
"ancestor … is not tracked (behind; fetch and retry)" but the fetch never
fires, so the node loops instead of catching up. Restart clears it. Fix:
actually schedule the ancestor fetch on this path.
## Monitoring (keep active)
- Eviction/materialize canaries: `STATE-MATERIALIZE`, `missing trie node`,
`ACCEPT-BACKSTOP` log lines — expect zero.
- Accepted height/hash equality across all validators.
- Explorer tip parity with chain head.
- `is BEHIND the inner` (heightBehind) occurrences — expect zero.
- Do not treat the heartbeat as a safety mechanism; it is a liveness nicety.
+7 -7
View File
@@ -75,9 +75,9 @@ func NewMultiNetworkNode() *MultiNetworkNode {
// StartRPCServer starts the unified RPC server
func (n *MultiNetworkNode) StartRPCServer(port int) {
http.HandleFunc("/ext/crossnet/status", n.handleCrossNetStatus)
http.HandleFunc("/ext/crossnet/validators", n.handleCrossNetValidators)
http.HandleFunc("/ext/network/", n.handleNetworkSpecific)
http.HandleFunc("/v1/crossnet/status", n.handleCrossNetStatus)
http.HandleFunc("/v1/crossnet/validators", n.handleCrossNetValidators)
http.HandleFunc("/v1/network/", n.handleNetworkSpecific)
fmt.Printf("🌐 Multi-Network RPC Server starting on port %d\n", port)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil))
@@ -172,7 +172,7 @@ func (n *MultiNetworkNode) handleCrossNetValidators(w http.ResponseWriter, r *ht
// handleNetworkSpecific routes to network-specific handlers
func (n *MultiNetworkNode) handleNetworkSpecific(w http.ResponseWriter, r *http.Request) {
// Parse network ID from path: /ext/network/{networkID}/...
// Parse network ID from path: /v1/network/{networkID}/...
// This would route to the appropriate network's chain manager
response := fmt.Sprintf(`{
@@ -251,9 +251,9 @@ func main() {
}
fmt.Println("\n🌐 Starting Multi-Network RPC Server...")
fmt.Println(" • Cross-network status: http://localhost:9650/ext/crossnet/status")
fmt.Println(" • Cross-network validators: http://localhost:9650/ext/crossnet/validators")
fmt.Println(" • Network-specific: http://localhost:9650/ext/network/{networkID}/...")
fmt.Println(" • Cross-network status: http://localhost:9650/v1/crossnet/status")
fmt.Println(" • Cross-network validators: http://localhost:9650/v1/crossnet/validators")
fmt.Println(" • Network-specific: http://localhost:9650/v1/network/{networkID}/...")
// This would be replaced with actual RPC server
node.StartRPCServer(9650)
+23 -13
View File
@@ -54,7 +54,8 @@ var (
QChainAliases = AliasesFor("Q")
AChainAliases = AliasesFor("A")
BChainAliases = AliasesFor("B")
TChainAliases = AliasesFor("T")
MChainAliases = AliasesFor("M")
FChainAliases = AliasesFor("F")
ZChainAliases = AliasesFor("Z")
GChainAliases = AliasesFor("G")
KChainAliases = AliasesFor("K")
@@ -604,7 +605,8 @@ func FromConfig(config *genesiscfg.Config) ([]byte, ids.ID, error) {
{[]byte(config.CChainGenesis), constants.EVMID, "C-Chain", nil},
{[]byte(config.DChainGenesis), constants.DexVMID, "D-Chain", nil},
{[]byte(config.BChainGenesis), constants.BridgeVMID, "B-Chain", nil},
{[]byte(config.TChainGenesis), constants.ThresholdVMID, "T-Chain", nil},
{[]byte(config.MChainGenesis), constants.MPCVMID, "M-Chain", nil},
{[]byte(config.FChainGenesis), constants.FHEVMID, "F-Chain", nil},
{[]byte(config.QChainGenesis), constants.QuantumVMID, "Q-Chain", nil},
{[]byte(config.ZChainGenesis), constants.ZKVMID, "Z-Chain", nil},
// A/G/K carry genesis blobs and are deterministic genesis chains
@@ -726,10 +728,10 @@ func UTXOAssetIDFromGenesisBytes(genesisBytes []byte) (ids.ID, bool, error) {
if !ok {
continue
}
if uChain.VMID != constants.XVMID {
if uChain.VMID() != constants.XVMID {
continue
}
id, err := xvmgenesis.AssetIDFromBytes(uChain.GenesisData)
id, err := xvmgenesis.AssetIDFromBytes(uChain.GenesisData())
if err != nil {
return ids.Empty, false, fmt.Errorf("derive X-Chain asset ID from genesis data: %w", err)
}
@@ -747,7 +749,7 @@ func VMGenesis(genesisBytes []byte, vmID ids.ID) (*pchaintxs.Tx, error) {
}
for _, chain := range gen.Chains {
uChain := chain.Unsigned.(*pchaintxs.CreateChainTx)
if uChain.VMID == vmID {
if uChain.VMID() == vmID {
return chain, nil
}
}
@@ -776,7 +778,7 @@ func Aliases(genesisBytes []byte) (map[string][]string, map[ids.ID][]string, err
uChain := chain.Unsigned.(*pchaintxs.CreateChainTx)
chainID := chain.ID()
endpoint := path.Join(constants.ChainAliasPrefix, chainID.String())
switch uChain.VMID {
switch uChain.VMID() {
case constants.XVMID:
apiAliases[endpoint] = []string{
"X",
@@ -832,16 +834,24 @@ func Aliases(genesisBytes []byte) (map[string][]string, map[ids.ID][]string, err
path.Join(constants.ChainAliasPrefix, "bridge"),
}
chainAliases[chainID] = BChainAliases
case constants.ThresholdVMID:
case constants.MPCVMID:
apiAliases[endpoint] = []string{
"T",
"threshold",
"thresholdvm",
"M",
"mpc",
path.Join(constants.ChainAliasPrefix, "T"),
path.Join(constants.ChainAliasPrefix, "threshold"),
"mpcvm",
path.Join(constants.ChainAliasPrefix, "M"),
path.Join(constants.ChainAliasPrefix, "mpc"),
}
chainAliases[chainID] = TChainAliases
chainAliases[chainID] = MChainAliases
case constants.FHEVMID:
apiAliases[endpoint] = []string{
"F",
"fhe",
"fhevm",
path.Join(constants.ChainAliasPrefix, "F"),
path.Join(constants.ChainAliasPrefix, "fhe"),
}
chainAliases[chainID] = FChainAliases
case constants.ZKVMID:
apiAliases[endpoint] = []string{
"Z",
+2 -3
View File
@@ -11,7 +11,6 @@ import (
"github.com/stretchr/testify/require"
"github.com/luxfi/node/vms/platformvm/genesis"
pchaintxs "github.com/luxfi/node/vms/platformvm/txs"
)
// TestUTXOAssetIDFromGenesisBytes_Sovereign asserts the canonical
@@ -63,7 +62,7 @@ func TestUTXOAssetIDFromGenesisBytes_POnly(t *testing.T) {
require := require.New(t)
pOnly := &genesis.Genesis{Chains: nil}
pOnlyBytes, err := genesis.Codec.Marshal(pchaintxs.CodecVersion, pOnly)
pOnlyBytes, err := pOnly.Bytes()
require.NoError(err)
id, ok, err := UTXOAssetIDFromGenesisBytes(pOnlyBytes)
@@ -93,7 +92,7 @@ func TestVMGenesisOptInChains(t *testing.T) {
pOnly := &genesis.Genesis{
Chains: nil,
}
pOnlyBytes, err := genesis.Codec.Marshal(pchaintxs.CodecVersion, pOnly)
pOnlyBytes, err := pOnly.Bytes()
require.NoError(err)
for name, vmID := range map[string]ids.ID{
+10 -10
View File
@@ -314,15 +314,15 @@ func TestFromConfigExplicitStakers(t *testing.T) {
for i, vdrTx := range parsed.Validators {
switch ut := vdrTx.Unsigned.(type) {
case *txs.AddValidatorTx:
require.Equal(stakers[i].Weight, ut.Wght,
require.Equal(stakers[i].Weight, ut.Weight(),
"validator %d weight mismatch", i)
t.Logf("Validator %d: NodeID=%s Weight=%d StakeOuts=%d",
i, ut.Validator.NodeID, ut.Wght, len(ut.StakeOuts))
i, ut.Validator().NodeID, ut.Weight(), len(ut.StakeOuts()))
case *txs.AddPermissionlessValidatorTx:
require.Equal(stakers[i].Weight, ut.Wght,
require.Equal(stakers[i].Weight, ut.Weight(),
"validator %d weight mismatch", i)
t.Logf("Validator %d: NodeID=%s Weight=%d StakeOuts=%d",
i, ut.Validator.NodeID, ut.Wght, len(ut.StakeOuts))
i, ut.Validator().NodeID, ut.Weight(), len(ut.StakeOuts()))
default:
t.Fatalf("unexpected validator tx type: %T", ut)
}
@@ -396,15 +396,15 @@ func TestFromConfigExplicitStakersNoStakedFunds(t *testing.T) {
for i, vdrTx := range parsed.Validators {
switch ut := vdrTx.Unsigned.(type) {
case *txs.AddValidatorTx:
require.Greater(ut.Wght, uint64(0), "validator %d weight must be non-zero", i)
require.Greater(len(ut.StakeOuts), 0, "validator %d must have stake outputs", i)
require.Greater(ut.Weight(), uint64(0), "validator %d weight must be non-zero", i)
require.Greater(len(ut.StakeOuts()), 0, "validator %d must have stake outputs", i)
t.Logf("Validator %d: NodeID=%s Weight=%d StakeOuts=%d",
i, ut.Validator.NodeID, ut.Wght, len(ut.StakeOuts))
i, ut.Validator().NodeID, ut.Weight(), len(ut.StakeOuts()))
case *txs.AddPermissionlessValidatorTx:
require.Greater(ut.Wght, uint64(0), "validator %d weight must be non-zero", i)
require.Greater(len(ut.StakeOuts), 0, "validator %d must have stake outputs", i)
require.Greater(ut.Weight(), uint64(0), "validator %d weight must be non-zero", i)
require.Greater(len(ut.StakeOuts()), 0, "validator %d must have stake outputs", i)
t.Logf("Validator %d: NodeID=%s Weight=%d StakeOuts=%d",
i, ut.Validator.NodeID, ut.Wght, len(ut.StakeOuts))
i, ut.Validator().NodeID, ut.Weight(), len(ut.StakeOuts()))
default:
t.Fatalf("unexpected validator tx type: %T", ut)
}
+2 -1
View File
@@ -62,7 +62,8 @@ var Registry = []ChainSpec{
{Letter: "Q", VMID: constants.QuantumVMID, Aliases: []string{"quantum", "quantumvm", "pq"}, Name: "Q-Chain"},
{Letter: "A", VMID: constants.AIVMID, Aliases: []string{"attest", "ai", "aivm"}, Name: "A-Chain"},
{Letter: "B", VMID: constants.BridgeVMID, Aliases: []string{"bridge", "bridgevm"}, Name: "B-Chain"},
{Letter: "T", VMID: constants.ThresholdVMID, Aliases: []string{"threshold", "thresholdvm", "mpc"}, Name: "T-Chain"},
{Letter: "M", VMID: constants.MPCVMID, Aliases: []string{"mpc", "mpcvm"}, Name: "M-Chain"},
{Letter: "F", VMID: constants.FHEVMID, Aliases: []string{"fhe", "fhevm"}, Name: "F-Chain"},
{Letter: "Z", VMID: constants.ZKVMID, Aliases: []string{"zk", "zkvm"}, Name: "Z-Chain"},
{Letter: "G", VMID: constants.GraphVMID, Aliases: []string{"graph", "graphvm", "dgraph"}, Name: "G-Chain"},
{Letter: "K", VMID: constants.KeyVMID, Aliases: []string{"key", "keyvm"}, Name: "K-Chain"},
+4 -2
View File
@@ -41,7 +41,8 @@ func TestChainAliasesRegistryParity(t *testing.T) {
{"Q", QChainAliases, []string{"Q", "quantum", "quantumvm", "pq"}},
{"A", AChainAliases, []string{"A", "attest", "ai", "aivm"}},
{"B", BChainAliases, []string{"B", "bridge", "bridgevm"}},
{"T", TChainAliases, []string{"T", "threshold", "thresholdvm", "mpc"}},
{"M", MChainAliases, []string{"M", "mpc", "mpcvm"}},
{"F", FChainAliases, []string{"F", "fhe", "fhevm"}},
{"Z", ZChainAliases, []string{"Z", "zk", "zkvm"}},
{"G", GChainAliases, []string{"G", "graph", "graphvm", "dgraph"}},
{"K", KChainAliases, []string{"K", "key", "keyvm"}},
@@ -72,7 +73,8 @@ func TestVMAliasesRegistryParity(t *testing.T) {
constants.QuantumVMID: {"quantumvm", "quantum", "pq"},
constants.AIVMID: {"aivm", "attest", "ai"},
constants.BridgeVMID: {"bridgevm", "bridge"},
constants.ThresholdVMID: {"thresholdvm", "threshold", "mpc"},
constants.MPCVMID: {"mpc", "mpcvm"},
constants.FHEVMID: {"fhe", "fhevm"},
constants.ZKVMID: {"zkvm", "zk"},
constants.GraphVMID: {"graphvm", "graph", "dgraph"},
constants.KeyVMID: {"keyvm", "key"},
+73
View File
@@ -0,0 +1,73 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// zzz_gateprobe_test.go is a BLUE-team gate instrument for the
// mpcvm-decomplect surgery (LP-134 / LP-7050). NOT a behavioral
// assertion — it emits a deterministic per-chain digest of the fully
// built P-Chain genesis for every network so before/after can be diffed
// to PROVE which chains changed and which stayed byte-identical.
// zzz_ prefix keeps it last. Delete after RED sign-off.
package builder
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"sort"
"testing"
"github.com/luxfi/node/vms/platformvm/genesis"
pchaintxs "github.com/luxfi/node/vms/platformvm/txs"
)
func TestZZZ_GateProbe_PerChainDigest(t *testing.T) {
nets := []struct {
name string
id uint32
}{
{"mainnet", 1},
{"testnet", 2},
{"devnet", 3},
{"localnet", 1337},
}
for _, n := range nets {
cfg := GetConfig(n.id)
if cfg == nil {
t.Fatalf("%s GetConfig: nil", n.name)
}
gb, rootID, err := FromConfig(cfg)
if err != nil {
t.Fatalf("%s FromConfig: %v", n.name, err)
}
gen, err := genesis.Parse(gb)
if err != nil {
t.Fatalf("%s Parse: %v", n.name, err)
}
type row struct {
name string
vmid string
dataSum string
chainID string
}
rows := make([]row, 0, len(gen.Chains))
for _, c := range gen.Chains {
u := c.Unsigned.(*pchaintxs.CreateChainTx)
sum := sha256.Sum256(u.GenesisData())
rows = append(rows, row{
name: u.BlockchainName(),
vmid: u.VMID().String(),
dataSum: hex.EncodeToString(sum[:]),
chainID: c.ID().String(),
})
}
sort.Slice(rows, func(i, j int) bool { return rows[i].name < rows[j].name })
fmt.Printf("=== NET %s (id=%d) P-ROOT=%s ===\n", n.name, n.id, rootID.String())
for _, r := range rows {
fmt.Printf(" CHAIN name=%-10s vmid=%-52s dataSHA256=%s chainTxID=%s\n",
r.name, r.vmid, r.dataSum, r.chainID)
}
}
}
+18 -26
View File
@@ -26,14 +26,14 @@ require (
github.com/huin/goupnp v1.3.0
github.com/jackpal/gateway v1.1.1
github.com/jackpal/go-nat-pmp v1.0.2
github.com/luxfi/consensus v1.35.2
github.com/luxfi/crypto v1.19.26
github.com/luxfi/consensus v1.36.1
github.com/luxfi/crypto v1.20.0
github.com/luxfi/database v1.20.4
github.com/luxfi/ids v1.3.0
github.com/luxfi/ids v1.3.1
github.com/luxfi/keychain v1.0.2
github.com/luxfi/log v1.4.3
github.com/luxfi/math v1.4.1
github.com/luxfi/metric v1.5.9
github.com/luxfi/metric v1.6.0
github.com/luxfi/mock v0.1.1
github.com/mr-tron/base58 v1.3.0
github.com/onsi/ginkgo/v2 v2.28.1
@@ -118,15 +118,15 @@ require (
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/golang/mock v1.7.0-rc.1
github.com/luxfi/accel v1.2.4
github.com/luxfi/api v1.0.15
github.com/luxfi/api v1.0.16
github.com/luxfi/atomic v1.0.0
github.com/luxfi/chains v1.4.8
github.com/luxfi/chains v1.7.5
github.com/luxfi/codec v1.1.5
github.com/luxfi/compress v0.0.5
github.com/luxfi/constants v1.5.8
github.com/luxfi/constants v1.6.1
github.com/luxfi/container v0.0.4
github.com/luxfi/filesystem v0.0.1
github.com/luxfi/genesis v1.13.16
github.com/luxfi/genesis v1.16.1
github.com/luxfi/genesis/pkg/genesis/security v1.13.8
github.com/luxfi/geth v1.17.12
github.com/luxfi/go-bip39 v1.1.2
@@ -139,14 +139,15 @@ require (
github.com/luxfi/runtime v1.1.3
github.com/luxfi/sdk v1.17.9
github.com/luxfi/sys v0.1.0
github.com/luxfi/threshold v1.12.1
github.com/luxfi/timer v1.0.2
github.com/luxfi/units v1.0.0
github.com/luxfi/utils v1.2.0
github.com/luxfi/utxo v0.3.7
github.com/luxfi/utxo v0.5.6
github.com/luxfi/validators v1.2.0
github.com/luxfi/vm v1.2.5
github.com/luxfi/vm v1.2.7
github.com/luxfi/warp v1.24.0
github.com/luxfi/zap v0.8.11
github.com/luxfi/zap v1.2.3
github.com/luxfi/zwing v0.5.2
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354
github.com/zap-proto/http v0.0.0-20260506200741-fd6047874433
@@ -176,8 +177,8 @@ require (
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.18 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect
github.com/aws/smithy-go v1.24.2 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.6 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.5.0 // indirect
github.com/btcsuite/btcd/chainhash/v2 v2.0.0 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/colega/zeropool v0.0.0-20230505084239-6fb4a4f75381 // indirect
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
@@ -189,8 +190,7 @@ require (
github.com/hanzoai/vfs v0.4.3 // indirect
github.com/hanzos3/go-sdk v1.0.2 // indirect
github.com/klauspost/crc32 v1.3.0 // indirect
github.com/luxfi/age v1.5.0 // indirect
github.com/luxfi/bft v0.1.5 // indirect
github.com/luxfi/age v1.6.0 // indirect
github.com/luxfi/corona v0.10.3 // indirect
github.com/luxfi/crypto/ipa v1.2.4 // indirect
github.com/luxfi/dkg v0.3.5 // indirect
@@ -200,13 +200,11 @@ require (
github.com/luxfi/magnetar v1.2.3 // indirect
github.com/luxfi/mdns v0.1.1 // indirect
github.com/luxfi/mlwe v0.2.1 // indirect
github.com/luxfi/pq v1.0.3 // indirect
github.com/luxfi/precompile v0.16.0 // indirect
github.com/luxfi/pq v1.1.0 // indirect
github.com/luxfi/precompile v0.19.1 // indirect
github.com/luxfi/pulsar v1.9.0 // indirect
github.com/luxfi/staking v1.5.1 // indirect
github.com/luxfi/threshold v1.12.0 // indirect
github.com/luxfi/trace v1.1.0 // indirect
github.com/luxfi/zapcodec v1.0.1 // indirect
github.com/luxfi/zapdb v1.10.1 // indirect
github.com/miekg/dns v1.1.72 // indirect
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b // indirect
@@ -216,6 +214,7 @@ require (
github.com/rs/xid v1.6.0 // indirect
github.com/tinylib/msgp v1.6.4 // indirect
go.mongodb.org/mongo-driver v1.17.9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
)
require (
@@ -262,10 +261,3 @@ require (
)
exclude github.com/ethereum/go-ethereum v1.10.26
// TEMPORARY local-dev build aid for the bootstrap frozen-cache convergence fix.
// FINAL CASCADE (publish step, NOT done here): tag consensus v1.25.36 (the uncommitted
// engine/chain/integration.go FinalizedLedger + FinalizedBlockAtHeight accessors and the
// engine/chain/bootstrap HasAccepted change), bump the require above v1.25.35 v1.25.36,
// then DELETE this replace. The zap client (option b) is node-only and does NOT widen the
// consensus bump.
+46 -37
View File
@@ -67,8 +67,8 @@ github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6
github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg=
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
github.com/btcsuite/btcd/btcec/v2 v2.3.6 h1:IzlsEr9olcSRKB/n7c4351F3xHKxS2lma+1UFGCYd4E=
github.com/btcsuite/btcd/btcec/v2 v2.3.6/go.mod h1:m22FrOAiuxl/tht9wIqAoGHcbnCCaPWyauO8y2LGGtQ=
github.com/btcsuite/btcd/btcec/v2 v2.5.0 h1:KioMXOWa76b86sTZZOmbzv/ldaQCmB8KFAyn5PbB8E8=
github.com/btcsuite/btcd/btcec/v2 v2.5.0/go.mod h1:+K/MYXcLBtHEQjRbjHuJChuybk4LCgjdjgRwil+e+Kk=
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00=
@@ -76,8 +76,9 @@ github.com/btcsuite/btcd/btcutil v1.1.6 h1:zFL2+c3Lb9gEgqKNzowKUPQNb8jV7v5Oaodi/
github.com/btcsuite/btcd/btcutil v1.1.6/go.mod h1:9dFymx8HpuLqBnsPELrImQeTQfKBQqzqGbbV3jK55aE=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ=
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/btcsuite/btcd/chainhash/v2 v2.0.0 h1:PMLlSloHJuEeB80XG9EjpXWNEKAZAMLl6YHZ6YsEuoA=
github.com/btcsuite/btcd/chainhash/v2 v2.0.0/go.mod h1:mKxcZ7oGTXE7IRV+sS9hP4EVBwc/SzfNR+52IsOP9j8=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
@@ -298,48 +299,46 @@ github.com/luxfi/accel v1.2.4 h1:5VbIHyEvvfobn2zBiTFODxDw1CeqxCepZOLlvkuf9yQ=
github.com/luxfi/accel v1.2.4/go.mod h1:ISIwAX+ZfsL/S5nsP2JvfldXN6Nc+QzoWf6Jtaq+xsQ=
github.com/luxfi/address v1.0.1 h1:Sc4keyuVzBIvHr7uVeYZf2/WY9YDGUgDi/iiWenj49g=
github.com/luxfi/address v1.0.1/go.mod h1:5j3Eh66v9zvv1GbNdZwt+23krV8JlSDaRzmWZU8ZRM0=
github.com/luxfi/age v1.5.0 h1:zC/Fw/ptZwAXr9nqrxmrcf8752EIl1Lq9RECp9OmCO0=
github.com/luxfi/age v1.5.0/go.mod h1:iAYAxgvrXxcy746+Ovh/eWWDuF9teJLNcCSSOX9RYW0=
github.com/luxfi/api v1.0.15 h1:Q5ox3Ompw/AZNMfB9wpHZosrj9C+ZldAEHKtHEotFdY=
github.com/luxfi/api v1.0.15/go.mod h1:Eer59msIXMnOlFncG0XjEGH3TZML0Dd1bUu4GtB7f4Q=
github.com/luxfi/age v1.6.0 h1:KMD8gSOP4NVCb7NWSlRcgBZNV2xm2a+qQWPyPmiX6f4=
github.com/luxfi/age v1.6.0/go.mod h1:7cu9CIyikgyAvr5MlXFapEDQ15yBaHOSdKkK5lG04WE=
github.com/luxfi/api v1.0.16 h1:RrNHafKYDzI49vHZigz+A8Kmlf60hiZZYcJD9dWfswg=
github.com/luxfi/api v1.0.16/go.mod h1:Eer59msIXMnOlFncG0XjEGH3TZML0Dd1bUu4GtB7f4Q=
github.com/luxfi/atomic v1.0.0 h1:xUV60MuzRvXngaQ1sM0yVC2v4TRoLlUGkkH7M9PS4yw=
github.com/luxfi/atomic v1.0.0/go.mod h1:0G2mTlQ6TXWHICUHrUUPu1/qAiIyR4gSZ2tva9ci/bI=
github.com/luxfi/bft v0.1.5 h1:5xVLPkog4e5LTgaVlb9pgxA0EWE6tkrKwHPZVRz+RZw=
github.com/luxfi/bft v0.1.5/go.mod h1:5I8Ft8yA69xZlDe3RB0i4MgbqFKLZe65o/sha8JuKvU=
github.com/luxfi/cache v1.2.1 h1:kAzOS55/hmYeNKR+0HAKv4ma48Y6JjkI8UQeqdZ8bfI=
github.com/luxfi/cache v1.2.1/go.mod h1:co7JTxZZHpKT31Yh01LFp5aZOxmoUg157FhBLQdQHVU=
github.com/luxfi/chains v1.4.8 h1:i5QxDfGR922oPGYrBbUo2Qn3tFMpJD9BilNTLFaQscE=
github.com/luxfi/chains v1.4.8/go.mod h1:F/jT9YbC8/yD4WxJu4AvRFbi4NyKY+FHBWrE8M08dgE=
github.com/luxfi/chains v1.7.5 h1:JTtE+PBwLSaIPLg38tpJre8K8UDzimvkHYxOrX1RiKo=
github.com/luxfi/chains v1.7.5/go.mod h1:0fC2xs/pwTM7z5P9PxugOcCdOQxIXdoSdr4cIc63YgU=
github.com/luxfi/codec v1.1.5 h1:KBq8uvYm5Dy+E1heG8WBmqbqu8kstlFyE5ASBBB+C8I=
github.com/luxfi/codec v1.1.5/go.mod h1:/ugIv5iEgI+VAuPIetzxNT0eJaEjOID/mrIsgIjJh8g=
github.com/luxfi/compress v0.0.5 h1:4tEUHw5MK1bu5UOjfYCt4OKMiH7yykIgmGPRA/BfJTM=
github.com/luxfi/compress v0.0.5/go.mod h1:Cc1yxD2pfzrvpO32W2GDwLKff+CylHEvzZh2Ko8RSIU=
github.com/luxfi/concurrent v0.0.3 h1:eJyv1fhaC0jMLMw6+QS774cUmp7GK+ouMgvLCqnC7cc=
github.com/luxfi/concurrent v0.0.3/go.mod h1:Aj/FR5NpM0cB2P4Nt3+tz9+dV6V+LUW4HuMgSjwq5hw=
github.com/luxfi/consensus v1.33.3 h1:gUfmxb+KSLJnixFkk7CvKHeO1B9CNBt/Zp6EJEwgVXE=
github.com/luxfi/consensus v1.33.3/go.mod h1:2ON+tN8hsAqd13DmsGk1Epfz6EQ0s4lv/rmOUsh7Gpk=
github.com/luxfi/consensus v1.35.2 h1:Vy0yrLkCqRHhijYu3qNNwEf7e79HvSAbkRqbBLAYZnM=
github.com/luxfi/consensus v1.35.2/go.mod h1:2ON+tN8hsAqd13DmsGk1Epfz6EQ0s4lv/rmOUsh7Gpk=
github.com/luxfi/constants v1.5.8 h1:iNP9AWNUcM4Tps7jYnx49CwtCWAC9mYRxJfGou2za0g=
github.com/luxfi/constants v1.5.8/go.mod h1:Pu5jWHdnUtQRbWC43yTUjU/pbIIKMDOd2a2yroSfo48=
github.com/luxfi/consensus v1.36.1 h1:aHCUTgu0SaJXRouisQuFo1CkkdpHX0XftpM9GU07LzU=
github.com/luxfi/consensus v1.36.1/go.mod h1:hmsGz3CcTrKxvw7/YSmfu8qAtzgyL5zQ3ajpUNcub/4=
github.com/luxfi/constants v1.6.1 h1:4AfBh1YxDgnQjWPLqLpjiBaLAjPBw5naTTzRWWM19ms=
github.com/luxfi/constants v1.6.1/go.mod h1:Pu5jWHdnUtQRbWC43yTUjU/pbIIKMDOd2a2yroSfo48=
github.com/luxfi/container v0.0.4 h1:BXhF82WyfqVP5mjlNcr7tP0Fcnvl0Ap1rkiu+rq5XuM=
github.com/luxfi/container v0.0.4/go.mod h1:Z3SpmMF5d4t77MM0nHYXURpn+EMVaeu1fhbd/3BGaek=
github.com/luxfi/corona v0.10.3 h1:Yi1oAkW0HEsf5fvst/tUN0AjRVg6DoNHB/IC0qrFWZE=
github.com/luxfi/corona v0.10.3/go.mod h1:xe5qRir0p+FA6eETpyGDv4LjYySg1zVB13kmHpy9x94=
github.com/luxfi/crypto v1.19.26 h1:+aHn/L479ak2ih7s/DkBZojjuhcyHBLqu3nYT81vcrU=
github.com/luxfi/crypto v1.19.26/go.mod h1:0DCU62kX8+zhYU2qeM07A4pifJyPkPujnUOfgc8TOFQ=
github.com/luxfi/crypto v1.20.0 h1:JNsQ25sVO6T8XuIHRue4akOpnt5pNmk1xg5hzmU6dNE=
github.com/luxfi/crypto v1.20.0/go.mod h1:bLCBuIV/KDjPytld7jSYe1WbfWknPQXcivq88Qo96QU=
github.com/luxfi/crypto/ipa v1.2.4 h1:6xfwhI9/HrcDkF3Ti5/NxsNQIWbwYDJmRSNIHRQ/xfU=
github.com/luxfi/crypto/ipa v1.2.4/go.mod h1:43J6f6rcfUMrZt4cQectMOZb6Ps+fAEj8ZTPC3Kk+gE=
github.com/luxfi/database v1.20.4 h1:WOt2GIGJxf8AFpg49odMz8DZ8RFSLDrozGhZtmorN70=
github.com/luxfi/database v1.20.4/go.mod h1:S/LvmfzNYWVNslcEcZwDrntqUO2ksaL8ql1nRmLUA/Q=
github.com/luxfi/dex v1.14.0 h1:4PtorVbRmbD73S6YWPvgp5GRCb9jeuaedl7mo1CbzCI=
github.com/luxfi/dex v1.14.0/go.mod h1:wYWQmwospvdKBWQ6OJMXb8I+kfgEtE46R1rD8H7vHCQ=
github.com/luxfi/dkg v0.3.5 h1:s2L2mMQaz+n9m0b0ghvoV5VZNxiwb2z4WrGugvK0udY=
github.com/luxfi/dkg v0.3.5/go.mod h1:M+WH7GFRN+YUD851Rlnumdp0Md98kplNN8pVx65U8I8=
github.com/luxfi/filesystem v0.0.1 h1:VZ6xMFKaAPBW/ddlMsDnI2G0VU1lV5rYaVcW5d+KwEY=
github.com/luxfi/filesystem v0.0.1/go.mod h1:OQVSU6XNwqrr1AI+MqkID2taHUclx7NYmmr3svgttec=
github.com/luxfi/formatting v1.0.1 h1:ZnE1rAdEUds9yAegdVdGDOBGN6hLMPOv6E03Fp8IEYo=
github.com/luxfi/formatting v1.0.1/go.mod h1:mYzNf5DJOiqSSKUPzNj5dKy4tstFbN3pZlkI5716eKc=
github.com/luxfi/genesis v1.13.16 h1:suwWPwUu2nv1fxvx9vwHgcgJCzkCpiVMxBrJIh4S3BQ=
github.com/luxfi/genesis v1.13.16/go.mod h1:qUa+AcTWwxv0x+CJochBsRNOMbmEBjw07HJKZLhs5c0=
github.com/luxfi/genesis v1.16.1 h1:t8zFIeFg9hwl39HpYKshpB2hlHjVVBpd8va4d+FZ8T0=
github.com/luxfi/genesis v1.16.1/go.mod h1:vpnyQ/YcGINhUekrCiZWFryvP3qgYzTgFkfWoExlUdE=
github.com/luxfi/genesis/pkg/genesis/security v1.13.8 h1:9ier0p55ErSpoXHRJpZ04WV5HwwMB1uDrU7PHGBKG2U=
github.com/luxfi/genesis/pkg/genesis/security v1.13.8/go.mod h1:DzU+GYUFv12ja4Vc46bWKNBBmNYbcow3u/DASx4wpfI=
github.com/luxfi/geth v1.17.12 h1:UP/fhpcfbGPTrkOCwX3d88Oc3jVm5gTOgfjgq+lek6s=
@@ -348,8 +347,8 @@ github.com/luxfi/go-bip32 v1.0.2 h1:7vFbb+Wr4Z499q2tuCLdd7wWjtn8sH+HWBlx76mhH9Y=
github.com/luxfi/go-bip32 v1.0.2/go.mod h1:bc7/LXDKAJQZ/F0Xjf5yXaTZxY9/ssLb4FC+Hxn/cDk=
github.com/luxfi/go-bip39 v1.1.2 h1:p+wLMPGs6MLQh7q0YIsmy2EhHL7LHiELEGTJko6t/Jg=
github.com/luxfi/go-bip39 v1.1.2/go.mod h1:96de9VkR2kY/ASAnhMtvt3TSh+PZkAFAngNj0GjRGDo=
github.com/luxfi/ids v1.3.0 h1:11xnwRDm6zQzbqcRnkFujOYkvhK4Fs/+g+sKRlRUNsU=
github.com/luxfi/ids v1.3.0/go.mod h1:6vpdcdZW0qxeade+3xby8aLTutbcJ7O0r8+fNQrksGI=
github.com/luxfi/ids v1.3.1 h1:CGE3QvYzdwfDpfODAVNjMygSaueVPWXSB9yaeyCEd+k=
github.com/luxfi/ids v1.3.1/go.mod h1:6vpdcdZW0qxeade+3xby8aLTutbcJ7O0r8+fNQrksGI=
github.com/luxfi/keychain v1.0.2 h1:uQgmjs37/VBIALEiYrrszTpxvtqr07/YvS9TnmxGafs=
github.com/luxfi/keychain v1.0.2/go.mod h1:q/4ULgZBlstKkwzOzG/0T6y73BDPgnkrcibbJyTvmbU=
github.com/luxfi/keys v1.2.0 h1:3TAcr4twyMpwQp7J29ZRtIa5vzAoDrnXnLcPKVHJWmw=
@@ -372,8 +371,8 @@ github.com/luxfi/math/safe v0.0.1 h1:GfSBINV9mOFgHzd32JbgfHSLhlNn0BwnP43rteYEosc
github.com/luxfi/math/safe v0.0.1/go.mod h1:EejrmOJHh03YAD8+Zww8cPcMR1K3Q2I7w1dX4sMloeo=
github.com/luxfi/mdns v0.1.1 h1:g2eRr9AXcziPkkcd24M+Qu9ApEpoKKjfI79QSNqv0rQ=
github.com/luxfi/mdns v0.1.1/go.mod h1:dbp5f3h3aE7CGzwbaWzBM9cwdcekhmSrWhQevgYhhNA=
github.com/luxfi/metric v1.5.9 h1:UAgXMNZf5oN/XJwwuKorf8iMaCj3nyP6thHPCwkUwY4=
github.com/luxfi/metric v1.5.9/go.mod h1:ux+w3RZQCfF1zM8MO0wAWyNj/CsDlPd2mwTGshB9vY0=
github.com/luxfi/metric v1.6.0 h1:PIxHOk8R0qs5etnWsUSPoZ5wGh37APgUiSt4gijOl80=
github.com/luxfi/metric v1.6.0/go.mod h1:ux+w3RZQCfF1zM8MO0wAWyNj/CsDlPd2mwTGshB9vY0=
github.com/luxfi/mlwe v0.2.1 h1:pRwTjNUUtzUxRIlMbUPpeh9DE2/NdqfS17hfdogazp4=
github.com/luxfi/mlwe v0.2.1/go.mod h1:DD9EHTeiyh/y0KGGeqL+q9S4n8raeGiGdaG/BQPAvT0=
github.com/luxfi/mock v0.1.1 h1:0HEtIjg1J6CWz+IUyP6rsGqNWTcmxjFnSQIhaDuARwY=
@@ -382,10 +381,10 @@ github.com/luxfi/net v0.0.5 h1:F1lD3NsIioV0wr2V5jWc4TtMyiE/Ffo1LoeblFv3TrI=
github.com/luxfi/net v0.0.5/go.mod h1:BEQR1HEVmkjii/F1R6vJrNUVE7wr55b4eMq9Iz5wjUw=
github.com/luxfi/p2p v1.21.1 h1:gmz1JMDhzHIL3dQlhwIDvR4OlFuhNVfnWUl/ipYhAIo=
github.com/luxfi/p2p v1.21.1/go.mod h1:SsNPR5fPGWWNem9plGWhSmRqyDoysJ3kPAN0zG0g3iw=
github.com/luxfi/pq v1.0.3 h1:ksw1dmfTR0dqqNMRS7BjGcprCO2Fhc+3Iiq2/NMuONw=
github.com/luxfi/pq v1.0.3/go.mod h1:8bppZcRElfrVt0n3nYCZW3iX1TvhvzNbdjNdK1irgIE=
github.com/luxfi/precompile v0.16.0 h1:lMdKapbApcbehtAc0mkRqkHFdTTITRTo3e3ivdI63RY=
github.com/luxfi/precompile v0.16.0/go.mod h1:nIO7c4arFTqCl3nR0BoumPn1etYY32EYExJxqwu23VA=
github.com/luxfi/pq v1.1.0 h1:ADplfUSyirLymSxs3Ix0HeDTyl5oswCNUpXJt/5vLY8=
github.com/luxfi/pq v1.1.0/go.mod h1:KT5rG9ztpzIkT9QSnXK4WFqBBLzKCLjY7l1c/unBi8I=
github.com/luxfi/precompile v0.19.1 h1:nTfhwrubwQKED5SAOFqIbFO1o6J49IZNdSYbOEaiPpA=
github.com/luxfi/precompile v0.19.1/go.mod h1:AOMGWGFXHtnGVYjel/mP/7Dt60e2u7ef0SswY4j8F+k=
github.com/luxfi/proto v1.3.5 h1:AW11rnu5xyvB7beyowoiY9uIffLOF3+eMR/a3EkK2c8=
github.com/luxfi/proto v1.3.5/go.mod h1:ixTofGpdW1rTYr+wgTuBhAsgBv8GnWYHMLWbPNEdm7M=
github.com/luxfi/pulsar v1.9.0 h1:c0JnatYF79aN87aof9VlYjIoCzmixxrgNPeUUuh8ScU=
@@ -404,8 +403,8 @@ github.com/luxfi/staking v1.5.1 h1:f9MaGnRm0xc02crDm5Qs1T2r88d3KzNkHZypAvsmAlU=
github.com/luxfi/staking v1.5.1/go.mod h1:lT7KLaiTpdq3lg78H0gp2qSEfX9LaK1vs7w73XV/9nw=
github.com/luxfi/sys v0.1.0 h1:M7RYOt8W4Wws7cxxsyOHe50UKMYTzIu7HYknqW4xt0Y=
github.com/luxfi/sys v0.1.0/go.mod h1:GT8vGdYTfoqRy9/11blmRuqPPypzwrudCTHZXT+ru9M=
github.com/luxfi/threshold v1.12.0 h1:JJ369xC/YyDvrqXj+xFoK98nP2rUM099qFs03hBvq/M=
github.com/luxfi/threshold v1.12.0/go.mod h1:iuRQGDAy8ZKjQhZjkSKg7NtbP75/8Up9zj52y7IuyZo=
github.com/luxfi/threshold v1.12.1 h1:pA6ZB8Qv6BStprSemfoCY3fD7P5PEod36Nj6FmJR1jQ=
github.com/luxfi/threshold v1.12.1/go.mod h1:iuRQGDAy8ZKjQhZjkSKg7NtbP75/8Up9zj52y7IuyZo=
github.com/luxfi/timer v1.0.2 h1:g/odi0VQJIsrzdklJUG1thHZ/sGNnbIiVGcU6LctJm0=
github.com/luxfi/timer v1.0.2/go.mod h1:SoaZwntYigUE3H6z1GV32YwP8QaSiAT0UiEv7iPugXg=
github.com/luxfi/tls v1.0.3 h1:rK3nxSAxrUOOSHOZnKChwV4f6UJ+cfOl8KWJXAQx/SI=
@@ -418,18 +417,24 @@ github.com/luxfi/upgrade v1.0.1 h1:7+ygYeUf/MuLeGL7pjIu6ckQimxctCp+Swybhpy64go=
github.com/luxfi/upgrade v1.0.1/go.mod h1:Re7g9Y+SYf/LvkHFpN0vbtlVH/Rr5ZpHQdPeVFEo3Jw=
github.com/luxfi/utils v1.2.0 h1:gtEiI7/NM6PQ/OasEpH0PvB+e5hIS/tpum9r64pYjMc=
github.com/luxfi/utils v1.2.0/go.mod h1:T2OCKT1xG9jtKR/gyJQoSkticzrE9WFQ8eohJHGu9Fg=
github.com/luxfi/utxo v0.3.7 h1:JlQ0F0u/QazHcgRK8CRu1mdJOyA+oGAlRMNoAu0/HpU=
github.com/luxfi/utxo v0.3.7/go.mod h1:dbJ7RHU8qj5ttobGYK/A2PsZIQpCsHAIay6xKwc8YQ8=
github.com/luxfi/utxo v0.5.3 h1:3RJ6Ow5zYs2atd6QkhRNsW/cgmrQKz/UOFI25ULebQ8=
github.com/luxfi/utxo v0.5.3/go.mod h1:YF2r3EGm9pGviZuFBY5tX6rIIYmF+2KvFC/OkW/gqTg=
github.com/luxfi/utxo v0.5.5 h1:57iLgiP2nY6d7ljvDpSoQefNbf9+S4Wknj0k99eQ20g=
github.com/luxfi/utxo v0.5.5/go.mod h1:Qom/3mk7+9aEziXzdEGvHxTAl7lA2IsSDs+WUkFxjRY=
github.com/luxfi/utxo v0.5.6 h1:xdmHmgzhq71rVLd752g4gzl7CFeUsKpxttDdzTQGXKc=
github.com/luxfi/utxo v0.5.6/go.mod h1:Qom/3mk7+9aEziXzdEGvHxTAl7lA2IsSDs+WUkFxjRY=
github.com/luxfi/validators v1.2.0 h1:VygpiBqBAdGrfkb7xzE2yrVmnXaqE+hm8FLWdGXO7G8=
github.com/luxfi/validators v1.2.0/go.mod h1:GYLulrNXAan23ZlX7sgWVbVnLpUexeB/m2qr2ymsXok=
github.com/luxfi/version v1.0.1 h1:T/1KYWEMmsrNQk7pN7PFPAwh/7XbeX7cFAKLBqI37Sk=
github.com/luxfi/version v1.0.1/go.mod h1:Y5fPkQ2DB0XRBCxgSPXp4ISzL1/jptKnmFknShRJCyg=
github.com/luxfi/vm v1.2.5 h1:L1etY/gh68f9tns1BtyDUpZcBVqc3Ng1mqU3n38GyLo=
github.com/luxfi/vm v1.2.5/go.mod h1:TCCg4lDcQFCjxaxfXnxPIrpRSVAyyf2ucT4A4w654Hg=
github.com/luxfi/vm v1.2.7 h1:/lHRgSU/Jmn3D5hBg4R3ZntWnY/L5fjF8JcYPoqcIjc=
github.com/luxfi/vm v1.2.7/go.mod h1:o52+zrBZCqBPrAO0dIAmK5Px7oKevT0sup5LssgFdYM=
github.com/luxfi/warp v1.24.0 h1:jrcJNlbOiZsAEopJMy9bSaCwI5NDZ8qgp/6sNoXqepg=
github.com/luxfi/warp v1.24.0/go.mod h1:bKvTi24JHlANsl7qkWZAVr/DsMfvwy42f+Cc9x4+Sq8=
github.com/luxfi/zap v0.8.11 h1:jT+ol9rj557MRdmnzxrVUCR3CDFaE+8OpzUsLIn92og=
github.com/luxfi/zap v0.8.11/go.mod h1:JfqII8VtVQYLLTX6obU1DP9sjGqf9L24vfug5ifh0b8=
github.com/luxfi/zap v1.2.2 h1:1WoijKzhx7P//fExdv9P9GNJS1rtpwlBzHLDh5TBc30=
github.com/luxfi/zap v1.2.2/go.mod h1:JfqII8VtVQYLLTX6obU1DP9sjGqf9L24vfug5ifh0b8=
github.com/luxfi/zap v1.2.3 h1:aLPUgXH5ITqbGRS6tFx1hMZqsB4kL6j39+4s7/JlGgs=
github.com/luxfi/zap v1.2.3/go.mod h1:JfqII8VtVQYLLTX6obU1DP9sjGqf9L24vfug5ifh0b8=
github.com/luxfi/zapcodec v1.0.1 h1:pRxLxCOi6uihQMg8A8riDjNjefU2cXZxfRVZ+obeuL8=
github.com/luxfi/zapcodec v1.0.1/go.mod h1:txrRt2JK4O76ssTxlXIwoNVsgzyZVL0ES4mlXqGNogs=
github.com/luxfi/zapdb v1.10.1 h1:XV3k4UTTKKxUMgbfC7woPXgUEIJd3P5nj2lGTQ88xeE=
@@ -727,6 +732,10 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+1 -1
View File
@@ -70,4 +70,4 @@ esac
echo ""
echo "Import complete! Verify with:"
echo " curl -s -X POST --data '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_blockNumber\",\"params\":[]}' -H 'content-type:application/json' \$RPC_URL/ext/bc/<blockchain-id>/rpc"
echo " curl -s -X POST --data '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_blockNumber\",\"params\":[]}' -H 'content-type:application/json' \$RPC_URL/v1/bc/<blockchain-id>/rpc"
+2 -2
View File
@@ -21,8 +21,8 @@ type Client struct {
// calls.
// [uri] is the path to make API calls to.
// For example:
// - http://1.2.3.4:9650/ext/index/C/block
// - http://1.2.3.4:9650/ext/index/X/tx
// - http://1.2.3.4:9650/v1/index/C/block
// - http://1.2.3.4:9650/v1/index/X/tx
func NewClient(uri string) *Client {
return &Client{
Requester: rpc.NewEndpointRequester(uri),
+2 -2
View File
@@ -20,7 +20,7 @@ import (
// and prints the ID of the block and its transactions.
func main() {
var (
uri = primary.LocalAPIURI + "/ext/index/P/block"
uri = primary.LocalAPIURI + "/v1/index/P/block"
client = indexer.NewClient(uri)
ctx = context.Background()
nextIndex uint64
@@ -39,7 +39,7 @@ func main() {
platformvmBlockBytes = proposerVMBlock.Block()
}
platformvmBlock, err := platformvmblock.Parse(platformvmblock.Codec, platformvmBlockBytes)
platformvmBlock, err := platformvmblock.Parse(platformvmBlockBytes)
if err != nil {
log.Fatalf("failed to parse platformvm block: %s\n", err)
}
+1 -1
View File
@@ -19,7 +19,7 @@ import (
// and prints the ID of the block and its transactions.
func main() {
var (
uri = primary.LocalAPIURI + "/ext/index/X/block"
uri = primary.LocalAPIURI + "/v1/index/X/block"
xChainID = ids.FromStringOrPanic("2eNy1mUFdmaxXNj1eQHUe7Np4gju9sJsEtWQ4MX3ToiNKuADed")
client = indexer.NewClient(uri)
ctx = context.Background()
+13 -13
View File
@@ -25,29 +25,29 @@ Each chain has one or more index. To see if a C-Chain block is accepted, for exa
### C-Chain Blocks
```
/ext/index/C/block
/v1/index/C/block
```
### P-Chain Blocks
```
/ext/index/P/block
/v1/index/P/block
```
### X-Chain Transactions
```
/ext/index/X/tx
/v1/index/X/tx
```
### X-Chain Blocks
```
/ext/index/X/block
/v1/index/X/block
```
<Callout type="warn">
To ensure historical data can be accessed, the `/ext/index/X/vtx` is still accessible, even though it is no longer populated with chain data since the X-Chain block linearization landed. If you are using `V1.10.0` or higher, you need to migrate to using the `/ext/index/X/block` endpoint.
To ensure historical data can be accessed, the `/v1/index/X/vtx` is still accessible, even though it is no longer populated with chain data since the X-Chain block linearization landed. If you are using `V1.10.0` or higher, you need to migrate to using the `/v1/index/X/block` endpoint.
</Callout>
## Methods
@@ -87,7 +87,7 @@ index.getContainerByID({
**Example Call**:
```sh
curl --location --request POST 'localhost:9630/ext/index/X/tx' \
curl --location --request POST 'localhost:9630/v1/index/X/tx' \
--header 'Content-Type: application/json' \
--data-raw '{
"jsonrpc": "2.0",
@@ -151,7 +151,7 @@ index.getContainerByIndex({
**Example Call**:
```sh
curl --location --request POST 'localhost:9630/ext/index/X/tx' \
curl --location --request POST 'localhost:9630/v1/index/X/tx' \
--header 'Content-Type: application/json' \
--data-raw '{
"jsonrpc": "2.0",
@@ -223,7 +223,7 @@ index.getContainerRange({
**Example Call**:
```sh
curl --location --request POST 'localhost:9630/ext/index/X/tx' \
curl --location --request POST 'localhost:9630/v1/index/X/tx' \
--header 'Content-Type: application/json' \
--data-raw '{
"jsonrpc": "2.0",
@@ -282,7 +282,7 @@ index.getIndex({
**Example Call**:
```sh
curl --location --request POST 'localhost:9630/ext/index/X/tx' \
curl --location --request POST 'localhost:9630/v1/index/X/tx' \
--header 'Content-Type: application/json' \
--data-raw '{
"jsonrpc": "2.0",
@@ -339,7 +339,7 @@ index.getLastAccepted({
**Example Call**:
```sh
curl --location --request POST 'localhost:9630/ext/index/X/tx' \
curl --location --request POST 'localhost:9630/v1/index/X/tx' \
--header 'Content-Type: application/json' \
--data-raw '{
"jsonrpc": "2.0",
@@ -394,7 +394,7 @@ index.isAccepted({
**Example Call**:
```sh
curl --location --request POST 'localhost:9630/ext/index/X/tx' \
curl --location --request POST 'localhost:9630/v1/index/X/tx' \
--header 'Content-Type: application/json' \
--data-raw '{
"jsonrpc": "2.0",
@@ -430,7 +430,7 @@ To get an X-Chain transaction by its index (the order it was accepted in), use I
For example, to get the second transaction (note that `"index":1`) accepted on the X-Chain, do:
```sh
curl --location --request POST 'https://indexer-demo.lux.network/ext/index/X/tx' \
curl --location --request POST 'https://indexer-demo.lux.network/v1/index/X/tx' \
--header 'Content-Type: application/json' \
--data-raw '{
"jsonrpc": "2.0",
@@ -474,7 +474,7 @@ curl -X POST --data '{
"txID":"ZGYTSU8w3zUP6VFseGC798vA2Vnxnfj6fz1QPfA9N93bhjJvo",
"encoding": "json"
}
}' -H 'content-type:application/json;' https://api.lux.network/ext/bc/X
}' -H 'content-type:application/json;' https://api.lux.network/v1/bc/X
```
**Response**:
+3 -3
View File
@@ -79,7 +79,7 @@ spec:
cpu: "4"
livenessProbe:
httpGet:
path: /ext/health
path: /v1/health
port: 9630
initialDelaySeconds: 120
periodSeconds: 30
@@ -87,7 +87,7 @@ spec:
timeoutSeconds: 10
readinessProbe:
httpGet:
path: /ext/health
path: /v1/health
port: 9630
initialDelaySeconds: 30
periodSeconds: 10
@@ -95,7 +95,7 @@ spec:
timeoutSeconds: 5
startupProbe:
httpGet:
path: /ext/health
path: /v1/health
port: 9630
initialDelaySeconds: 10
periodSeconds: 10
+24
View File
@@ -34,6 +34,15 @@ type Net interface {
// IsBootstrapped returns true if the chains in this chain are done bootstrapping
IsBootstrapped() bool
// IsChainBootstrapped reports whether a SPECIFIC chain in this net has finished
// initial sync — i.e. Bootstrapped(chainID) was called for it (the chain reached
// the network frontier and its VM went to normal operation). This is the per-chain
// truth that info.isBootstrapped keys on, distinct from IsBootstrapped() which is
// the net-wide "no chain still bootstrapping" aggregate. A chain that is merely
// tracked (added, sync goroutine launched) but has NOT converged is still in the
// bootstrapping set and reads false here — closing the premature-true masking bug.
IsChainBootstrapped(chainID ids.ID) bool
// Bootstrapped marks the chain as done bootstrapping
Bootstrapped(chainID ids.ID)
@@ -66,6 +75,21 @@ func (s *chain) IsBootstrapped() bool {
return s.bootstrapping.Len() == 0
}
// IsChainBootstrapped assumes MONOTONIC per-process bootstrapped state: a chain
// only ever moves bootstrapping→bootstrapped (Bootstrapped is forward-only and
// AddChain refuses to re-add a chain already in either set), so this signal — and
// the Bootstrapping() health check that shares the set — never report stale-true.
// INVARIANT: if a future path ever moves a live chain BACK to bootstrapping (e.g.
// SetState(Bootstrapping) on a running chain) it MUST remove the chainID from
// bootstrapped, or both this signal and the readiness health check will report a
// re-syncing chain as still bootstrapped.
func (s *chain) IsChainBootstrapped(chainID ids.ID) bool {
s.lock.RLock()
defer s.lock.RUnlock()
return s.bootstrapped.Contains(chainID)
}
func (s *chain) Bootstrapped(chainID ids.ID) {
s.lock.Lock()
defer s.lock.Unlock()
+8 -6
View File
@@ -340,15 +340,16 @@ func NewNetwork(
// AddPermissionlessValidatorTx (modern). Handle both.
switch tx := validatorTx.Unsigned.(type) {
case *txs.AddPermissionlessValidatorTx:
nodeID := tx.Validator.NodeID
weight := tx.Validator.Wght
validator := tx.Validator()
nodeID := validator.NodeID
weight := validator.Wght
if weight == 0 {
weight = 1
}
var blsKey []byte
if tx.Signer != nil {
if pubKey := tx.Signer.Key(); pubKey != nil {
if s := tx.Signer(); s != nil {
if pubKey := s.Key(); pubKey != nil {
blsKey = bls.PublicKeyToCompressedBytes(pubKey)
}
}
@@ -368,8 +369,9 @@ func NewNetwork(
zap.Int("blsKeyLen", len(blsKey)),
)
case *txs.AddValidatorTx:
nodeID := tx.Validator.NodeID
weight := tx.Validator.Wght
validator := tx.Validator()
nodeID := validator.NodeID
weight := validator.Wght
if weight == 0 {
weight = 1
}
+5 -5
View File
@@ -1825,18 +1825,18 @@ func (n *Node) initInfoAPI() error {
// initSecurityAPI exposes the chain-wide ChainSecurityProfile as a
// read-only API surface. Three endpoints share one handler:
//
// - JSON-RPC: POST /ext/security with methods securityProfile and
// - JSON-RPC: POST /v1/security with methods securityProfile and
// blockSecurity (dispatched on the wire as security_securityProfile
// / security_blockSecurity per gorilla/rpc namespace convention)
// - REST: GET /ext/security/profile
// - REST: GET /ext/security/block/{n}
// - REST: GET /v1/security/profile
// - REST: GET /v1/security/block/{n}
//
// All three share the same Service receiver; the shape returned is
// the SCREAMING_SNAKE canonical profile JSON consumed by audit tooling,
// wallet posture banners, and block explorers.
//
// Prometheus gauges for the active profile are stamped onto the
// node-wide metrics gatherer here so /ext/metrics carries the profile
// node-wide metrics gatherer here so /v1/metrics carries the profile
// posture immediately after boot.
//
// Closes F102 follow-ups (securityProfile RPC + profile metrics).
@@ -1844,7 +1844,7 @@ func (n *Node) initSecurityAPI() error {
n.Log.Info("initializing security API")
// Register profile metrics under the "security" namespace on the
// node-wide gatherer so /ext/metrics carries them alongside the
// node-wide gatherer so /v1/metrics carries them alongside the
// existing process / api / chain metric families.
securityMetricsReg, err := metric.MakeAndRegister(
n.MetricsGatherer,
+2 -1
View File
@@ -124,7 +124,8 @@ var OptionalVMs = map[ids.ID]PluginSpec{
constants.KeyVMID: {Name: "keyvm"},
constants.OracleVMID: {Name: "oraclevm"},
constants.RelayVMID: {Name: "relayvm"},
constants.ThresholdVMID: {Name: "thresholdvm"},
constants.MPCVMID: {Name: "mpcvm"},
constants.FHEVMID: {Name: "fhevm"},
}
func init() {
+1 -1
View File
@@ -58,7 +58,7 @@ func TestOptionalVMsNotLinkedInProcess(t *testing.T) {
"github.com/luxfi/chains/keyvm",
"github.com/luxfi/chains/oraclevm",
"github.com/luxfi/chains/relayvm",
"github.com/luxfi/chains/thresholdvm",
"github.com/luxfi/chains/mpcvm",
}
for _, pkg := range []string{"./node/", "./main"} {
deps := goListDeps(t, pkg)
+5 -5
View File
@@ -72,7 +72,7 @@ echo ""
# Wait for RPC to be ready
echo -n "Waiting for RPC..."
for _ in {1..30}; do
if curl -s "http://127.0.0.1:$HTTP_PORT/ext/info" >/dev/null 2>&1; then
if curl -s "http://127.0.0.1:$HTTP_PORT/v1/info" >/dev/null 2>&1; then
echo " ready!"
break
fi
@@ -83,10 +83,10 @@ done
# Show status
echo ""
echo "=== Instance Ready ==="
echo " C-Chain RPC: http://127.0.0.1:$HTTP_PORT/ext/bc/C/rpc"
echo " X-Chain RPC: http://127.0.0.1:$HTTP_PORT/ext/bc/X"
echo " P-Chain RPC: http://127.0.0.1:$HTTP_PORT/ext/bc/P"
echo " Info API: http://127.0.0.1:$HTTP_PORT/ext/info"
echo " C-Chain RPC: http://127.0.0.1:$HTTP_PORT/v1/bc/C/rpc"
echo " X-Chain RPC: http://127.0.0.1:$HTTP_PORT/v1/bc/X"
echo " P-Chain RPC: http://127.0.0.1:$HTTP_PORT/v1/bc/P"
echo " Info API: http://127.0.0.1:$HTTP_PORT/v1/info"
echo ""
echo " Logs: tail -f $LOG_FILE"
echo " Stop: kill $(cat "$PID_FILE")"
+1 -1
View File
@@ -60,7 +60,7 @@ PLUGINS=(
r5m1ujrmXxVcQetG3CQfuDLHp2RHKh6vCDaFgBRQfUcTZh7eS # oraclevm
ry9Sg8rZdT26iEKvJDmC2wkESs4SDKgZEhk5BgLSwg1EpcNug # quantumvm
sP6dLqrrBR9w3soP18fbJ3YzZecZdD7DDdfH2cFhhLq7Hy9bz # relayvm
tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t # thresholdvm
tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t # mpcvm
vv3qPfyTVXZ5ArRZA9Jh4hbYDTBe43f7sgQg4CHfNg1rnnvX9 # zkvm
)
+1 -1
View File
@@ -101,7 +101,7 @@ global:
scrape_configs:
- job_name: "node"
metrics_path: "/ext/metrics"
metrics_path: "/v1/metrics"
file_sd_configs:
- files:
- '${FILE_SD_PATH}/*.json'
+11 -11
View File
@@ -147,9 +147,9 @@ func (r *router) handleRootGET(w http.ResponseWriter, _ *http.Request) {
P string `json:"p"`
X string `json:"x"`
}{
C: "/ext/bc/C/rpc",
P: "/ext/bc/P",
X: "/ext/bc/X",
C: baseURL + "/bc/C/rpc",
P: baseURL + "/bc/P",
X: baseURL + "/bc/X",
},
Endpoints: struct {
RPC string `json:"rpc"`
@@ -157,10 +157,10 @@ func (r *router) handleRootGET(w http.ResponseWriter, _ *http.Request) {
Info string `json:"info"`
Health string `json:"health"`
}{
RPC: "/ext/bc/C/rpc",
Websocket: "/ext/bc/C/ws",
Info: "/ext/info",
Health: "/ext/health",
RPC: baseURL + "/bc/C/rpc",
Websocket: baseURL + "/bc/C/ws",
Info: baseURL + "/info",
Health: baseURL + "/health",
},
}
}
@@ -173,10 +173,10 @@ func (r *router) handleRootGET(w http.ResponseWriter, _ *http.Request) {
// handleRootPOST proxies JSON-RPC requests to the C-chain
func (r *router) handleRootPOST(w http.ResponseWriter, req *http.Request) {
// Look up the C-chain RPC handler
handler, err := r.GetHandler("/ext/bc/C", "/rpc")
handler, err := r.GetHandler(baseURL+"/bc/C", "/rpc")
if err != nil {
// Try alternate path formats
handler, err = r.GetHandler("/ext/bc/C/rpc", "")
handler, err = r.GetHandler(baseURL+"/bc/C/rpc", "")
if err != nil {
// Return proper JSON-RPC error
w.Header().Set("Content-Type", "application/json")
@@ -198,10 +198,10 @@ func (r *router) SetRootInfoProvider(provider RootInfoProvider) {
}
// handleHealthz returns a minimal health response for K8s probes.
// This delegates to the full /ext/health handler when available,
// This delegates to the full /v1/health handler when available,
// falling back to a static 200 response during early startup.
func (r *router) handleHealthz(w http.ResponseWriter, req *http.Request) {
if handler, err := r.GetHandler("/ext/health", "/health"); err == nil {
if handler, err := r.GetHandler(baseURL+"/health", "/health"); err == nil {
handler.ServeHTTP(w, req)
return
}
+7 -2
View File
@@ -25,7 +25,12 @@ import (
)
const (
baseURL = "/ext"
// baseURL is the canonical — and only — prefix for every luxd HTTP route
// (/v1/bc/C/rpc, /v1/info, /v1/health, ...). Single source of truth:
// AddRoute/AddAliases and the root/health helpers in router.go all derive
// their paths from it. The legacy Avalanche-heritage /ext prefix is gone;
// one way, no backward compatibility (activation Dec 25 2025).
baseURL = "/v1"
maxConcurrentStreams = 64
)
@@ -95,7 +100,7 @@ type server struct {
listener net.Listener
// handler is the fully-wrapped API handler chain (CORS + host-filter +
// /ext/* router). Held here so the optional ZAP-RPC listener serves the
// /v1/* router). Held here so the optional ZAP-RPC listener serves the
// exact same handler as the HTTP listener.
handler http.Handler
+1 -1
View File
@@ -2,7 +2,7 @@
// See the file LICENSE for licensing terms.
// ZAP-RPC listener — serves the EXACT same fully-wrapped API handler chain
// (CORS + host-filter + /ext/* router) as the HTTP listener, but over the
// (CORS + host-filter + /v1/* router) as the HTTP listener, but over the
// github.com/zap-proto/http binary protocol. This is what makes luxd a
// first-class citizen of the ZAP service mesh: the api.<brand> gateway can
// proxy to luxd over native ZAP instead of HTTP/1.1.
+2 -2
View File
@@ -60,7 +60,7 @@ func TestStartZapRPCListener_RoundTrip(t *testing.T) {
const body = `{"jsonrpc":"2.0","result":"0x2a","id":1}`
mux := http.NewServeMux()
mux.HandleFunc("/ext/bc/C/rpc", func(w http.ResponseWriter, r *http.Request) {
mux.HandleFunc("/v1/bc/C/rpc", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, body)
})
@@ -75,7 +75,7 @@ func TestStartZapRPCListener_RoundTrip(t *testing.T) {
time.Sleep(150 * time.Millisecond)
client := &http.Client{Transport: zaphttp.NewTransport(addr)}
resp, err := client.Post("http://"+addr+"/ext/bc/C/rpc", "application/json", nil)
resp, err := client.Post("http://"+addr+"/v1/bc/C/rpc", "application/json", nil)
if err != nil {
t.Fatalf("ZAP round-trip POST failed: %v", err)
}
+12 -12
View File
@@ -143,7 +143,7 @@ func TestRevokeToken(t *testing.T) {
auth := NewFromHash(log.NewNoOpLogger(), "auth", hashedPassword).(*auth)
// Make a token
endpoints := []string{"/ext/info", "/ext/bc/X", "/ext/metrics"}
endpoints := []string{"/v1/info", "/v1/bc/X", "/v1/metrics"}
tokenStr, err := auth.NewToken(testPassword, defaultTokenLifespan, endpoints)
require.NoError(err)
@@ -157,7 +157,7 @@ func TestWrapHandlerHappyPath(t *testing.T) {
auth := NewFromHash(log.NewNoOpLogger(), "auth", hashedPassword)
// Make a token
endpoints := []string{"/ext/info", "/ext/bc/X", "/ext/metrics"}
endpoints := []string{"/v1/info", "/v1/bc/X", "/v1/metrics"}
tokenStr, err := auth.NewToken(testPassword, defaultTokenLifespan, endpoints)
require.NoError(err)
@@ -178,7 +178,7 @@ func TestWrapHandlerRevokedToken(t *testing.T) {
auth := NewFromHash(log.NewNoOpLogger(), "auth", hashedPassword)
// Make a token
endpoints := []string{"/ext/info", "/ext/bc/X", "/ext/metrics"}
endpoints := []string{"/v1/info", "/v1/bc/X", "/v1/metrics"}
tokenStr, err := auth.NewToken(testPassword, defaultTokenLifespan, endpoints)
require.NoError(err)
@@ -205,7 +205,7 @@ func TestWrapHandlerExpiredToken(t *testing.T) {
auth.clock.Set(time.Now().Add(-2 * defaultTokenLifespan))
// Make a token that expired well in the past
endpoints := []string{"/ext/info", "/ext/bc/X", "/ext/metrics"}
endpoints := []string{"/v1/info", "/v1/bc/X", "/v1/metrics"}
tokenStr, err := auth.NewToken(testPassword, defaultTokenLifespan, endpoints)
require.NoError(err)
@@ -227,7 +227,7 @@ func TestWrapHandlerNoAuthToken(t *testing.T) {
auth := NewFromHash(log.NewNoOpLogger(), "auth", hashedPassword)
endpoints := []string{"/ext/info", "/ext/bc/X", "/ext/metrics"}
endpoints := []string{"/v1/info", "/v1/bc/X", "/v1/metrics"}
wrappedHandler := auth.WrapHandler(dummyHandler)
for _, endpoint := range endpoints {
req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://127.0.0.1:9630%s", endpoint), strings.NewReader(""))
@@ -245,11 +245,11 @@ func TestWrapHandlerUnauthorizedEndpoint(t *testing.T) {
auth := NewFromHash(log.NewNoOpLogger(), "auth", hashedPassword)
// Make a token
endpoints := []string{"/ext/info"}
endpoints := []string{"/v1/info"}
tokenStr, err := auth.NewToken(testPassword, defaultTokenLifespan, endpoints)
require.NoError(err)
unauthorizedEndpoints := []string{"/ext/bc/X", "/ext/metrics", "", "/foo", "/ext/info/foo"}
unauthorizedEndpoints := []string{"/v1/bc/X", "/v1/metrics", "", "/foo", "/v1/info/foo"}
wrappedHandler := auth.WrapHandler(dummyHandler)
for _, endpoint := range unauthorizedEndpoints {
@@ -269,12 +269,12 @@ func TestWrapHandlerAuthEndpoint(t *testing.T) {
auth := NewFromHash(log.NewNoOpLogger(), "auth", hashedPassword)
// Make a token
endpoints := []string{"/ext/info", "/ext/bc/X", "/ext/metrics", "", "/foo", "/ext/info/foo"}
endpoints := []string{"/v1/info", "/v1/bc/X", "/v1/metrics", "", "/foo", "/v1/info/foo"}
tokenStr, err := auth.NewToken(testPassword, defaultTokenLifespan, endpoints)
require.NoError(err)
wrappedHandler := auth.WrapHandler(dummyHandler)
req := httptest.NewRequest(http.MethodPost, "http://127.0.0.1:9630/ext/auth", strings.NewReader(""))
req := httptest.NewRequest(http.MethodPost, "http://127.0.0.1:9630/v1/auth", strings.NewReader(""))
req.Header.Add("Authorization", headerValStart+tokenStr)
rr := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rr, req)
@@ -287,7 +287,7 @@ func TestWrapHandlerAccessAll(t *testing.T) {
auth := NewFromHash(log.NewNoOpLogger(), "auth", hashedPassword)
// Make a token that allows access to all endpoints
endpoints := []string{"/ext/info", "/ext/bc/X", "/ext/metrics", "", "/foo", "/ext/foo/info"}
endpoints := []string{"/v1/info", "/v1/bc/X", "/v1/metrics", "", "/foo", "/v1/foo/info"}
tokenStr, err := auth.NewToken(testPassword, defaultTokenLifespan, []string{"*"})
require.NoError(err)
@@ -316,7 +316,7 @@ func TestWrapHandlerMutatedRevokedToken(t *testing.T) {
auth := NewFromHash(log.NewNoOpLogger(), "auth", hashedPassword)
// Make a token
endpoints := []string{"/ext/info", "/ext/bc/X", "/ext/metrics"}
endpoints := []string{"/v1/info", "/v1/bc/X", "/v1/metrics"}
tokenStr, err := auth.NewToken(testPassword, defaultTokenLifespan, endpoints)
require.NoError(err)
@@ -339,7 +339,7 @@ func TestWrapHandlerInvalidSigningMethod(t *testing.T) {
auth := NewFromHash(log.NewNoOpLogger(), "auth", hashedPassword).(*auth)
// Make a token
endpoints := []string{"/ext/info", "/ext/bc/X", "/ext/metrics"}
endpoints := []string{"/v1/info", "/v1/bc/X", "/v1/metrics"}
idBytes := [tokenIDByteLen]byte{}
_, err := rand.Read(idBytes[:])
require.NoError(err)
+1 -1
View File
@@ -22,7 +22,7 @@ type Password struct {
type NewTokenArgs struct {
Password
// Endpoints that may be accessed with this token e.g. if endpoints is
// ["/ext/bc/X", "/ext/admin"] then the token holder can hit the X-Chain API
// ["/v1/bc/X", "/v1/admin"] then the token holder can hit the X-Chain API
// and the admin API. If [Endpoints] contains an element "*" then the token
// allows access to all API endpoints. [Endpoints] must have between 1 and
// [maxEndpoints] elements
+9 -9
View File
@@ -23,7 +23,7 @@ To get an HTTP status code response that indicates the node's health, make a `GE
To filter GET health checks, add a `tag` query parameter to the request. The `tag` parameter is a string. For example, to filter health results by netID `29uVeLPJB1eQJkzRemU8g8wZDw5uJRqpab5U2mX9euieVwiEbL`, use the following query:
```sh
curl 'http://localhost:9630/ext/health?tag=29uVeLPJB1eQJkzRemU8g8wZDw5uJRqpab5U2mX9euieVwiEbL'
curl 'http://localhost:9630/v1/health?tag=29uVeLPJB1eQJkzRemU8g8wZDw5uJRqpab5U2mX9euieVwiEbL'
```
In this example returned results will contain global health checks and health checks that are related to netID `29uVeLPJB1eQJkzRemU8g8wZDw5uJRqpab5U2mX9euieVwiEbL`.
@@ -33,7 +33,7 @@ In this example returned results will contain global health checks and health ch
In order to filter results by multiple tags, use multiple `tag` query parameters. For example, to filter health results by netID `29uVeLPJB1eQJkzRemU8g8wZDw5uJRqpab5U2mX9euieVwiEbL` and `28nrH5T2BMvNrWecFcV3mfccjs6axM1TVyqe79MCv2Mhs8kxiY` use the following query:
```sh
curl 'http://localhost:9630/ext/health?tag=29uVeLPJB1eQJkzRemU8g8wZDw5uJRqpab5U2mX9euieVwiEbL&tag=28nrH5T2BMvNrWecFcV3mfccjs6axM1TVyqe79MCv2Mhs8kxiY'
curl 'http://localhost:9630/v1/health?tag=29uVeLPJB1eQJkzRemU8g8wZDw5uJRqpab5U2mX9euieVwiEbL&tag=28nrH5T2BMvNrWecFcV3mfccjs6axM1TVyqe79MCv2Mhs8kxiY'
```
The returned results will include health checks for both netIDs as well as global health checks.
@@ -42,10 +42,10 @@ The returned results will include health checks for both netIDs as well as globa
The available endpoints for GET requests are:
- `/ext/health` returns a holistic report of the status of the node. **Most operators should monitor this status.**
- `/ext/health/health` is the same as `/ext/health`.
- `/ext/health/readiness` returns healthy once the node has finished initializing.
- `/ext/health/liveness` returns healthy once the endpoint is available.
- `/v1/health` returns a holistic report of the status of the node. **Most operators should monitor this status.**
- `/v1/health/health` is the same as `/v1/health`.
- `/v1/health/readiness` returns healthy once the node has finished initializing.
- `/v1/health/liveness` returns healthy once the endpoint is available.
## JSON RPC Request
@@ -71,7 +71,7 @@ curl -H 'Content-Type: application/json' --data '{
"params": {
"tags": ["11111111111111111111111111111111LpoYY", "29uVeLPJB1eQJkzRemU8g8wZDw5uJRqpab5U2mX9euieVwiEbL"]
}
}' 'http://localhost:9630/ext/health'
}' 'http://localhost:9630/v1/health'
```
**Example Response**:
@@ -203,7 +203,7 @@ curl -H 'Content-Type: application/json' --data '{
"params": {
"tags": ["11111111111111111111111111111111LpoYY", "29uVeLPJB1eQJkzRemU8g8wZDw5uJRqpab5U2mX9euieVwiEbL"]
}
}' 'http://localhost:9630/ext/health'
}' 'http://localhost:9630/v1/health'
```
**Example Response**:
@@ -249,7 +249,7 @@ curl -H 'Content-Type: application/json' --data '{
"jsonrpc":"2.0",
"id" :1,
"method" :"health.liveness"
}' 'http://localhost:9630/ext/health'
}' 'http://localhost:9630/v1/health'
```
**Example Response**:
+15 -15
View File
@@ -7,7 +7,7 @@ This API uses the `json 2.0` RPC format. For more information on making JSON RPC
## Endpoint
```
/ext/info
/v1/info
```
## Methods
@@ -38,7 +38,7 @@ curl -sX POST --data '{
"id" :1,
"method" :"info.lps",
"params" :{}
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/info
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/info
```
**Example Response**:
@@ -125,7 +125,7 @@ curl -X POST --data '{
"params": {
"chain":"X"
}
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/info
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/info
```
**Example Response**:
@@ -160,7 +160,7 @@ curl -X POST --data '{
"params": {
"alias":"X"
}
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/info
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/info
```
**Example Response**:
@@ -192,7 +192,7 @@ curl -X POST --data '{
"jsonrpc":"2.0",
"id" :1,
"method" :"info.getNetworkID"
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/info
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/info
```
**Example Response**:
@@ -226,7 +226,7 @@ curl -X POST --data '{
"jsonrpc":"2.0",
"id" :1,
"method" :"info.getNetworkName"
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/info
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/info
```
**Example Response**:
@@ -273,7 +273,7 @@ curl -X POST --data '{
"jsonrpc":"2.0",
"id" :1,
"method" :"info.getNodeID"
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/info
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/info
```
**Example Response**:
@@ -313,7 +313,7 @@ curl -X POST --data '{
"jsonrpc":"2.0",
"id" :1,
"method" :"info.getNodeIP"
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/info
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/info
```
**Example Response**:
@@ -359,7 +359,7 @@ curl -X POST --data '{
"jsonrpc":"2.0",
"id" :1,
"method" :"info.getNodeVersion"
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/info
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/info
```
**Example Response**:
@@ -426,7 +426,7 @@ curl -X POST --data '{
"jsonrpc":"2.0",
"id" :1,
"method" :"info.getTxFee"
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/info
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/info
```
**Example Response**:
@@ -473,7 +473,7 @@ curl -X POST --data '{
"id" :1,
"method" :"info.getVMs",
"params" :{}
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/info
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/info
```
**Example Response**:
@@ -540,7 +540,7 @@ curl -X POST --data '{
"params": {
"nodeIDs": []
}
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/info
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/info
```
**Example Response**:
@@ -619,7 +619,7 @@ curl -X POST --data '{
"jsonrpc":"2.0",
"id" :1,
"method" :"info.uptime"
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/info
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/info
```
**Example Response**:
@@ -645,7 +645,7 @@ curl -X POST --data '{
"params" :{
"netID":"29uVeLPJB1eQJkzRemU8g8wZDw5uJRqpab5U2mX9euieVwiEbL"
}
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/info
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/info
```
#### Example Lux L1 Response
@@ -672,7 +672,7 @@ curl -X POST --data '{
"jsonrpc":"2.0",
"id" :1,
"method" :"info.upgrades"
}' -H 'content-type:application/json;' 127.0.0.1:9650/ext/info
}' -H 'content-type:application/json;' 127.0.0.1:9650/v1/info
```
**Example Response**:
+4 -4
View File
@@ -64,7 +64,7 @@ func TestGetNodeVersionConsensusRoundtrip(t *testing.T) {
log: log.NewNoOpLogger(),
}
req := httptest.NewRequest("POST", "/ext/info", nil)
req := httptest.NewRequest("POST", "/v1/info", nil)
reply := apiinfo.GetNodeVersionReply{}
require.NoError(info.GetNodeVersion(req, nil, &reply))
require.NotNil(reply.Consensus)
@@ -114,7 +114,7 @@ func TestGetVMsSuccess(t *testing.T) {
id2: []string{alias2},
}
req := httptest.NewRequest("POST", "/ext/info", nil)
req := httptest.NewRequest("POST", "/v1/info", nil)
resources.mockVMManager.EXPECT().ListFactories(req.Context()).Times(1).Return(vmIDs, nil)
resources.mockVMManager.EXPECT().PrimaryAlias(req.Context(), id1).Times(1).Return(alias1, nil)
resources.mockVMManager.EXPECT().PrimaryAlias(req.Context(), id2).Times(1).Return(alias2, nil)
@@ -128,7 +128,7 @@ func TestGetVMsSuccess(t *testing.T) {
func TestGetVMsVMsListFactoriesFails(t *testing.T) {
resources := initGetVMsTest(t)
req := httptest.NewRequest("POST", "/ext/info", nil)
req := httptest.NewRequest("POST", "/v1/info", nil)
resources.mockVMManager.EXPECT().ListFactories(req.Context()).Times(1).Return(nil, errTest)
reply := apiinfo.GetVMsReply{}
@@ -146,7 +146,7 @@ func TestGetVMsGetAliasesFails(t *testing.T) {
vmIDs := []ids.ID{id1, id2}
alias1 := "vm1-alias-1"
req := httptest.NewRequest("POST", "/ext/info", nil)
req := httptest.NewRequest("POST", "/v1/info", nil)
resources.mockVMManager.EXPECT().ListFactories(req.Context()).Times(1).Return(vmIDs, nil)
resources.mockVMManager.EXPECT().PrimaryAlias(req.Context(), id1).Times(1).Return(alias1, nil)
resources.mockVMManager.EXPECT().PrimaryAlias(req.Context(), id2).Times(1).Return("", errTest)
+1 -1
View File
@@ -38,7 +38,7 @@ type client struct {
// instead.
func NewClient(uri string) Client {
return &client{requester: rpc.NewEndpointRequester(
uri + "/ext/keystore",
uri + "/v1/keystore",
)}
}
+6 -6
View File
@@ -48,7 +48,7 @@ This API uses the `json 2.0` API format. For more information on making JSON RPC
## Endpoint
```text
/ext/keystore
/v1/keystore
```
## Methods
@@ -89,7 +89,7 @@ curl -X POST --data '{
"username":"myUsername",
"password":"myPassword"
}
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/keystore
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/keystore
```
**Example Response:**
@@ -129,7 +129,7 @@ curl -X POST --data '{
"username":"myUsername",
"password":"myPassword"
}
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/keystore
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/keystore
```
**Example Response:**
@@ -183,7 +183,7 @@ curl -X POST --data '{
"username":"myUsername",
"password":"myPassword"
}
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/keystore
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/keystore
```
**Example Response:**
@@ -238,7 +238,7 @@ curl -X POST --data '{
"password":"myPassword",
"user" :"0x7655a29df6fc2747b0874e1148b423b954a25fcdb1f170d0ec8eb196430f7001942ce55b02a83b1faf50a674b1e55bfc000000008cf2d869"
}
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/keystore
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/keystore
```
**Example Response:**
@@ -274,7 +274,7 @@ curl -X POST --data '{
"jsonrpc":"2.0",
"id" :1,
"method" :"keystore.listUsers"
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/keystore
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/keystore
```
**Example Response:**
+1 -1
View File
@@ -32,7 +32,7 @@ type Client struct {
// NewClient returns a new Metrics API Client
func NewClient(uri string) *Client {
return &Client{
uri: uri + "/ext/metrics",
uri: uri + "/v1/metrics",
}
}
+2 -2
View File
@@ -7,7 +7,7 @@ This API set is for a specific node, it is unavailable on the [public server](ht
## Endpoint
```
/ext/metrics
/v1/metrics
```
## Usage
@@ -15,7 +15,7 @@ This API set is for a specific node, it is unavailable on the [public server](ht
To get the node metrics:
```sh
curl -X POST 127.0.0.1:9630/ext/metrics
curl -X POST 127.0.0.1:9630/v1/metrics
```
## Format
+9 -9
View File
@@ -27,7 +27,7 @@ var ErrNoProfile = errors.New(
"(genesis carries no SecurityProfile{} block); RPC unavailable")
// Service is the JSON-RPC handler set registered under the security
// namespace at /ext/security. Methods are read-only; the underlying
// namespace at /v1/security. Methods are read-only; the underlying
// profile pointer is set once at construction and never mutated.
//
// Exposed methods:
@@ -37,7 +37,7 @@ var ErrNoProfile = errors.New(
//
// On the wire, gorilla/rpc dispatches these as security_securityProfile
// and security_blockSecurity (namespace_method). Callers using the REST
// sidecars hit /ext/security/profile and /ext/security/block/{n}
// sidecars hit /v1/security/profile and /v1/security/block/{n}
// directly. One namespace, two transports, one shape.
//
// The "block" method returns the chain-wide envelope; per-block
@@ -53,13 +53,13 @@ type Service struct {
// namespace. Profile may be nil — see ErrNoProfile.
//
// The returned handler is suitable for APIServer.AddRoute(handler,
// "security", "") so it lands at /ext/security on the node's HTTP
// "security", "") so it lands at /v1/security on the node's HTTP
// listener. REST sidecars are mounted at /profile and /block/{n} on
// the same handler so the full external surface is:
//
// POST /ext/security (JSON-RPC, methods above)
// GET /ext/security/profile (REST sidecar)
// GET /ext/security/block/{n} (REST sidecar)
// POST /v1/security (JSON-RPC, methods above)
// GET /v1/security/profile (REST sidecar)
// GET /v1/security/block/{n} (REST sidecar)
func NewHandler(logger log.Logger, profile *consensusconfig.ChainSecurityProfile) (http.Handler, error) {
server := rpc.NewServer()
codec := avajson.NewCodec()
@@ -71,7 +71,7 @@ func NewHandler(logger log.Logger, profile *consensusconfig.ChainSecurityProfile
}
// REST sidecars share the Service receiver so the two transports
// stay byte-identical in semantics (one and only one way to
// compute the shape). Paths relative to /ext/security:
// compute the shape). Paths relative to /v1/security:
// /profile → restProfile
// /block/{n} → restBlockSecurity
mux := http.NewServeMux()
@@ -133,7 +133,7 @@ func (s *Service) BlockSecurity(_ *http.Request, _ *BlockSecurityArgs, reply *Bl
return nil
}
// restProfile is the GET /profile sidecar (full path /ext/security/profile).
// restProfile is the GET /profile sidecar (full path /v1/security/profile).
// Same body as the securityProfile JSON-RPC method; useful for
// explorers that don't speak JSON-RPC. Refuses every non-GET method so
// callers can't smuggle state through the read-only endpoint.
@@ -151,7 +151,7 @@ func (s *Service) restProfile(w http.ResponseWriter, r *http.Request) {
}
// restBlockSecurity is the GET /block/{n} sidecar (full path
// /ext/security/block/{n}). The path suffix is taken as the block
// /v1/security/block/{n}). The path suffix is taken as the block
// number; chain alias defaults to the platform chain (callers can
// re-route per-chain at the chain-manager layer if needed).
func (s *Service) restBlockSecurity(w http.ResponseWriter, r *http.Request) {
+2 -2
View File
@@ -203,7 +203,7 @@ func TestRPC_blockSecurity_StrictPQ(t *testing.T) {
}
// TestREST_securityProfile_GET proves the /profile sidecar (full path
// /ext/security/profile when mounted on APIServer) returns the same
// /v1/security/profile when mounted on APIServer) returns the same
// JSON shape as the JSON-RPC handler. One shape, two transports — no
// per-transport drift.
func TestREST_securityProfile_GET(t *testing.T) {
@@ -262,7 +262,7 @@ func TestREST_securityProfile_MethodNotAllowed(t *testing.T) {
}
// TestREST_blockSecurity_GET proves the /block/{n} sidecar (full path
// /ext/security/block/{n} when mounted on APIServer) returns the same
// /v1/security/block/{n} when mounted on APIServer) returns the same
// JSON shape as the JSON-RPC handler. One shape, two transports — no
// per-transport drift.
func TestREST_blockSecurity_GET(t *testing.T) {
+5 -5
View File
@@ -3,8 +3,8 @@
// Package security exposes the chain-wide ChainSecurityProfile to operators,
// dApps, and auditors through the security JSON-RPC namespace at
// /ext/security and two REST sidecars at /ext/security/profile and
// /ext/security/block/{n}.
// /v1/security and two REST sidecars at /v1/security/profile and
// /v1/security/block/{n}.
//
// The handlers in this package are read-only: every shape is derived from
// the immutable *consensusconfig.ChainSecurityProfile resolved at node
@@ -22,7 +22,7 @@ import (
)
// ProfileReply is the JSON body returned by the securityProfile RPC
// (POST /ext/security) and the REST sidecar (GET /ext/security/profile).
// (POST /v1/security) and the REST sidecar (GET /v1/security/profile).
//
// Stable shape: every field has a fixed JSON tag, no embedded structs.
// Adding a new field requires bumping the major version of the security
@@ -99,8 +99,8 @@ type ProfileReply struct {
}
// BlockSecurityReply is the JSON body returned by the blockSecurity
// RPC (POST /ext/security) and the REST endpoint
// /ext/security/block/{n}. It enriches a block lookup with the
// RPC (POST /v1/security) and the REST endpoint
// /v1/security/block/{n}. It enriches a block lookup with the
// chain-wide security envelope so explorers can show "this block was
// finalised under profile X with backend Y" without reimplementing
// profile lookup.
+1 -1
View File
@@ -31,7 +31,7 @@ type SimpleNodesMetrics map[string]SimpleNodeMetrics
// GetSimpleNodeMetrics retrieves the specified metrics the provided node URI.
func GetSimpleNodeMetrics(nodeURI string, metricNames ...string) (SimpleNodeMetrics, error) {
uri := nodeURI + "/ext/metrics"
uri := nodeURI + "/v1/metrics"
return GetMetricsValue(uri, metricNames...)
}
+1 -1
View File
@@ -1 +1 @@
1.22.79
1.32.11
+7 -1
View File
@@ -63,7 +63,13 @@
"v1.30.3",
"v1.30.4",
"v1.30.5",
"v1.30.6"
"v1.30.6",
"v1.32.11",
"v1.36.3",
"v1.36.4",
"v1.36.5",
"v1.36.6",
"v1.36.7"
],
"41": [
"v1.13.2"
+2 -2
View File
@@ -76,8 +76,8 @@ var (
// These should match the latest git tag
const (
defaultMajor = 1
defaultMinor = 30
defaultPatch = 6
defaultMinor = 36
defaultPatch = 7
)
func init() {
@@ -71,16 +71,16 @@ const (
// holding the POST-transition field values. Its fields are hashed, in this exact
// order, into the leaf preimage:
//
// SignerID ‖ LuxAddress[20] ‖ 0u32 ‖ BondLo ‖ BondHi ‖ OptInHeight ‖
// SignerID ‖ UTXOAddr[20] ‖ 0u32 ‖ BondLo ‖ BondHi ‖ OptInHeight ‖
// ExitEpoch ‖ SignCount ‖ BLSPubkey[48] ‖ CoronaPubkey[32] ‖ MLDSAPubkey[32] ‖
// Status ‖ JailUntilEpoch ‖ SlashCount ‖ index (integers little-endian)
//
// A signer with Occupied == 0 is skipped (not folded), exactly as the kernel
// skips unoccupied slots. The 0u32 after LuxAddress is the GPU struct's
// skips unoccupied slots. The 0u32 after UTXOAddr is the GPU struct's
// _pad_addr, committed as four zero bytes.
type SignerLeaf struct {
SignerID uint64
LuxAddress [20]byte
UTXOAddr [20]byte
BondLo uint64
BondHi uint64
OptInHeight uint64
@@ -175,7 +175,7 @@ func le64(b []byte, v uint64) []byte {
func signerLeafDigest(s SignerLeaf, i uint32) [Size]byte {
b := make([]byte, 0, 8+20+4+8+8+8+8+8+48+32+32+4+4+4+4)
b = le64(b, s.SignerID)
b = append(b, s.LuxAddress[:]...)
b = append(b, s.UTXOAddr[:]...)
b = le32(b, 0) // _pad_addr
b = le64(b, s.BondLo)
b = le64(b, s.BondHi)
@@ -106,7 +106,7 @@ func katMixedSigners() []SignerLeaf {
s.Occupied = 1
s.SignerID = uint64(i + 1)
for k := 0; k < 20; k++ {
s.LuxAddress[k] = byte(i + k)
s.UTXOAddr[k] = byte(i + k)
}
for k := 0; k < 48; k++ {
s.BLSPubkey[k] = byte(0x10 + k)
@@ -281,7 +281,7 @@ func katDenseSigners(n uint32) []SignerLeaf {
s.Occupied = 1
s.SignerID = uint64(i + 1)
for k := 0; k < 20; k++ {
s.LuxAddress[k] = byte(0x40 + int(i) + k)
s.UTXOAddr[k] = byte(0x40 + int(i) + k)
}
for k := 0; k < 48; k++ {
s.BLSPubkey[k] = byte(0x11 + int(i) + k)
+3 -3
View File
@@ -16,7 +16,7 @@ import (
"github.com/luxfi/database/prefixdb"
"github.com/luxfi/ids"
"github.com/luxfi/math/set"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/utils/wrappers"
lux "github.com/luxfi/utxo"
)
@@ -143,7 +143,7 @@ func (i *indexer) Accept(txID ids.ID, inputUTXOs []*lux.UTXO, outputUTXOs []*lux
idx = binary.BigEndian.Uint64(idxBytes)
case database.ErrNotFound:
// idx not found; this must be the first entry.
idxBytes = make([]byte, pcodecs.LongLen)
idxBytes = make([]byte, wrappers.LongLen)
default:
// Unexpected error
return fmt.Errorf("unexpected error when indexing txID %s: %w", txID, err)
@@ -184,7 +184,7 @@ func (i *indexer) Read(address []byte, assetID ids.ID, cursor, pageSize uint64)
assetPrefixDB := prefixdb.New(assetID[:], addressTxDB)
// get cursor in bytes
cursorBytes := make([]byte, pcodecs.LongLen)
cursorBytes := make([]byte, wrappers.LongLen)
binary.BigEndian.PutUint64(cursorBytes, cursor)
// start reading from the cursor bytes, numeric keys maintain the order (see Accept)
-32
View File
@@ -1,32 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package keystore
import (
"errors"
"github.com/luxfi/node/vms/pcodecs"
)
const CodecVersion = 0
var (
Codec pcodecs.Manager
LegacyCodec pcodecs.Manager
)
func init() {
c := pcodecs.NewLinearCodec()
Codec = pcodecs.NewDefaultManager()
lc := pcodecs.NewLinearCodec()
LegacyCodec = pcodecs.NewMaxInt32Manager()
err := errors.Join(
Codec.RegisterCodec(CodecVersion, c),
LegacyCodec.RegisterCodec(CodecVersion, lc),
)
if err != nil {
panic(err)
}
}
+29 -8
View File
@@ -71,9 +71,34 @@ func (u *user) GetAddresses() ([]ids.ShortID, error) {
return nil, err
}
var addresses []ids.ShortID
_, err = LegacyCodec.Unmarshal(addressBytes, &addresses)
return addresses, err
return parseAddresses(addressBytes)
}
// marshalAddresses encodes the user's controlled addresses as the flat
// concatenation of their 20-byte values. ids.ShortID is fixed-width, so the
// count is implied by len/ShortIDLen — no length prefix or codec is needed.
func marshalAddresses(addresses []ids.ShortID) []byte {
b := make([]byte, 0, len(addresses)*ids.ShortIDLen)
for i := range addresses {
b = append(b, addresses[i][:]...)
}
return b
}
// parseAddresses is the inverse of marshalAddresses.
func parseAddresses(b []byte) ([]ids.ShortID, error) {
if len(b)%ids.ShortIDLen != 0 {
return nil, fmt.Errorf("keystore: address blob length %d is not a multiple of %d", len(b), ids.ShortIDLen)
}
n := len(b) / ids.ShortIDLen
if n == 0 {
return nil, nil
}
addresses := make([]ids.ShortID, n)
for i := 0; i < n; i++ {
copy(addresses[i][:], b[i*ids.ShortIDLen:])
}
return addresses, nil
}
func (u *user) PutKeys(privKeys ...*secp256k1.PrivateKey) error {
@@ -119,11 +144,7 @@ func (u *user) PutKeys(privKeys ...*secp256k1.PrivateKey) error {
addresses = append(addresses, address)
}
addressBytes, err := Codec.Marshal(CodecVersion, addresses)
if err != nil {
return err
}
return u.db.Put(addressesKey, addressBytes)
return u.db.Put(addressesKey, marshalAddresses(addresses))
}
func (u *user) GetKey(address ids.ShortID) (*secp256k1.PrivateKey, error) {
+8 -11
View File
@@ -8,22 +8,20 @@ import (
"github.com/luxfi/ids"
"github.com/luxfi/math/set"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/vm/chains/atomic"
)
var _ AtomicUTXOManager = (*atomicUTXOManager)(nil)
type atomicUTXOManager struct {
sm atomic.SharedMemory
codec pcodecs.Manager
sm atomic.SharedMemory
}
func NewAtomicUTXOManager(sm atomic.SharedMemory, codec pcodecs.Manager) AtomicUTXOManager {
return &atomicUTXOManager{
sm: sm,
codec: codec,
}
// NewAtomicUTXOManager returns an AtomicUTXOManager backed by ZAP-native
// wire bytes in cross-chain shared memory (no codec.Manager). Callers rely
// on this package's init() fx-aware UTXO.Unmarshal dispatch.
func NewAtomicUTXOManager(sm atomic.SharedMemory) AtomicUTXOManager {
return &atomicUTXOManager{sm: sm}
}
func (a *atomicUTXOManager) GetAtomicUTXOs(
@@ -64,7 +62,7 @@ func (a *atomicUTXOManager) GetAtomicUTXOs(
utxos := make([]*UTXO, len(allUTXOBytes))
for i, utxoBytes := range allUTXOBytes {
utxo := &UTXO{}
if _, err := a.codec.Unmarshal(utxoBytes, utxo); err != nil {
if err := utxo.Unmarshal(utxoBytes); err != nil {
return nil, ids.ShortID{}, ids.Empty, fmt.Errorf("error parsing UTXO: %w", err)
}
utxos[i] = utxo
@@ -84,13 +82,12 @@ func (a *atomicUTXOManager) GetAtomicUTXOs(
// * Any error that may have occurred upstream.
func GetAtomicUTXOs(
sharedMemory atomic.SharedMemory,
codec pcodecs.Manager,
chainID ids.ID,
addrs set.Set[ids.ShortID],
startAddr ids.ShortID,
startUTXOID ids.ID,
limit int,
) ([]*UTXO, ids.ShortID, ids.ID, error) {
manager := NewAtomicUTXOManager(sharedMemory, codec)
manager := NewAtomicUTXOManager(sharedMemory)
return manager.GetAtomicUTXOs(chainID, addrs, startAddr, startUTXOID, limit)
}
+7 -6
View File
@@ -8,14 +8,13 @@ import (
"github.com/luxfi/ids"
"github.com/luxfi/math"
"github.com/luxfi/node/vms/pcodecs"
)
var ErrInsufficientFunds = errors.New("insufficient funds")
type FlowChecker struct {
consumed, produced map[ids.ID]uint64
errs pcodecs.Errs
errs []error
}
func NewFlowChecker() *FlowChecker {
@@ -36,18 +35,20 @@ func (fc *FlowChecker) Produce(assetID ids.ID, amount uint64) {
func (fc *FlowChecker) add(value map[ids.ID]uint64, assetID ids.ID, amount uint64) {
var err error
value[assetID], err = math.Add64(value[assetID], amount)
fc.errs.Add(err)
if err != nil {
fc.errs = append(fc.errs, err)
}
}
func (fc *FlowChecker) Verify() error {
if !fc.errs.Errored() {
if len(fc.errs) == 0 {
for assetID, producedAssetAmount := range fc.produced {
consumedAssetAmount := fc.consumed[assetID]
if producedAssetAmount > consumedAssetAmount {
fc.errs.Add(ErrInsufficientFunds)
fc.errs = append(fc.errs, ErrInsufficientFunds)
break
}
}
}
return fc.errs.Err
return errors.Join(fc.errs...)
}

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