Compare commits

..
Author SHA1 Message Date
zeekayandHanzo Dev 6b8b3c8004 gpu-build: wire per-arch lux-gpu into the DexVM node image (opt-in GPU variant)
Deliverable #2 — node carries the DexVM natively + links lux-gpu on GPU variants:
- Confirmed: the D-Chain dexvm plugin (vmID mDVT5EWMumBp3LCqvKwuyZQeY1VXr1jvjGNAt8nL4UFiXvqXr)
  is already built from luxfi/dex ./cmd/dchain, guarded by test -s || FATAL, and
  bundled into the runtime image (plugin group 2). The STANDARD node keeps
  building it pure-Go (CGO_ENABLED=0) → lx.MatchOrderCPU, portable to every arch.
- Added DEXVM_GPU (default 0). DEXVM_GPU=1 fetches the per-arch unified lux-gpu
  (built by lux-private/gpu-kernels liblux-gpu.yml) and builds the plugin with
  CGO_ENABLED=1 so pkg/lx/orderbook_gpu.go links liblux_gpu
  (lux_gpu_dex_match_order + lux_gpu_backend_name) and runtime-selects
  CUDA/HIP/Metal, falling back to MatchOrderCPU. Missing lib is FATAL under
  DEXVM_GPU=1 (no silent CPU downgrade). Runtime stage now COPYs /usr/local/lib
  so the plugin's liblux_gpu.so is resolvable (no-op for the CPU image).
- docker-gpu.yml: builds the GPU variant NATIVELY per-arch on the arcd GPU pools
  (arm64/spark-CUDA, amd64/evo-ROCm) — never cross-compiled, since a native GPU
  lib cannot be cross-linked — and fuses them into :<tag>-gpu. The plain CPU
  multi-arch image (docker.yml) is untouched.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-01 20:08:11 -07:00
491 changed files with 24730 additions and 19594 deletions
+1
View File
@@ -0,0 +1 @@
# CI Status Check - 2025-09-23 23:30:58
+1
View File
@@ -0,0 +1 @@
# Triggering CI - Version 1.13.5 Ready
+34
View File
@@ -0,0 +1,34 @@
name: Lint proto files
on:
push:
paths:
- 'proto/**/*.proto'
- '.github/workflows/buf-lint.yml'
permissions:
contents: read
jobs:
buf-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check for .proto files
id: proto-check
run: |
if find proto -name '*.proto' -type f 2>/dev/null | grep -q .; then
echo "has_proto=true" >> $GITHUB_OUTPUT
else
echo "has_proto=false" >> $GITHUB_OUTPUT
echo "No .proto files found — skipping buf-lint (node is ZAP-native by default; protobuf is opt-in)."
fi
- uses: bufbuild/buf-setup-action@v1
if: steps.proto-check.outputs.has_proto == 'true'
with:
github_token: ${{ github.token }}
version: "1.47.2"
- uses: bufbuild/buf-lint-action@v1
if: steps.proto-check.outputs.has_proto == 'true'
with:
input: "proto"
+1 -1
View File
@@ -9,7 +9,7 @@ on:
jobs:
push:
runs-on: lux-build-amd64
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: bufbuild/buf-setup-action@v1.31.0
+52 -5
View File
@@ -50,7 +50,7 @@ jobs:
TIMEOUT: ${{ env.TIMEOUT }}
CGO_ENABLED: '0'
Fuzz:
runs-on: lux-build-amd64
runs-on: ubuntu-latest
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: lux-build-amd64
runs-on: ubuntu-latest
env:
GOPRIVATE: github.com/luxfi/*
GONOSUMDB: github.com/luxfi/*
@@ -94,9 +94,56 @@ jobs:
- name: Run actionlint
shell: bash
run: scripts/actionlint.sh
buf-lint:
name: Protobuf Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install buf
shell: bash
run: |
BUF_VERSION="1.47.2"
curl -sSL "https://github.com/bufbuild/buf/releases/download/v${BUF_VERSION}/buf-Linux-x86_64" -o /usr/local/bin/buf
chmod +x /usr/local/bin/buf
buf --version
- name: Lint protobuf
shell: bash
run: buf lint proto
check_generated_protobuf:
name: Up-to-date protobuf
runs-on: ubuntu-latest
continue-on-error: true
env:
GOPRIVATE: github.com/luxfi/*
GONOSUMDB: github.com/luxfi/*
GOWORK: off
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-go-for-project
- name: Configure Git for private modules
shell: bash
run: git config --global url."https://${{ github.token }}@github.com/".insteadOf "https://github.com/"
- name: Install buf
shell: bash
run: |
BUF_VERSION="1.47.2"
curl -sSL "https://github.com/bufbuild/buf/releases/download/v${BUF_VERSION}/buf-Linux-x86_64" -o /usr/local/bin/buf
chmod +x /usr/local/bin/buf
- name: Install protoc-gen-go tools
shell: bash
run: |
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.35.1
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.3.0
go install connectrpc.com/connect/cmd/protoc-gen-connect-go@latest
- shell: bash
run: scripts/protobuf_codegen.sh
env:
CGO_ENABLED: '0'
- shell: bash
run: .github/workflows/check-clean-branch.sh
check_mockgen:
name: Up-to-date mocks
runs-on: lux-build-amd64
runs-on: ubuntu-latest
continue-on-error: true
env:
GOPRIVATE: github.com/luxfi/*
@@ -116,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: lux-build-amd64
runs-on: ubuntu-latest
env:
GOPRIVATE: github.com/luxfi/*
GONOSUMDB: github.com/luxfi/*
@@ -133,7 +180,7 @@ jobs:
run: .github/workflows/check-clean-branch.sh
test_build_image:
name: Image build
runs-on: lux-build-amd64
runs-on: ubuntu-latest
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: lux-build-amd64
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
+127
View File
@@ -0,0 +1,127 @@
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: lux-build-amd64
runs-on: ubuntu-latest
env:
GOPRIVATE: github.com/luxfi/*
GONOSUMDB: github.com/luxfi/*
+1 -1
View File
@@ -11,7 +11,7 @@ permissions:
jobs:
MerkleDB:
runs-on: lux-build-amd64
runs-on: ubuntu-latest
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: lux-build-amd64
runs-on: ubuntu-latest
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: lux-build-amd64
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v10
with:
+2 -2
View File
@@ -15,7 +15,7 @@ env:
jobs:
test-zapdb-replay:
runs-on: lux-build-amd64
runs-on: ubuntu-latest
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: lux-build-amd64
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
-16
View File
@@ -5,22 +5,6 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.36.31]
### Fixed
- **`/v1/health` reported a chain the node denies exists.** The `bootstrapped` check publishes `chains.Nets.Bootstrapping()` verbatim as its message, but `Nets.chains` is keyed by **net** ID and the aggregate appended the map **key**, not the chain. Every primary-network chain that failed to converge therefore surfaced as the single ID `11111111111111111111111111111111LpoYY``constants.PrimaryNetworkID` (`ids.Empty`) — so an operator resolving it got "there is no chain with alias/ID", and N stuck chains collapsed into one indistinguishable entry (measured on devnet/testnet: `"message":["11111111111111111111111111111111LpoYY"],"contiguousFailures":3213`). The net owns the bootstrapping set, so the net now names its own chains: new `nets.Net.Bootstrapping() []ids.ID` (`nets/net.go`), and `chains/chains.go` aggregates those instead of the keys. `TestNetsBootstrappingReportsChainsNotNets` asserts `ids.Empty` never appears and that two stuck chains are individually named; `TestNetsBootstrapping` no longer asserts the bug (it demanded the net ID).
- Ships the GET `/v1/health` encoder fix from `dada5a31` (`{"healthy":…,"error":"health reply encode failed"}` on every node — jsonv2 has no representation for `apihealth.Result.Duration`), which was on `main` but had never been tagged.
## [1.36.13]
### Fixed
- **The durable rejoin fix (#66/#74) shipped INERT for the C-Chain — the exact chain whose 40h mainnet freeze motivated it.** `chains/manager.go` gated `expectsStakedBeacons` on `ids.IsNativeChain(chainParams.ID)` — the *blockchain* ID. But `ids.IsNativeChain` only matches the symbolic `111…C` alias form, and every deployed C/X/Q carries a **hash** blockchain ID (devnet C `21HieZng…`, mainnet C `2wRdZG…`); only the P-chain has a symbolic blockchain ID (`111…P`), and it is excluded as the platform chain. So `isNativeChain` was **always false** for the real C-Chain → `expectsStakedBeacons=false` → under the production `--skip-bootstrap=true` the beacon set was emptied → a behind C-Chain named its STALE local tip the frontier and never caught up (verified on devnet v1.36.12: C-Chain wedged at height 0 with "using empty beacons for single-node mode"). Fix: discriminate on the **validating Net** (`chainParams.ChainID == constants.PrimaryNetworkID`) via new `chainValidatesOnPrimaryNetwork`, which is `PrimaryNetworkID` for C/X/Q and each sovereign L1's own net ID for an L2 — so C/X/Q now keep their staked beacons under `--skip-bootstrap` (peer-sync a behind validator) while L2s keep the empty-beacon single-node path. Regression test `TestChainValidatesOnPrimaryNetwork_RealHashChainID` exercises the real discriminator with hash chain IDs (the prior hardcoded-bool tests could not catch this); `TestRED_EmptyStakedSetFailsSafe` still passes (forged-frontier gate intact).
## [1.36.12]
### Fixed
- **EVM chains (C/D/L2) failed to initialize on v1.36.11 — bundled VM plugins were built against a stale `luxfi/api`.** The node pins `luxfi/api v1.0.16`, whose `InitializeResponse` carries the appended `Capabilities uint64` field (Quasar-export handshake, api commit `1f2dc5a`). The image baked the C-Chain EVM plugin from `luxfi/evm@v1.104.8` and the D-Chain dexvm plugin from `luxfi/dex@v1.5.15`, both of which resolve `luxfi/api v1.0.15` (no `Capabilities`). Their `InitializeResponse.Encode` writes a shorter payload than the node's strict `InitializeResponse.Decode` expects, so `vms/rpcchainvm/zap/client.go` fails every EVM VM handshake with `zap decode initialize response: unexpected EOF`. Native VMs (P/X/Q) were unaffected. Fix: bump `EVM_VERSION` `v1.104.8 → v1.104.9` (api v1.0.16) and force `luxfi/api@v1.0.16` in the dexvm build stage. The api bump is code-free for plugins (`luxfi/chains v1.7.4 → v1.7.5` adopted it with a go.mod-only diff; `CHAINS_REF=v1.7.6` already carries v1.0.16). No node source change beyond the version bump — this is v1.36.11's node binary (durable rejoin fix, commit `63f61429d1`) rebuilt with plugin↔node ZAP-wire alignment.
## [1.28.0]
### Added
+77 -82
View File
@@ -124,27 +124,22 @@ RUN --mount=type=secret,id=ghtok,required=false \
# linked into the C-Chain plugin via github.com/luxfi/chains/evm/cevm
# and become the default execution backend (parallel + GPU EVM).
#
# The release assets live in the PRIVATE lux-private/cevm repo, so the fetch is
# authenticated with the same `ghtok` secret as the private go modules and
# lux-accel above. Best-effort, like lux-accel: only a build that asks for the
# cevm backend (EVM_CGO=1 below) actually needs these.
# CI/RELEASE GAP: the luxcpp/cevm release artifacts MUST publish per-arch
# tarballs at the URL below for both linux-x86_64 and linux-arm64. Until
# those tarballs exist, builds with CGO_ENABLED=1 will fail at this step and
# operators must build with CGO_ENABLED=0 (pure-Go fallback).
ARG CGO_ENABLED=1
ARG CEVM_VERSION=v0.51.10
ARG CEVM_REPO=lux-private/cevm
RUN --mount=type=secret,id=ghtok,required=false \
if [ "${CGO_ENABLED}" = "1" ]; then \
ARG CEVM_VERSION=v0.19.0
RUN if [ "${CGO_ENABLED}" = "1" ]; then \
ARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) && \
if [ "$ARCH" = "amd64" ]; then CEVM_ARCH="linux-x86_64"; else CEVM_ARCH="linux-arm64"; fi && \
AUTH=""; [ -s /run/secrets/ghtok ] && AUTH="--header=Authorization: Bearer $(cat /run/secrets/ghtok)"; \
( wget -q ${AUTH:+"$AUTH"} \
"https://github.com/${CEVM_REPO}/releases/download/${CEVM_VERSION}/luxcpp-cevm-${CEVM_ARCH}.tar.gz" \
-O /tmp/cevm.tar.gz \
&& tar -xzf /tmp/cevm.tar.gz -C /usr/local \
&& rm /tmp/cevm.tar.gz \
&& ldconfig 2>/dev/null \
) || echo "WARN: cevm ${CEVM_VERSION} fetch skipped (unreachable; needed only when EVM_CGO=1)" ; \
wget -q "https://github.com/luxcpp/cevm/releases/download/${CEVM_VERSION}/luxcpp-cevm-${CEVM_ARCH}.tar.gz" \
-O /tmp/cevm.tar.gz && \
tar -xzf /tmp/cevm.tar.gz -C /usr/local && \
rm /tmp/cevm.tar.gz && \
ldconfig 2>/dev/null || true ; \
else \
echo "CGO_ENABLED=0: skipping cevm fetch (pure-Go fallback build)" ; \
echo "CGO_ENABLED=0: skipping luxcpp/cevm fetch (pure-Go fallback build)" ; \
fi
# Build node. CGO_ENABLED=1 (default) links luxcpp/cevm for parallel + GPU EVM.
@@ -262,35 +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.
# v1.104.9 bumps luxfi/api v1.0.15 -> v1.0.16 (indirect via luxfi/vm). v1.0.16
# APPENDED InitializeResponse.Capabilities (uint64) for the Quasar-export
# handshake (api commit 1f2dc5a). The node pins api v1.0.16 and DECODES that
# field; an EVM plugin built at v1.104.8 (api v1.0.15) omits it, so the node's
# strict struct decode hits "zap decode initialize response: unexpected EOF"
# and EVERY EVM chain (C + L2s hanzo/zoo/pars/spc, all mgj786) fails to
# initialize. The api bump is code-free for plugins (chains v1.7.4->v1.7.5
# adopted it with a go.mod-only diff), so v1.104.9 = v1.104.8 + api alignment.
# C-Chain execution backend. The default is the pure-Go EVM, which is what
# every image has shipped so far. EVM_CGO=1 EVM_TAGS=cevm links luxcpp/cevm
# instead — that pair is what makes AutoEVM resolve to CppEVM. The libraries
# come from the cevm fetch in this same stage above.
ARG EVM_CGO=0
ARG EVM_TAGS=""
# v1.104.22 realigns the plugin with THIS node's own go.mod: api v1.0.16 ->
# v1.1.1, vm v1.2.6 -> v1.3.1, geth v1.17.12 -> v1.20.1. Node main pins api
# v1.1.1, so pinning an evm at api v1.0.16 reintroduces the exact
# InitializeResponse decode mismatch described above, just in the opposite
# direction — keep this ARG and node's go.mod on the same api/vm/geth line.
#
# v1.104.14 is also the FIRST evm tag carrying the C-Chain fee-split seam
# (core/fee_split.go creditTxFee + extras.FeeSplitTimestamp/FeeRewardVault).
# Below it, encoding/json silently DISCARDS the genesis "feeSplitTimestamp"
# key: a chain configured for the 50/50 split instead routes 100% of every fee
# to the block coinbase and the reward vault 0x0100..0002 stays 0 forever, with
# nothing burned. The split stays dormant wherever feeSplitTimestamp is absent
# (mainnet), so this bump is behaviour-preserving there.
ARG EVM_VERSION=v1.104.22
ARG EVM_VERSION=v1.101.2
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
@@ -314,11 +281,11 @@ 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.7.0 && \
go mod edit -require=github.com/luxfi/chains@v1.4.8 && \
find /tmp/evm -name go.sum -exec sed -i -E '/^github.com\/(luxfi|hanzoai)\//d' {} + && \
GOARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) \
CGO_ENABLED=${EVM_CGO} GOFLAGS=-mod=mod \
go build -ldflags="-s -w" -tags "${EVM_TAGS}" -o /luxd/build/plugins/${EVM_VM_ID} ./plugin && \
CGO_ENABLED=0 GOFLAGS=-mod=mod \
go build -ldflags="-s -w" -o /luxd/build/plugins/${EVM_VM_ID} ./plugin && \
chmod +x /luxd/build/plugins/${EVM_VM_ID} && \
rm -rf /tmp/evm
@@ -335,16 +302,16 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
# oraclevm -> r5m1ujrmXxVcQetG3CQfuDLHp2RHKh6vCDaFgBRQfUcTZh7eS
# quantumvm -> ry9Sg8rZdT26iEKvJDmC2wkESs4SDKgZEhk5BgLSwg1EpcNug
# relayvm -> sP6dLqrrBR9w3soP18fbJ3YzZecZdD7DDdfH2cFhhLq7Hy9bz
# mpcvm -> qCURact1n41FcoNBch8iMVBwc9AWie48D118ZNJ5tBdWrvryS
# thresholdvm -> 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/mpcvm), graphvm genesis-last-accepted fix, built on
# helper (bridgevm/zkvm/thresholdvm), 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.7.6
ARG CHAINS_REF=v1.4.8
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' {} +
@@ -378,25 +345,13 @@ 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/mpcvm && CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
-o /luxd/build/plugins/qCURact1n41FcoNBch8iMVBwc9AWie48D118ZNJ5tBdWrvryS ./cmd/plugin ) || echo "WARN: mpcvm plugin build skipped" ; \
( cd /tmp/chains/thresholdvm && CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
-o /luxd/build/plugins/tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t ./cmd/plugin ) || echo "WARN: thresholdvm plugin build skipped" ; \
( cd /tmp/chains/zkvm && CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
-o /luxd/build/plugins/vv3qPfyTVXZ5ArRZA9Jh4hbYDTBe43f7sgQg4CHfNg1rnnvX9 ./cmd/plugin ) || echo "WARN: zkvm plugin build skipped" ; \
( chmod +x /luxd/build/plugins/* 2>/dev/null || true ) && \
for p in \
juFxSrbCM4wszxddKepj1GWwmrn9YgN1g4n3VUWPpRo9JjERA \
kMhHABHM8j4bH94MCc4rsTNdo5E9En37MMyiujk4WdNxgXFsY \
nZQm4Dmg1rjX18rb8maL9gamYyXPf1xCvF7ymWzxp6a1nSQTt \
oR6tnZHezwogyf9fRnomNXC9ojwCEBAU6jdUzpgy2PB1tD7fM \
pJJCSV7hHYVY6TUZwR8qUPAfuhX8JLb2C1AzNSezrYNbgau8M \
r5m1ujrmXxVcQetG3CQfuDLHp2RHKh6vCDaFgBRQfUcTZh7eS \
ry9Sg8rZdT26iEKvJDmC2wkESs4SDKgZEhk5BgLSwg1EpcNug \
sP6dLqrrBR9w3soP18fbJ3YzZecZdD7DDdfH2cFhhLq7Hy9bz \
qCURact1n41FcoNBch8iMVBwc9AWie48D118ZNJ5tBdWrvryS \
vv3qPfyTVXZ5ArRZA9Jh4hbYDTBe43f7sgQg4CHfNg1rnnvX9 ; do \
test -s /luxd/build/plugins/$p \
|| { echo "FATAL: required chain-VM plugin $p missing/empty — its build failed above (see the matching WARN line); the runtime-stage hard COPY would otherwise fail cryptically. Surface & fix the real Go build error, or remove the plugin from BOTH the build list and the runtime COPY."; exit 1; } ; \
done && \
test -s /luxd/build/plugins/kMhHABHM8j4bH94MCc4rsTNdo5E9En37MMyiujk4WdNxgXFsY \
|| { echo "FATAL: bridgevm (B-Chain) plugin missing — the v1.30.16 regression would recur"; exit 1; } && \
rm -rf /tmp/chains
# ============= Native D-Chain DEX VM Plugin Stage ================
@@ -408,11 +363,18 @@ 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, 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
# 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
# (drops the phantom dchain+cgo gate); v1.5.11 wires CLOB order ingestion over the
# node HTTP router (VM.CreateHandlers -> /v1/bc/D/dex/<method>, pkg/dchain/ingest.go)
# node HTTP router (VM.CreateHandlers -> /ext/bc/D/dex/<method>, pkg/dchain/ingest.go)
# so an order POSTed to the node flows submitTx -> mempool -> consensus -> Verify
# (match) -> Accept; v1.5.12 persists the head block so the VM survives a restart
# once advanced past genesis (GetBlock(lastAccepted) no longer ErrNotFound);
@@ -424,25 +386,54 @@ 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
# /v1/bc/D/dex/<method>, pkg/dchain/read.go): read-only JSON of the durable trade
# /ext/bc/D/dex/<method>, pkg/dchain/read.go): read-only JSON of the durable trade
# log / resting book / markets, served beside the writes with ZERO consensus
# impact. Needed to VERIFY a fill replicated identically across validators (query
# every node, diff the trade rows + head root) and to feed markets-display (native
# fills are trade: rows). Bump with every dex release that changes the VM, like
# CHAINS_REF for the other 10 VMs.
ARG DEX_REF=v1.5.15
# 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 && \
# Align the dexvm plugin's ZAP wire (luxfi/api) with the node. No dex release
# pins api v1.0.16 yet (all <=v1.5.20 carry v1.0.15), so force it here; the
# bump is code-free (adds InitializeResponse.Capabilities, capability-gated),
# so dexvm emits the field the node decodes -> no "unexpected EOF" on D-Chain.
go mod edit -require=github.com/luxfi/api@v1.0.16 && \
. /build/build_env.sh && \
# 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=0 GOFLAGS=-mod=mod \
CGO_ENABLED=${DEX_CGO} GOFLAGS=-mod=mod \
go build -ldflags="-s -w" \
-o /luxd/build/plugins/mDVT5EWMumBp3LCqvKwuyZQeY1VXr1jvjGNAt8nL4UFiXvqXr ./cmd/dchain && \
chmod +x /luxd/build/plugins/mDVT5EWMumBp3LCqvKwuyZQeY1VXr1jvjGNAt8nL4UFiXvqXr && \
@@ -473,8 +464,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
curl ca-certificates git \
&& rm -rf /var/lib/apt/lists/*
# GPU crypto library (optional -- only present when built with CGO_ENABLED=1 + luxcpp).
# Pure Go fallbacks are used when the library is absent.
# 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/
RUN ldconfig 2>/dev/null || true
# Maintain compatibility with previous images.
@@ -502,7 +497,7 @@ COPY --from=builder \
/luxd/build/plugins/r5m1ujrmXxVcQetG3CQfuDLHp2RHKh6vCDaFgBRQfUcTZh7eS \
/luxd/build/plugins/ry9Sg8rZdT26iEKvJDmC2wkESs4SDKgZEhk5BgLSwg1EpcNug \
/luxd/build/plugins/sP6dLqrrBR9w3soP18fbJ3YzZecZdD7DDdfH2cFhhLq7Hy9bz \
/luxd/build/plugins/qCURact1n41FcoNBch8iMVBwc9AWie48D118ZNJ5tBdWrvryS \
/luxd/build/plugins/tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t \
/luxd/build/plugins/vv3qPfyTVXZ5ArRZA9Jh4hbYDTBe43f7sgQg4CHfNg1rnnvX9 \
/luxd/build/plugins/
WORKDIR /luxd/build
+324
View File
@@ -0,0 +1,324 @@
# Lux Mainnet Launch Checklist
Node: luxfi/node v1.24.11
Consensus: Quasar (BLS+Corona, slashing, stake-weighted sampling)
EVM: GPU ecrecover, 18 precompiles
Genesis: networkID=1, startTime=2025-12-12T21:06:51Z (mainnet), networkID=2, startTime=2026-02-10T16:00:00Z (testnet)
Precompile constraint: all activations MUST be after 2025-12-25
---
## Infrastructure
| Environment | Cluster | DOKS ID | K8s Version | Namespace | Validators |
|-------------|---------|---------|-------------|-----------|------------|
| Testnet | do-sfo3-lux-test-k8s | `005ec3c4` | 1.35.1-do.0 | lux-testnet | 11 (target) |
| Rehearsal | do-sfo3-lux-dev-k8s | `0ff340e1` | 1.35.1-do.0 | lux-devnet | 21 (target) |
| Mainnet | do-sfo3-lux-k8s | `04c46df5` | 1.34.1-do.4 | lux-mainnet | 21 (target) |
Current state: lux-k8s runs 5 validators (v1.23.31) via LuxNetwork CRD. Testnet cluster (lux-test-k8s) has a 3-replica StatefulSet (v1.23.40).
Image: `ghcr.io/luxfi/node:v1.24.11` (built via CI/CD, linux/amd64+arm64)
Staking keys: KMS at `kms.lux.network`, project `lux-infra`, synced via KMSSecret CRD
Secrets: never in manifests, never in env files, never committed
Genesis configs: `/Users/z/work/lux/genesis/configs/{testnet,mainnet,devnet}/`
K8s manifests: `/Users/z/work/lux/universe/k8s/`
Profiles: `standard.json` (~100MB/node), `max.json` (~512MB/node)
Bootstrappers:
- Mainnet: 5 seeds (ports 9631) -- `209.38.118.46`, `209.38.174.69`, `24.144.69.101`, `134.199.187.56`, `143.198.246.173`
- Testnet: 2 seeds (ports 9641) -- `134.199.187.16`, `209.38.174.84`
Consensus parameters (mainnet): K=20, AlphaPreference=15, AlphaConfidence=15, Beta=20, ConcurrentPolls=4
Tokenomics: 10B total supply (9 decimals), min validator stake 1M LUX, min delegator stake 25K LUX, combined staking allowed (NFT+delegation), 80% uptime threshold
C-Chain: chainId=96369 (mainnet), 96368 (testnet), gasLimit=12M, targetBlockRate=2s, minBaseFee=25gwei
---
## Phase 1: Testnet (lux-test-k8s, networkID=2)
Target: validate all consensus, EVM, and staking behavior with K=11 validators.
### 1.1 Deployment
- [ ] Update LuxNetwork CRD in `universe/k8s/lux-k8s/validators/statefulset.yaml` (testnet section): `validators: 11`, `image.tag: v1.24.11`
- [ ] Update lux-test-k8s StatefulSet in `universe/k8s/lux-test-k8s/testnet/statefulset.yaml`: replicas=11, image=v1.24.11
- [ ] Generate 11 staking key pairs via `lux cli` and store in KMS (`lux-infra/testnet/staking/`)
- [ ] Add 9 new bootstrapper entries to `genesis/configs/testnet/bootstrappers.json` (currently 2)
- [ ] Apply `max.json` profile for testnet validators (512MB/node for stress testing headroom)
- [ ] Regenerate testnet genesis with 11 initial validators via `genesis` tool
- [ ] Verify all precompile activation timestamps are after 2025-12-25
- [ ] Deploy via PaaS (platform.hanzo.ai), not manual kubectl
- [ ] Verify all 11 pods reach Running state
- [ ] Verify all 11 nodes report healthy via `/ext/health/liveness`
### 1.2 Bootstrap and Connectivity
- [ ] Verify all 11 validators discover each other via P2P (check `info.peers` RPC, expect 10 peers per node)
- [ ] Verify staking port 9641 reachable between all pods (`luxd-{0..10}.luxd-headless.testnet.svc.cluster.local:9641`)
- [ ] Verify P-chain bootstraps and all validators appear in `platform.getCurrentValidators`
- [ ] Verify C-chain bootstraps and produces blocks
- [ ] Verify X-chain bootstraps and processes UTXO transactions
### 1.3 Quasar Consensus Verification
- [ ] Submit transactions, verify Quasar finalization with K=11
- [ ] Verify BLS aggregate signatures in block headers
- [ ] Verify Corona optimistic fast path activates when all 11 validators are online
- [ ] Measure finality latency (target: sub-second with Corona)
- [ ] Verify stake-weighted sampling: validators with more stake get polled proportionally
### 1.4 EVM Execution
- [ ] Deploy a test contract, call all standard opcodes
- [ ] Submit 100 sequential transactions, verify correct nonce ordering
- [ ] Verify `eth_call` and `eth_estimateGas` return correct results
- [ ] Verify block gas limit is 12M (from cchain.json config)
- [ ] Verify minBaseFee=25gwei is enforced
### 1.5 GPU ecrecover
- [ ] Verify GPU backend auto-detection: CUDA on Linux DOKS nodes, Metal on macOS
- [ ] Run ecrecover-heavy workload (1000 signature verifications per block)
- [ ] Compare ecrecover throughput: GPU vs CPU fallback
- [ ] Verify graceful fallback to CPU when GPU unavailable (set `--gpu-backend=cpu`)
### 1.6 Precompiles (all 18)
- [ ] Test each precompile individually via contract calls
- [ ] Verify DEX precompile (LP-9010 PoolManager): pool creation, swaps, flash loans
- [ ] Verify DEX router precompile (LP-9012): multi-hop routing
- [ ] Verify all precompile addresses are deterministic and match spec
- [ ] Verify precompile gas metering is correct (no underpriced or overpriced ops)
- [ ] Verify precompiles revert correctly on invalid input
### 1.7 Slashing
- [ ] Craft equivocation evidence: have a validator sign two different blocks at same height
- [ ] Submit equivocation proof to P-chain slashing precompile
- [ ] Verify slashed validator's stake is burned
- [ ] Verify slashed validator is removed from active set
- [ ] Verify honest validators are unaffected
### 1.8 Uptime and Rewards
- [ ] Stop 1 validator (scale pod to 0)
- [ ] Wait for reward period to elapse
- [ ] Verify stopped validator's uptime drops below 80%
- [ ] Verify rewards are withheld for the stopped validator
- [ ] Restart the validator, verify it re-bootstraps and resumes
- [ ] Verify validators with >80% uptime receive expected rewards
### 1.9 Stress Test
- [ ] Run stress test: maximum TPS with 1B gas blocks (increase gas limit temporarily)
- [ ] Measure sustained TPS over 1 hour (target: verify consensus is the bottleneck, not EVM)
- [ ] Monitor memory usage per node (should stay within `max.json` profile ~512MB)
- [ ] Monitor disk I/O and database growth rate
- [ ] Verify no consensus stalls under load
- [ ] Verify block production rate stays at targetBlockRate=2s
### 1.10 Validator Join/Leave
- [ ] Add a 12th validator via `platform.addPermissionlessValidator` (permissionless staking)
- [ ] Verify new validator bootstraps from existing state
- [ ] Verify new validator begins participating in consensus
- [ ] Remove a validator via unstaking (wait for stake period to end or use testnet short periods)
- [ ] Verify removed validator exits gracefully
- [ ] Verify remaining validators continue producing blocks
### 1.11 Formal Verification
- [ ] Run Lean proofs for Quasar consensus safety and liveness
- [ ] Run TLA+ model checker for consensus state machine
- [ ] Run Tamarin prover for BLS+Corona security properties
- [ ] Run Halmos for EVM precompile correctness (symbolic execution)
- [ ] All proofs pass with zero counterexamples
---
## Phase 2: Mainnet Rehearsal (lux-dev-k8s, networkID=3)
Target: full mainnet simulation with real parameters for 72 hours.
### 2.1 Deployment
- [ ] Update LuxNetwork CRD (devnet section): `validators: 21`, `image.tag: v1.24.11`
- [ ] Generate 21 staking key pairs, store in KMS (`lux-infra/devnet/staking/`)
- [ ] Use mainnet genesis parameters (networkID=3, but same tokenomics, same stake amounts)
- [ ] Apply `max.json` profile
- [ ] Deploy via PaaS
- [ ] Verify all 21 pods healthy
### 2.2 Real Staking Parameters
- [ ] Configure minimum validator stake: 1M LUX
- [ ] Configure minimum delegator stake: 25K LUX
- [ ] Configure max delegation ratio: 10x
- [ ] Configure NFT staking tiers (Genesis 500K/2x, Pioneer 750K/1.5x, Standard 1M/1x)
- [ ] Verify combined staking logic: NFT value + delegation + staked >= 1M
- [ ] Verify B-chain validators require 100M LUX + KYC
### 2.3 72-Hour Soak Test
- [ ] Start clock. Record block height and timestamp.
- [ ] Continuous transaction load: 50 TPS sustained
- [ ] Monitor: CPU, memory, disk, network per node (Prometheus + Grafana via PaaS)
- [ ] Monitor: consensus latency p50/p95/p99
- [ ] Monitor: block production rate (target: 1 block per 2s)
- [ ] Monitor: peer count stability (all 21 connected)
- [ ] Monitor: no OOMKills, no pod restarts, no crashloops
- [ ] At hour 24: rolling restart of 5 validators (verify zero downtime)
- [ ] At hour 48: simulate network partition (isolate 7 nodes), verify chain halts (< 2/3 online)
- [ ] Restore partition, verify chain resumes within 30s
- [ ] At hour 72: record final block height, calculate actual vs expected blocks
- [ ] Pass criteria: zero consensus faults, zero data loss, <1% block time variance
### 2.4 Security Audit
- [ ] External security audit firm engaged (Red team)
- [ ] Audit scope: consensus, EVM, precompiles, staking, slashing, P2P networking
- [ ] Audit result: 0 critical findings, 0 high findings
- [ ] All medium findings remediated or accepted with documented risk
- [ ] Audit report signed and archived
### 2.5 Bridge / Teleport (B-Chain + T-Chain)
- [ ] Deploy MPC threshold signing (5 nodes, threshold 3) in `lux-mpc` namespace
- [ ] Deploy bridge UI and API in `lux-bridge` namespace
- [ ] Verify CGGMP21 keygen: 5 parties generate shared key
- [ ] Verify threshold signing: 3-of-5 produces valid signature
- [ ] Test cross-chain transfer: lock on source chain, mint on Lux
- [ ] Test reverse: burn on Lux, unlock on source chain
- [ ] Verify MPC API at `mpc-api.lux.network` responds
- [ ] Verify bridge handles partial MPC node failure (2 down, 3 still sign)
### 2.6 DEX (D-Chain + Precompiles)
- [ ] Deploy DEX precompile PoolManager (LP-9010) -- already active from genesis
- [ ] Deploy DEX Router precompile (LP-9012) -- already active from genesis
- [ ] Deploy off-chain CLOB matching engine
- [ ] Create liquidity pool via precompile
- [ ] Execute swap via router precompile
- [ ] Verify AMM pricing matches expected curve
- [ ] Verify flash loan execution and repayment
- [ ] Test CLOB: place limit order, verify fill
- [ ] Verify DEX on lux.exchange frontend connects to devnet
---
## Phase 3: Mainnet Launch (lux-k8s, networkID=1)
Target: production network with real value.
### 3.1 Pre-launch
- [ ] All Phase 1 items passed
- [ ] All Phase 2 items passed
- [ ] Security audit sign-off received
- [ ] Formal verification suite green
- [ ] Legal review complete (terms of service, validator agreements)
- [ ] Incident response runbook written and tested
### 3.2 Genesis Ceremony
- [ ] Final genesis config reviewed: `genesis/configs/mainnet/genesis.json` (networkID=1)
- [ ] Genesis startTime confirmed: 2025-12-12T21:06:51Z
- [ ] Initial allocations verified (500M initial + unlock schedule)
- [ ] All 5 bootstrapper IPs confirmed reachable on port 9631
- [ ] Genesis hash computed and published to lux.network
- [ ] Genesis block signed by founding validators
### 3.3 Validator Onboarding
- [ ] Update LuxNetwork CRD (mainnet section): `validators: 21`, `image.tag: v1.24.11`
- [ ] Scale from 5 current validators to 21
- [ ] Generate 16 new staking key pairs in KMS (`lux-infra/mainnet/staking/`)
- [ ] Update bootstrappers.json with all 21 validator endpoints
- [ ] Deploy via PaaS with rolling update strategy
- [ ] Verify all 21 validators healthy and in consensus
- [ ] Publish validator onboarding guide for external operators
- [ ] Open permissionless staking after initial stabilization period
### 3.4 Public RPC Endpoints
- [ ] Deploy KrakenD API gateway in `lux-gateway` namespace
- [ ] Configure rate limiting per IP and per API key
- [ ] Configure Cloudflare DNS (proxied, full SSL):
- `api.lux.network` -> gateway (C-chain + P-chain + X-chain RPC)
- `ws.lux.network` -> gateway (WebSocket subscriptions)
- [ ] Verify `eth_chainId` returns `0x17871` (96369)
- [ ] Verify `net_version` returns `96369`
- [ ] Verify RPC endpoints handle 10K req/s without degradation
- [ ] Verify WebSocket subscriptions for `newHeads`, `logs`, `pendingTransactions`
### 3.5 Explorer Deployment
- [ ] Deploy explorer (luxfi/explorer) in `lux-explorer` namespace (already has manifests for 5 chains)
- [ ] Configure for C-chain (chainId 96369)
- [ ] Configure indexers for all active chains
- [ ] Configure Cloudflare DNS: `explore.lux.network`
- [ ] Verify block display, transaction search, contract verification
- [ ] Deploy exchange frontend: `lux.exchange`
### 3.6 Bridge Activation
- [ ] Deploy MPC production cluster (5 nodes, threshold 3)
- [ ] Generate production MPC keys (CGGMP21 keygen ceremony)
- [ ] Store MPC key shares in KMS (`lux-infra/mainnet/mpc/`)
- [ ] Deploy bridge contracts on supported chains (ETH, BNB, Polygon, Arbitrum, Base, Optimism)
- [ ] Deploy bridge UI at bridge domain
- [ ] Configure Cloudflare DNS
- [ ] Enable deposits (one chain at a time, small limits first)
- [ ] Monitor for 24h, then raise limits
### 3.7 Post-Launch Monitoring
- [ ] Prometheus + Grafana dashboards live (via PaaS o11y stack)
- [ ] Alerts configured:
- Validator down (any pod not Ready for >5min)
- Consensus stall (no new block for >30s)
- Peer count drop (any node <15 peers)
- Memory usage >80% of limit
- Disk usage >70%
- Error rate >1% on RPC endpoints
- [ ] On-call rotation established
- [ ] Runbook covers: validator restart, chain halt recovery, emergency upgrade, key rotation
---
## Port Reference
| Network | HTTP | Staking | Metrics |
|---------|------|---------|---------|
| Mainnet | 9630 | 9631 | 9090 |
| Testnet | 9640 | 9641 | 9090 |
| Devnet | 9650 | 9651 | 9090 |
## Chain IDs
| Chain | Mainnet | Testnet | Devnet |
|-------|---------|---------|--------|
| C-Chain | 96369 | 96368 | 96370 |
| Zoo EVM | 200200 | 200201 | 200202 |
| Hanzo EVM | 36963 | 36964 | 36964 |
| SPC EVM | 36911 | 36910 | 36912 |
| Pars EVM | 494949 | 7071 | 494951 |
## File References
| What | Path |
|------|------|
| Node source | `~/work/lux/node/` |
| Genesis configs | `~/work/lux/genesis/configs/{mainnet,testnet,devnet}/` |
| Chain configs | `~/work/lux/genesis/configs/chain-configs/` |
| K8s manifests | `~/work/lux/universe/k8s/` |
| Validator CRD | `~/work/lux/universe/k8s/lux-k8s/validators/statefulset.yaml` |
| Testnet StatefulSet | `~/work/lux/universe/k8s/lux-test-k8s/testnet/statefulset.yaml` |
| Node profiles | `~/work/lux/node/config/profiles/{standard,max}.json` |
| Tokenomics config | `~/work/lux/node/config/tokenomics.go` |
| GPU config | `~/work/lux/node/config/gpu.go` |
| Health/consensus params | `~/work/lux/node/config/health.go` |
| Network registry | `~/work/lux/universe/NETWORKS.yaml` |
-21
View File
@@ -10,24 +10,3 @@ 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).
+15 -299
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 /v1/security`
- JSON-RPC namespace: `security` at `POST /ext/security`
(methods `securityProfile`, `blockSecurity`)
- REST sidecars: `GET /v1/security/profile`, `GET /v1/security/block/{n}`
- Prometheus gauges: `/v1/metrics` under the `security_*` family
- REST sidecars: `GET /ext/security/profile`, `GET /ext/security/block/{n}`
- Prometheus gauges: `/ext/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,29 +85,6 @@ 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.
@@ -120,14 +97,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, ...}` (deducted from bridged amount, LP-0130 §8) |
| quantumvm | Q-Chain | **service-only** (LP-0130 §6) | `NoUserTxPolicy{}` — validator obligation, no user-payable blockspace |
| bridgevm | B-Chain | user-tx | `FlatPolicy{Fee: MinTxFeeFloor, ...}` |
| quantumvm | Q-Chain | user-tx | `FlatPolicy{Fee: MinTxFeeFloor, ...}` |
| identityvm | I-Chain | user-tx | `FlatPolicy{Fee: MinTxFeeFloor, ...}` |
| mpcvm | M-Chain | service-only (LP-0130 §7) | `NoUserTxPolicy{}` — fees pulled from originating chain's fee pool |
| thresholdvm | M-Chain | service-only | `NoUserTxPolicy{}` |
| oraclevm | O-Chain | service-only | `NoUserTxPolicy{}` |
| relayvm | R-Chain | service-only | `NoUserTxPolicy{}` |
| graphvm | G-Chain | read-only | `NoUserTxPolicy{}` (GraphQL refuses `mutation`) |
| evm | C-Chain | user-tx | native EVM gas (gas * gasPrice >= 0 enforced upstream); balance is X-imported LUX (LP-0130 §10) |
| evm | C-Chain | user-tx | native EVM gas (gas * gasPrice >= 0 enforced upstream) |
| platformvm | P-Chain | user-tx | native `TxFee` field on Config |
| avm | X-Chain | user-tx | native `TxFee` field on Config |
@@ -158,257 +135,6 @@ charge less.
- Relay (R-Chain): `~/work/lux/relay/vm/feegate.go` (re-exported by `~/work/lux/chains/relayvm/`)
- Graph (G-Chain): `~/work/lux/chains/graphvm/feegate.go` (read-only; NoUserTxPolicy)
## C-Chain tx-fee routing — RewardManager to DAO Safe (P-Chain: NO CHANGE)
Owner tokenomics pivoted: **100% of C-Chain tx fees accrue to the chain's DAO Gov
Safe** via the existing `rewardmanager` precompile (C-Chain only). This needs **no
P-Chain change** — routing is `GetCoinbaseAt` → reward address on the C-Chain. See
`~/work/lux/evm/LLM.md` → "C-Chain Tx-Fee Routing — RewardManager". The P-Chain
`feeRewardPool` fold-in below is **NOT built** (design-only, superseded); no
`vms/platformvm/state` change was made.
<details><summary>Superseded design — 50/50 burn + P-Chain staking-reward fold (dormant option)</summary>
If the DAO ever chooses an in-protocol 50/50 split, the C-Chain half exists (dormant,
`FeeSplitTimestamp` gated off) and the P-Chain fold-in would be: system-triggered epoch
export of the C-Chain vault C→P → persisted `feeRewardPool` in `vms/platformvm/state`
(mirror the `accruedFees` singleton, upgrade-safe) → pro-rata payout at
`vms/platformvm/txs/executor/proposal_tx_executor.go` `rewardValidatorTx` (~line 285),
unified into `PotentialReward`, NO second mint → decrement `currentSupply` by the epoch
burn. Model R1 (move-not-mint), conservation-exact; R2 (burn+re-mint) rejected.
</details>
## v1.36.12 fleet rollout — durable rejoin fix + RewardManager→DAO Safe (IN PROGRESS 2026-07-15)
Rolling the durable rejoin fix (node `63f61429d1`) across all Lux nets, gated
devnet→testnet→mainnet, + activating RewardManager fees. Two hard facts were found
on the devnet canary that change the naive "swap image" plan:
**BLOCKER (fixed in v1.36.12): published `node:v1.36.11` (digest `c3cf92a6`) cannot
run ANY EVM chain.** Its baked VM plugins were built against a stale `luxfi/api`:
C-Chain EVM from `luxfi/evm@v1.104.8` and D-Chain dexvm from `luxfi/dex@v1.5.15`
both resolve `api v1.0.15`; the node pins `api v1.0.16`, which APPENDED
`InitializeResponse.Capabilities` (Quasar-export handshake, api `1f2dc5a`). Node
decodes the field, stale plugins never encode it → `vms/rpcchainvm/zap/client.go`
fails every EVM `Initialize` with `zap decode initialize response: unexpected EOF`.
Native VMs (P/X/Q) unaffected. **Fix = v1.36.12** (this repo, tag pushed, ARC docker
build run 29381442539): `EVM_VERSION v1.104.8→v1.104.9`, force `api@v1.0.16` in the
dexvm build stage; `CHAINS_REF=v1.7.6` was already v1.0.16. Node binary unchanged
(still the durable fix). api bump is code-free for plugins (`chains v1.7.4→v1.7.5`
adopted it go.mod-only). **Roll v1.36.12, NOT v1.36.11.** (Also: `api 1f2dc5a` added
`Capabilities` WITHOUT bumping `version.RPCChainVMProtocol` (42) → skew is invisible
at handshake, only fails at Initialize decode. Consider bumping the protocol next
api-wire change so skews fail fast.)
**MIGRATION: v1.36.2→v1.36.x is a P-Chain codec change (linearcodec→ZAP-native),
one-time DB wipe + re-bootstrap.** v1.36.11/12 cannot read a v1.36.2 P-Chain zapdb
(`loadMetadata: feeState: zap: invalid magic bytes`; `state_commit.go:116` "database
must be wiped"). Recovery = wipe `/data/db`+`/data/chainData`, re-bootstrap from
peers. **Cross-version bootstrap (v1.36.11 node ← v1.36.2 peers) is PROVEN working**
(devnet luxd-1: P/X re-bootstrapped from the 4 v1.36.2 peers). Devnet startup got a
marker-gated one-time wipe (`/data/.zap-native-migrated`): absent→wipe+set marker,
present→NO wipe (the durable-fix no-wipe restart path). mainnet/testnet/zoo use
`startup.sh` which already has `.wipe-cchain` (C-Chain only) + `.allow-bootstrap`
(flips skip-bootstrap=false + EVM state-sync); for the codec migration the P-Chain
zapdb (`/data/db`) must also be cleared. **Mainnet C-Chain is 1.08M blocks → MUST
enable EVM state-sync for the re-sync (full replay is too slow); native VMs are tiny.**
**Durable fix (`63f61429d1`):** discriminator for keeping the staked beacon set is
SYBIL-PROTECTION, not `--skip-bootstrap`. So a behind validator on a sybil-protected
net catches up from peers even with `--skip-bootstrap=true` (which prod hardcodes),
no wipe. Devnet added `--skip-bootstrap=true` to the inline cmd to exercise this.
**RewardManager (C-Chain precompile, per-net `cchain-upgrade.json` → append one
`precompileUpgrades` entry, dated `blockTimestamp`):** proven testnet shape is
`{"rewardManagerConfig":{"blockTimestamp":<ts>,"adminAddresses":["<admin>"],
"initialRewardConfig":{"rewardAddress":"<reward>"}}}`. Reward addr = coinbase; 100%
fees land there, blackhole `0x0100…00` goes flat. Addresses: **testnet ALREADY LIVE**
(reward `0xEAbCC110fAcBfebabC66Ad6f9E7B67288e720B59`, admin `0x9011…94714`); **mainnet**
reward+admin = DAO Gov Safe `0x8E29b816c6C35b13cE1ff68D33E245C2bda8ac3D`; **zoo**
reward `0x229599f227231d8C90fcF1a78589F5DC4b7A6962`; **devnet** reward
`0x8d5081153aE1cfb41f5c932fe0b6Beb7E159cF84` (idx2), admin `0x9011…94714` (idx0).
Source ConfigMaps: devnet `luxd-chain-upgrades/cchain-upgrade.json`; mainnet+testnet
`luxd-startup/cchain-upgrade.json`; zoo `zood-mv-genesis/upgrade.json` (`--upgrade-file`).
**Rollout levers (lux-operator is scaled 0/0 — sts/cm are the live source of truth;
CRs are STALE, do not scale operator up mid-roll):** ports devnet 9650 / testnet 9640
/ mainnet 9630 / zoo 9630; RPC path `/v1/bc/C/rpc`; container `luxd` (`zood` on zoo).
Devnet uses an inline generated cmd; testnet/mainnet/zoo use `/scripts/startup.sh`.
**Zoo `zood-mv` trap: RollingUpdate + hardcoded `--skip-bootstrap=true` with NO
`.allow-bootstrap` gate → switch to OnDelete BEFORE rolling.** Lux sts are OnDelete.
Master funded key = LUX_MNEMONIC (secret `lux-deployer`) idx0 `0x9011…94714`;
`genesis/cmd/derivekey -mnemonic "$M"` (path m/44'/9000'/0'/0/i, `CGO_ENABLED=0`).
**Per-node roll protocol (ALL nets, one at a time, NEVER 2 mainnet down — quorum
4/5):** set sts image v1.36.12 (+ rewardManager cm edit) → delete ONE pod → WAIT
until it is back at **TIP HEIGHT matching the others** (NOT pod-Ready; a wedged node
false-reports Ready) AND C-Chain serves RPC → only then the next. If any node fails
to return to tip, STOP that net and report.
**State at pause:** v1.36.12 tag pushed + ARC build dispatched (run 29381442539).
Devnet sts = v1.36.11 + skip-bootstrap + wipe-marker; luxd-1 migrated (P/X up on
v1.36.11, C-Chain down = the plugin bug → will heal on v1.36.12); luxd-0/2/3/4 still
v1.36.2 healthy (devnet C-Chain 4/5). Nothing rolled on testnet/mainnet/zoo. NEXT:
when v1.36.12 image is ready → set devnet sts image v1.36.12, delete luxd-1, confirm
C-Chain inits + reaches tip; then finish devnet (durable-fix proof + RewardManager),
then gated testnet→mainnet→zoo.
## v1.36.35 — the certified-descendant false halt + the plugin-killing map fatal (devnet 96367, 2026-07-28)
> Shipped as v1.36.35, not v1.36.34: the v1.36.34 tag inherited a build break that
> `011e9bf99d` ("deps: drop go.sum lines that disagree with the checksum log") had
> already pushed to main — it bumped `zap-proto/http` from a pseudo-version to v0.3.0,
> whose `Server.Handler` is a `fasthttp.RequestHandler` and whose `NewTransport` is now
> `Dial(network, addr)`, so `server/http/zap_listener.go` no longer compiled. **main was
> unbuildable from that commit until this one.** Repaired here by bridging the one
> net/http handler chain with `fasthttpadaptor.NewFastHTTPHandler` (the two transports
> stay behaviourally identical — only the wire encoding differs) and moving the
> round-trip test onto the fasthttp request/response pair. v1.36.34 produced no image
> and its tag was deleted.
Rolling devnet to v1.36.33 fixed the build→verify-fail→drop loop and unmasked two
DIFFERENT failures. Both are fixed here; each has a regression test that fails before
and passes after.
**P0-1 — five validators `os.Exit(1)` on a benign state.** Each devnet node hit, once:
```
error VM accepted head is CONSENSUS-CERTIFIED and conflicts with the newly finalized
block — refusing to orphan it orphanedHeight=1364
fatal SetPreference would orphan a CONSENSUS-CERTIFIED block — refusing (fail-closed)
error="cannot orphan finalized block at height: 1364 to common block at height: 1363"
```
at three distinct height pairs (1258/1259, 1363/1364, 1364/1365), ALWAYS with
`certified = head 1`, with every surviving node holding byte-identical blocks at all
five heights — no fork anywhere. (The "1168/1169" pair in the original report never
appears in any fatal line; those two blocks are a normal parent/child present on all
live nodes.)
Two defects in `luxfi/consensus`, one crash — fixed in **consensus v1.36.12**:
- *Producer.* `acceptWithCertCore` releases `t.mu` across every VM call-out, then steers
the VM with its STALE local `blockID`. A finalize that completed in that window has
already advanced the ledger AND the EVM to `blockID+1`, so the steer is BACKWARDS and
the EVM's accepted-irreversibility guard (`evm/core/blockchain.go:1987`,
`commonBlock < lastAccepted`) correctly refuses it. **That guard is right and is
untouched.** Now steers at the live build anchor (`PreferredBuildTip`
`ledger.BuildAnchor`), which the accept ordering (ApplyCert BEFORE VM.Accept) keeps at
or above the VM's own accepted head. Same value the build path already uses.
- *Classifier.* `reconcileVMToCertified` asked only "is the head the ledger's certified
canonical at ITS OWN height?" — trivially true for every healthy node whose head is
certified — and called that a two-blocks-at-one-height double-finalization. It never
established that `certified` was at that same height. The ledger holds one canonical
per height along one contiguous chain, so when both are certified at their own heights
they lie on that one chain and the head merely DESCENDS from the target: nothing is
orphaned. **The fail-closed halt is not weakened** — it now fires on the state that is
actually unsafe (steering off a certified head onto a block our own ledger does not
certify at its height).
Direction: NEITHER roll the head back NOR certify forward. The VM head is legitimately
ahead and already CONTAINS the certified block; `FinalityLedger.BuildAnchor` already
documents `head > certified` as the designed state, and rolling back is exactly what
`blockchain.go:1987` exists to refuse. The correct action is no action — plus not issuing
the backwards steer at all.
**P0-2 — the EVM plugin process dies and never comes back.** devnet luxd-1:
```
fatal error: concurrent map read and map write
vm/components/chain.(*State).getCachedBlock state.go:216
vm/components/chain.(*State).ParseBlock state.go:267
evm/plugin/evm.(*VM).ParseBlock vm.go:340
vm/rpc.(*zapVMServer).handleParseBlock vm_server_zap.go:477
```
`chain.State` was written against avalanchego's contract that the consensus engine holds
the chain lock across every VM call. The ZAP VM server does NOT reinstate it — it
dispatches ParseBlock/GetBlock and the Verify/Accept/Reject wrappers concurrently.
`verifiedBlocks` (plain map) and `lastAcceptedBlock` (pointer) are the only State that is
not self-synchronising; every `cache.Cacher` carries its own mutex. Fixed in **vm v1.3.3**
by giving State one RWMutex for exactly those two, never held across a call into the
inner VM. A Go map fatal is unrecoverable: it kills the plugin, **luxd survives and keeps
answering `info.getNodeVersion` while its chain is gone** — pod-Ready and `/v1/health`
both stay green — and there is no self-heal.
**Quorum arithmetic (reported, not changed).** Devnet and testnet both run
`--consensus-sample-size=5 --consensus-quorum-size=4`. With only 3 live C-Chains the
quorum is arithmetically unreachable; devnet demonstrated both directions in one session
(pinned at 1378 on 3 live, 1378→1395→1396 the moment luxd-4 restored a 4th). This is a
config value, not a code defect, and lowering it lowers the safety margin — left as is.
## v1.36.33 — the build→self-verify-fail→drop loop (devnet 96367 / testnet 96368, 2026-07-28)
**Symptom.** Every proposer built a block and then rejected the block it had just
built, forever: `built block … height=1047` immediately followed by
`built block failed verification — dropping error="inner parentID didn't match
expected parent"`, 83456 drops/min per node, accepted tip frozen two heights BELOW
what the builder kept proposing (devnet tip 1045, builds 1047).
**Root cause (one line).** `buildChild` asked the inner VM for a block without first
pointing the inner VM at the parent's inner block. The inner VM builds on ITS OWN
head (`luxfi/evm` miner reads `bc.CurrentBlock()`); the verify path requires
`child.innerBlk.Parent() == parent.innerBlk.ID()`. Two different pointers, one
required equal to the other, nothing asserting it. The head drifts on its own:
verifying a GOSSIPED block whose parent is the current head optimistically sets the
head (`core/blockchain.go writeBlockAndSetHead → newTip → writeCanonicalBlockWithLogs`),
and `VM.SetPreference` short-circuits on an unchanged outer preference so it never
re-pushes the inner preference to drag the head back. Non-self-correcting by
construction.
**Fix.** `VM.anchorInnerBuildParent` (vms/proposervm/vm.go) — one inner
`SetPreference` at the point of use, called from BOTH build delegations
(`postForkCommonComponents.buildChild`, `preForkBlock.buildChild`). On a healthy node
the inner `setPreference` early-returns on `current.Hash() == block.Hash()`, so it is
a lookup and no state change; when it fails, the head is provably not the parent's
inner block, so refusing to build beats emitting a block we would drop.
Regression proof: `vms/proposervm/build_inner_parent_test.go` (models the three evm
head semantics — build-on-head, verify-advances-head, SetPreference-reorgs-head).
**NOT the cause, measured:** the P-Chain. `info.isBootstrapped{"chain":"P"}` = true on
15/15 nodes and `platform.getHeight` = 0 on 15/15 **including mainnet**, whose built
blocks also carry `pChainHeight=0`. `bootstrapped.message:["111…LpoYY"]` is the
primary-network NET id (`chains/chains.go Nets.Bootstrapping` keys by net), not the
P-Chain — the P-Chain's chain id prints `111…P` (`constants.PlatformChainID =
ids.PChainID`, while `PrimaryNetworkID = ids.Empty`). The verify path DOES read
pChainHeight before the parent check, but only as monotonicity
(`childPChainHeight < parentPChainHeight`, and 0 < 0 is false, so it passes); every
P-Chain-DEPENDENT validation — epoch, `GetCurrentHeight`, proposer window — is gated
behind `consensusState == Ready` and sits AFTER the parent check. So `pChainHeight=0`
cannot produce `errInnerParentMismatch`.
**Sibling failure modes on the same fleets (already fixed in 1.36.32, needs the roll):**
outer index BEHIND the inner tip ⇒ `refusing to build`/boot repair
(`height_backfill.go`, 7d2f01eb), and preferred-absent-locally ⇒ last-accepted
fallback (`vm.go BuildBlock`). All three are the same invariant seen from three sides.
**Roll surface — measured 2026-07-28T07:5xZ, do not use the LuxNetwork CR.**
`lux-operator` and `lux-operator-devnet` are **0/0** in `lux-system`, so nothing
reconciles `luxnetworks.lux.cloud/luxd`; its tags are stale garbage (mainnet
`v1.34.0`, testnet `v1.32.12` — v1.34.0 exists in no registry). The live image is on
the **StatefulSet**, hand-maintained: mainnet `ghcr.io/luxfi/node:v1.36.2`
(`kubectl-patch` 07-25T00:27:04Z), devnet `v1.36.25@sha256:ca497eff…`, testnet
`v1.36.24@sha256:91e2542b…`, all three `updateStrategy: OnDelete`. Change the image in
the universe manifest and apply, then delete one pod. Editing the CR and deleting a pod
reboots it on the OLD image.
**Mainnet 96369 is NOT a clean control — 3 of 5 nodes have Mode B, invisibly.**
luxd-0/3/4 `eth_blockNumber` = `0x10c1cf` (1098191, block ts 2026-07-24T15:46:19Z) and
have not moved in 3+ days while luxd-1 mines (1098341, tx status 0x1, 07:58Z); luxd-2
serves 404 (no C-Chain). Not a fork — 1098191 hashes identically on luxd-0 and luxd-1.
3998 of luxd-0's last 4000 log lines are the same `built block … height=1098196`. The
drop line is absent because the **binary** lacks it: `grep -c "built block failed
verification" /luxd/build/luxd` = **0** on mainnet v1.36.2, **1** on devnet v1.36.25.
`/v1/health` says `{"healthy":true,"error":"health reply encode failed"}` on the frozen
node and the mining node alike — never gate on it, use tip parity.
Two hazards for the roll: the 4 broken mainnet pods are exactly the ones on
ControllerRevision `luxd-596857c9d6` (rev 147, kubectl-patch 07-25: GOMEMLIMIT=6GiB,
mem limit 16Gi→12Gi, request 4Gi→7Gi) and the one mining node, luxd-1, is the only pod
still on rev 144 — **deleting luxd-1 recreates it on the revision every other node
broke on**. And `ghcr.io/luxfi/node:v1.36.2` has no git tag at all
(`git ls-remote origin refs/tags/v1.36.2` → empty; v1.36.24/25 → present), so what
mainnet runs is not reproducible from this repo.
## Essential Commands
### Release & build (canonical) — via platform.hanzo.ai, NOT GitHub Actions
@@ -504,7 +230,7 @@ Located in `/vms/`:
- **platformvm**: Staking, validation, network management
- **xvm**: Asset transfers, UTXO model
- **dexvm**: DEX with order book, perpetuals, AMM
- **mpcvm**: Threshold MPC and FHE for confidential computing
- **thresholdvm**: 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)
@@ -594,11 +320,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/mpcvm/fhe` - GPU FHE operations
- `vms/thresholdvm/fhe` - GPU FHE operations
- `x/blockdb` - zstd compression
### FHE (Fully Homomorphic Encryption)
Located in `vms/mpcvm/fhe/`:
Located in `vms/thresholdvm/fhe/`:
- Uses `github.com/luxfi/lattice/multiparty` for DKG
- Lattice-based cryptography only (no fallbacks)
- Threshold decryption via Warp messaging
@@ -913,7 +639,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/mpcvm/fhe/gpu_fhe_nocgo.go`
- `vms/thresholdvm/fhe/gpu_fhe_nocgo.go`
- `vms/zkvm/accel/accel_mlx.go`
### 8. ZAP CreateHandlers for VM HTTP Endpoints
@@ -933,7 +659,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/v1/bc/C/rpc
http://localhost:9640/ext/bc/C/rpc
# Returns: {"jsonrpc":"2.0","id":1,"result":"0x17870"}
```
@@ -942,7 +668,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 `/v1/bc/C/rpc`
- **POST /**: Proxies JSON-RPC requests directly to C-chain `/ext/bc/C/rpc`
- **OPTIONS /**: Returns CORS preflight headers
**Files Modified**: `server/http/router.go`, `server/http/server.go`
@@ -1009,7 +735,7 @@ if s.validators.NumNets() != 0 {
**Verification**:
```bash
curl -s http://localhost:9650/v1/health | jq '.checks.bls'
curl -s http://localhost:9650/ext/health | jq '.checks.bls'
# Should show: "message": "node has the correct BLS key"
```
@@ -1043,7 +769,7 @@ Testing conducted on a single Lux validator node (testnet mode, macOS):
**Benchmark Command:**
```bash
cd ~/work/lux/benchmarks
NODE_ENDPOINT="http://localhost:9640/v1/bc/C/rpc" \
NODE_ENDPOINT="http://localhost:9640/ext/bc/C/rpc" \
PRIVATE_KEY="<funded_key>" \
./bin/bench tps --chains=lux --duration=60s --concurrency=5
```
@@ -1091,14 +817,4 @@ v2 semantic differences worth knowing (these change wire shape):
---
## Housekeeping
Removed 6 generated write-ups / stale root artifacts (`LAUNCH_CHECKLIST.md`,
`rename_app.sh`, `replace_imports.sh`, `gen_zoo_addr` binary, `.ci-status-check.md`,
`.ci-trigger`) plus the 73MB `.claude/worktrees/` agent scratch tree. Release and
launch state live in this file, `CHANGELOG.md`, `RELEASE.md`; chain IDs/ports in
`~/work/lux/universe/NETWORKS.yaml` and `~/work/lux/genesis/configs/`.
---
*Last Updated*: 2026-06-06
+2 -9
View File
@@ -7,15 +7,8 @@ CGO_ENABLED ?= 1
FIPS_STRICT ?= 0
# Go 1.26 experimental features:
# runtimesecret - zeroes stack/register state after secret.Do() for forward secrecy.
# It SIGSEGVs at startup under the WSL2 kernel (confirmed on go1.26.3 and go1.26.4), so enable
# it only off-WSL; forward secrecy stays on for real Linux/macOS/production builds.
WSL := $(shell grep -qiE 'microsoft|WSL' /proc/sys/kernel/osrelease 2>/dev/null && echo 1)
ifeq ($(WSL),1)
GOEXPERIMENT ?= none
else
# runtimesecret - zeroes stack/register state after secret.Do() for forward secrecy
GOEXPERIMENT ?= runtimesecret
endif
export GOEXPERIMENT
# FIPS 140-3 always enabled (required for blockchain/financial systems)
@@ -219,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/v1/info | jq
-H 'content-type:application/json;' http://localhost:9630/ext/info | jq
stop-node:
@echo "$(YELLOW)Stopping Lux node...$(NC)"
-38
View File
@@ -1,38 +0,0 @@
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 `/v1/index/X/block` API
- Added example usage of the `/ext/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-mpcvm-audit.md` | ThresholdVM (T-Chain) |
| `audits/2025-12-30-thresholdvm-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) |
+10
View File
@@ -73,6 +73,12 @@ tasks:
- task: generate-mocks
- task: check-clean-branch
check-generate-protobuf:
desc: Checks that generated protobuf is up-to-date (requires a clean git working tree)
cmds:
- task: generate-protobuf
- task: check-clean-branch
check-go-mod-tidy:
desc: Checks that go.mod and go.sum are up-to-date (requires a clean git working tree)
cmds:
@@ -95,6 +101,10 @@ tasks:
- cmd: grep -lr -E '^// Code generated - DO NOT EDIT\.$' tests/load/c | xargs -r rm
- cmd: go generate ./tests/load/c/...
generate-protobuf:
desc: Generates protobuf
cmd: ./scripts/protobuf_codegen.sh
ginkgo-build:
desc: Runs ginkgo against the current working directory
cmd: ./bin/ginkgo build {{.USER_WORKING_DIR}}
@@ -1,107 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package chains
import (
"context"
"sync"
"testing"
"github.com/stretchr/testify/require"
"github.com/luxfi/ids"
"github.com/luxfi/log"
version "github.com/luxfi/version"
"github.com/luxfi/vm/chain"
)
// recordingConnector is a chain.ChainVM that records the version it is handed on
// Connected. Every other method comes from the embedded (nil) interface and is
// never invoked by the connect path.
type recordingConnector struct {
chain.ChainVM
mu sync.Mutex
called bool
gotVersion *version.Application
}
func (c *recordingConnector) Connected(_ context.Context, _ ids.NodeID, nodeVersion *chain.VersionInfo) error {
c.mu.Lock()
defer c.mu.Unlock()
c.called = true
c.gotVersion = nodeVersion
return nil
}
func (c *recordingConnector) Disconnected(context.Context, ids.NodeID) error { return nil }
// TestBlockHandlerConnectedWithVersionDeliversRealVersion is the regression
// guard for RED CRITICAL #1 (C-Chain nil-version panic).
//
// Before the fix blockHandler.Connected forwarded connector.Connected(ctx,
// nodeID, nil) — a hardcoded nil version. proposervm promotes Connected to the
// inner C-Chain VM (coreth), whose state-sync peer tracker compares peer
// versions; a nil version there dereferences nil (version.Application.Compare)
// and PANICS a state-syncing node (a fresh join, or a validator rejoining after
// falling behind — the launch's core invariant).
//
// The fix plumbs the REAL peer version through ConnectedWithVersion to the
// connector. This test drives the real blockHandler with a real version and
// asserts the connector receives that non-nil version — never nil.
func TestBlockHandlerConnectedWithVersionDeliversRealVersion(t *testing.T) {
require := require.New(t)
rc := &recordingConnector{}
bh := newBlockHandler(
nil, // BlockBuilder — unused by the connect path
rc, // connector under test
log.Noop(), // logger
nil, // engine
nil, // net
nil, // msgCreator
ids.Empty, // chainID
ids.Empty, // networkID
nil, // beacons
ids.NodeID{}, // selfNodeID
false, // expectsStakedBeacons
)
nodeID := ids.GenerateTestNodeID()
peerVersion := &version.Application{Name: "lux", Major: 1, Minor: 36, Patch: 27}
require.NoError(bh.ConnectedWithVersion(context.Background(), nodeID, peerVersion))
rc.mu.Lock()
defer rc.mu.Unlock()
require.True(rc.called, "connector.Connected must be invoked")
require.NotNil(rc.gotVersion, "connector must receive a NON-nil version (coreth dereferences it in state-sync)")
require.Equal("lux", rc.gotVersion.Name)
require.Equal(1, rc.gotVersion.Major)
require.Equal(36, rc.gotVersion.Minor)
require.Equal(27, rc.gotVersion.Patch)
}
// TestBlockHandlerConnectedDedupsButStillCarriesVersion confirms the version
// survives the once-only dedup: the first (versioned) dispatch reaches the
// connector; a duplicate is a no-op (not a nil-version overwrite).
func TestBlockHandlerConnectedDedupsButStillCarriesVersion(t *testing.T) {
require := require.New(t)
rc := &recordingConnector{}
bh := newBlockHandler(nil, rc, log.Noop(), nil, nil, nil, ids.Empty, ids.Empty, nil, ids.NodeID{}, false)
nodeID := ids.GenerateTestNodeID()
peerVersion := &version.Application{Name: "lux", Major: 1, Minor: 36, Patch: 27}
require.NoError(bh.ConnectedWithVersion(context.Background(), nodeID, peerVersion))
// A duplicate connect (e.g. dispatched again per tracked network) must be a
// no-op — never a second call that could clobber the stored version with nil.
require.NoError(bh.ConnectedWithVersion(context.Background(), nodeID, nil))
rc.mu.Lock()
defer rc.mu.Unlock()
require.NotNil(rc.gotVersion, "the deduped duplicate must not overwrite the real version with nil")
require.Equal(27, rc.gotVersion.Patch)
}
+16 -91
View File
@@ -157,27 +157,8 @@ func (b *blockHandler) sampleAncestorBeacons() (set.Set[ids.NodeID], bool) {
if len(connected) == 0 {
return nil, false
}
// 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)
sample := set.NewSet[ids.NodeID](bootstrapAncestorSample)
for i := 0; i < len(connected) && sample.Len() < bootstrapAncestorSample; i++ {
sample.Add(connected[(start+i)%len(connected)])
}
@@ -201,19 +182,6 @@ func (b *blockHandler) fullyConnectedBeacons(weights map[ids.NodeID]uint64, conn
return external > 0 && len(connected) >= external
}
// hasExternalBeacons reports whether the staked beacon set contains any validator OTHER than this
// node. False for a self-only (single-validator) set — there is then no peer to sync from, so the
// node's own tip IS the frontier (FrontierTip returns FrontierNoBeacons). The set is read from
// P-chain STATE, not connectivity, so this is a TOPOLOGY fact (a genuine single-validator net),
// never an eclipse artifact (an eclipse drops peers from `connected`, never from `weights`).
func (b *blockHandler) hasExternalBeacons(weights map[ids.NodeID]uint64) bool {
external := len(weights)
if _, selfIsBeacon := weights[b.selfNodeID]; selfIsBeacon {
external--
}
return external > 0
}
// withSelfVote returns `replies` plus the node's OWN accepted frontier as a beacon reply — the
// SELF-VOTE. The node is itself a beacon (selfNodeID in `weights`, the trust anchor) and knows its
// own accepted tip (lastID) with certainty, so it vouches for it exactly as a connected peer's reply
@@ -324,21 +292,6 @@ func (b *blockHandler) FrontierTip(ctx context.Context) (ids.ID, chainbootstrap.
return ids.Empty, chainbootstrap.FrontierConnecting
}
// SELF-ONLY staked set: the configured validator set is exactly THIS node — a genuine
// single-validator network (a sybil-protected devnet, or the sole validator of an L1). There
// is no OTHER beacon to sync from, so the node's own accepted tip IS the network frontier →
// immediate start (FrontierNoBeacons). This is NOT an eclipse: the staked set is read from
// P-chain STATE, not connectivity, so a hidden peer would still appear in `weights`; only a
// genuine single-validator topology reaches here. Placed AFTER the P-ready gate so a boot-race
// partial set (transiently self-only mid-P-replay) WAITS above rather than false-completing here.
// This is the sybil-protected single-validator counterpart to the empty-set FrontierNoBeacons
// path: a multi-validator staked set (the ≥2 case) still runs the ⅔-by-stake quorum below.
if !b.hasExternalBeacons(weights) {
b.logger.Debug("bootstrap frontier: self-only staked set (single validator) — NoBeacons, nothing to sync to",
log.Stringer("chainID", b.chainID))
return ids.Empty, chainbootstrap.FrontierNoBeacons
}
// THE MASS-RECOVERY FIX. The acceptance decision is the BootstrapPolicy (a SEPARATE object
// with a SEPARATE threat model — bootstrap_trust.go), NOT the ⅔-of-CURRENT-total-stake
// consensus floor. The prior code required a ⅔-by-stake quorum of the WHOLE validator set to
@@ -365,7 +318,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 FinalityQuorum alone governs).
// re-executes the descent and re-enters consensus, where ConsensusQuorum 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
@@ -585,17 +538,6 @@ 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 {
@@ -609,16 +551,13 @@ 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 record() // every connected beacon answered — resolve now, do not wait the window
return replies // every connected beacon answered — resolve now, do not wait the window
}
case <-deadline:
return record()
return replies
case <-ctx.Done():
return record()
return replies
}
}
}
@@ -642,15 +581,7 @@ func (b *blockHandler) Ancestors(ctx context.Context, blockID ids.ID, maxBlocks
requestID := b.requestIDCounter
b.contextRequestMu.Unlock()
// 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)
ch := make(chan [][]byte, 1)
b.bsMu.Lock()
b.bsAncestorCh[requestID] = ch
b.bsMu.Unlock()
@@ -666,19 +597,13 @@ func (b *blockHandler) Ancestors(ctx context.Context, blockID ids.ID, maxBlocks
}
b.net.Send(msg, sample, b.networkID, 0)
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()
}
select {
case blocks := <-ch:
return blocks, nil
case <-time.After(bootstrapAncestorsTimeout):
return nil, nil
case <-ctx.Done():
return nil, ctx.Err()
}
}
@@ -891,7 +816,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 /v1/health/liveness, would be a permanent brick. It is
// the K8s probes only poll the always-green /ext/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() }
@@ -996,7 +921,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
// /v1/health/liveness, so a fail-safe-DOWN node is never restarted). A STRUCTURAL failure (deep gap
// /ext/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.
@@ -1038,7 +963,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 /v1/health/liveness, so a fail-safe-DOWN node is
// provide (they all poll the always-green /ext/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.
+6 -217
View File
@@ -25,7 +25,6 @@ import (
consensuschain "github.com/luxfi/consensus/engine/chain"
cblock "github.com/luxfi/consensus/engine/chain/block"
chainbootstrap "github.com/luxfi/consensus/engine/chain/bootstrap"
"github.com/luxfi/constants"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/math/set"
@@ -218,13 +217,6 @@ 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
@@ -342,23 +334,6 @@ 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 {
@@ -537,66 +512,6 @@ 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
@@ -860,9 +775,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
@@ -909,7 +824,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{}
@@ -1613,7 +1528,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 /v1/health/liveness and would never restart it). This is the bounded
// poll the always-green /ext/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
@@ -1937,8 +1852,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()
@@ -2033,129 +1948,3 @@ func TestRED_BehindNode_PeerConnectDelay_NeverFalseCompletesAtStale(t *testing.T
require.Greater(t, net.peerInfoCalls, net.connectAfterCalls,
"the loop must have WAITED through the connecting passes before naming the frontier")
}
// TestChainExpectsStakedBeacons_SybilDrivesNotSkipBootstrap pins the ROOT-CAUSE decision of the
// rejoin wedge (tasks #66/#74): whether a chain syncs its bootstrap frontier against the STAKED
// primary-network set is driven by SYBIL PROTECTION, NOT --skip-bootstrap. Production validators
// set --skip-bootstrap (to skip the initial bootstrap WAIT); the prior `!m.SkipBootstrap && ...`
// made that flag also EMPTY the beacon set, so a behind native chain (C/X/Q) reported
// FrontierNoBeacons, named its STALE local tip the network frontier, went live there, and never
// caught up across restarts until a manual chaindata wipe. skip-bootstrap is not even an input to
// the fixed decision — that is the point.
func TestChainExpectsStakedBeacons_SybilDrivesNotSkipBootstrap(t *testing.T) {
require.True(t, chainExpectsStakedBeacons(true, true, false),
"sybil-protected native non-platform chain (C/X/Q) MUST expect staked beacons → peer-sync a behind validator (regardless of skip-bootstrap)")
require.False(t, chainExpectsStakedBeacons(false, true, false),
"non-sybil (dev / single-node) native chain → NO staked beacons: the empty-beacon immediate-start path")
require.False(t, chainExpectsStakedBeacons(true, false, false),
"a non-native chain does not sync against the primary staked set here")
require.False(t, chainExpectsStakedBeacons(true, true, true),
"the platform chain anchors to its OWN CustomBeacons, never the staked-set frontier quorum")
}
// TestChainValidatesOnPrimaryNetwork_RealHashChainID is the regression the hardcoded-bool test
// above could NOT catch: it exercises the ACTUAL discriminator buildChain feeds into
// chainExpectsStakedBeacons. The durable rejoin fix (#66/#74) shipped INERT in production because
// the discriminator keyed off the blockchain ID via ids.IsNativeChain — false for every deployed
// C/X/Q (they carry HASH blockchain IDs; ids.IsNativeChain only matches the symbolic 111...C alias
// form, which no deployed chain uses). So a real behind C-Chain still got empty beacons under
// --skip-bootstrap and wedged. The fix keys off the validating Net (chainParams.ChainID) instead.
func TestChainValidatesOnPrimaryNetwork_RealHashChainID(t *testing.T) {
// Real deployed C-Chain blockchain IDs (devnet + mainnet) are HASHES, and ids.IsNativeChain
// is false for them — the exact trap that disabled the fix.
for _, s := range []string{
"21HieZngQW8unBnSTbdQ9PcPAz6hhPLGrPzZhTvjC8KgjE95Bg", // devnet C-Chain
"2wRdZGeca1qkxzNCq88NWDF5nJ5A9o623vRJKd3FsjRYvuVvvt", // mainnet C-Chain
} {
id, err := ids.FromString(s)
require.NoError(t, err)
require.False(t, ids.IsNativeChain(id),
"trap: ids.IsNativeChain is false for real hash C-Chain ID %s — do NOT discriminate on the blockchain ID", s)
}
// C/X/Q validate on the PRIMARY NETWORK: chainParams.ChainID == PrimaryNetworkID. That is the
// signal buildChain now passes, so the durable fix fires for the real (hash-ID) C-Chain.
require.True(t, chainValidatesOnPrimaryNetwork(constants.PrimaryNetworkID))
require.True(t, chainExpectsStakedBeacons(true, chainValidatesOnPrimaryNetwork(constants.PrimaryNetworkID), false),
"a sybil-protected primary-network non-platform chain (C/X/Q) MUST expect staked beacons regardless of its hash blockchain ID")
// An L2 validates on its OWN sovereign net, not the primary network → stays on the
// empty-beacon single-node path (behavior unchanged for L2s).
l2Net := ids.GenerateTestID()
require.False(t, chainValidatesOnPrimaryNetwork(l2Net))
require.False(t, chainExpectsStakedBeacons(true, chainValidatesOnPrimaryNetwork(l2Net), false))
}
// TestNodeBootstrap_SelfOnlyStakedSet_ReportsNoBeacons covers the single-VALIDATOR counterpart of
// the fix: a sybil-protected chain whose staked set is EXACTLY this node (a single-validator
// devnet, or the sole validator of an L1) has no OTHER beacon to sync from, so FrontierTip reports
// FrontierNoBeacons (immediate start at the node's own tip) rather than hanging in FrontierConnecting.
// This is what lets skip-bootstrap stop emptying native beacons WITHOUT bricking a single-validator
// net. It is NOT an eclipse: the staked set is read from P-chain STATE (hasExternalBeacons), so a
// hidden peer would still appear in `weights`.
func TestNodeBootstrap_SelfOnlyStakedSet_ReportsNoBeacons(t *testing.T) {
const N = 10
chain, byID := buildBSChain(N, -1)
vm := newBSVMAt(chain, N) // the sole validator IS at the frontier
self := ids.GenerateTestNodeID()
bh, chainID := newBSHandlerWeighted(t, vm, map[ids.NodeID]uint64{self: 100})
bh.selfNodeID = self // the staked set is EXACTLY this node — a genuine single-validator net
bh.net = &bsBeaconNet{bh: bh, chainID: chainID, byID: byID, tip: chain[N]}
bh.msgCreator = bsMsgBuilder{}
bh.bsActive.Store(true)
tip, status := bh.FrontierTip(context.Background())
bh.bsActive.Store(false)
require.Equal(t, chainbootstrap.FrontierNoBeacons, status,
"self-only staked set (single validator) → NoBeacons (nothing to sync to), never a FrontierConnecting hang")
require.Equal(t, ids.Empty, tip)
// Discriminator: add ONE other validator and the SAME node must now run the quorum (has a peer
// to sync from) — proving the self-only shortcut fires ONLY for a genuinely alone validator.
require.True(t, bh.hasExternalBeacons(map[ids.NodeID]uint64{self: 100, ids.GenerateTestNodeID(): 100}),
"a ≥2-validator staked set has an external beacon → runs the ⅔-by-stake quorum, not the self-only shortcut")
}
// TestNodeBootstrap_BehindValidator_StakedSet_CatchesUpNoWipe is THE REJOIN INVARIANT (tasks
// #66/#74), the code reproduction of the mainnet luxd-0 wedge: a staked validator that fell behind
// must sync from its peers on restart and reach the network frontier WITHOUT a chaindata wipe.
//
// The node reloads its STALE on-disk tip at height M (the "restart" — no wipe) and holds a
// sybil-protected native-chain staked beacon set (expectsStakedBeacons=true — exactly what
// buildChain now leaves in place under --skip-bootstrap, instead of emptying it). The 4 healthy
// producers name+serve the frontier N over the real GetAcceptedFrontier/GetAncestors transport, so
// the behind node fetch+executes M+1..N and ends ACCEPTED at N — never stuck at the stale M.
func TestNodeBootstrap_BehindValidator_StakedSet_CatchesUpNoWipe(t *testing.T) {
const N = 60 // network frontier (the healthy producers)
const M = 42 // our STALE local height after a restart — behind by N-M, NO wipe
chain, byID := buildBSChain(N, -1)
vm := newBSVMAt(chain, M) // reload the stale on-disk tip (the restart)
// A 5-validator staked set (equal stake): this node is one, the other 4 are the connected,
// serving producers at the frontier N. This mirrors lux-mainnet's 5-validator C-Chain.
weights := map[ids.NodeID]uint64{}
producers := make([]ids.NodeID, 4)
for i := range producers {
producers[i] = ids.GenerateTestNodeID()
weights[producers[i]] = 100
}
self := ids.GenerateTestNodeID()
weights[self] = 100
bh, chainID := newBSHandlerWeighted(t, vm, weights)
bh.selfNodeID = self
require.True(t, bh.expectsStakedBeacons,
"a native sybil chain expects staked beacons even under skip-bootstrap — the fix that keeps peer-sync alive")
bh.net = &bsBeaconNet{bh: bh, chainID: chainID, connected: producers, byID: byID, tip: chain[N], serveAncestors: true}
bh.msgCreator = bsMsgBuilder{}
ctx := context.Background()
require.NoError(t, runBS(t, bh),
"behind validator must converge to the frontier from its peers — no wipe, chain keeps finalizing")
last, _ := vm.LastAccepted(ctx)
require.Equal(t, chain[N].id, last,
"REJOIN: behind validator caught up from peers to frontier N=%d (was stuck at stale M=%d, no wipe)", N, M)
require.True(t, bh.Accepted(ctx, chain[N].id),
"node holds + ACCEPTED the frontier tip after the rejoin (genuine catch-up, not a stale false-complete)")
}
+10 -63
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. FinalityQuorum decides FINALITY
// The fix is a type split, NOT a renamed threshold. ConsensusQuorum 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,7 +24,6 @@ package chains
import (
"bytes"
"context"
"encoding/binary"
"errors"
"fmt"
"math"
@@ -38,30 +37,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 FinalityQuorum.
// Live block acceptance remains governed exclusively by ConsensusQuorum.
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 FinalityQuorum alone governs acceptance.
// re-entering live consensus, where ConsensusQuorum alone governs acceptance.
AcceptsFrontier(ctx context.Context, replies []BeaconReply) (*Frontier, error)
}
// FinalityQuorum decides FINALITY: whether a weight is a finalizing supermajority (> ⅔) of the
// ConsensusQuorum 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 FinalityQuorum interface {
type ConsensusQuorum 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 FinalityQuorum: > ⅔ of the CURRENT total stake, exactly
// twoThirdsFinality is the production ConsensusQuorum: > ⅔ 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.
@@ -71,10 +70,10 @@ func (twoThirdsFinality) HasFinality(weight, total StakeWeight) bool {
return weight > consensusconfig.TwoThirdsStakeFloor(total)
}
// DefaultFinalityQuorum returns the live ⅔-of-current-stake finality rule — the thing bootstrap
// DefaultConsensusQuorum 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 DefaultFinalityQuorum() FinalityQuorum { return twoThirdsFinality{} }
func DefaultConsensusQuorum() ConsensusQuorum { return twoThirdsFinality{} }
var (
// ErrInsufficientBootstrapResponses: fewer than MinResponses configured beacons answered.
@@ -128,48 +127,9 @@ 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
@@ -192,7 +152,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 FinalityQuorum
// FLOOR and an agreement threshold over the RESPONDERS — a SEPARATE object from ConsensusQuorum
// 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
@@ -237,12 +197,8 @@ 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. When set, it is trusted ONLY if CheckpointVerifier authenticates its
// signature (INVARIANT 4).
// reject below the floor.
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
@@ -374,15 +330,6 @@ 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,
+12 -100
View File
@@ -16,7 +16,6 @@ package chains
import (
"context"
"crypto/ed25519"
"testing"
"github.com/stretchr/testify/require"
@@ -303,7 +302,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
// FinalityQuorum.HasFinality — live block acceptance still requires > ⅔ of CURRENT validator
// ConsensusQuorum.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
@@ -322,76 +321,39 @@ 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")
// FinalityQuorum says that SAME 3-of-5 weight is NOT finality — the decisions are different
// ConsensusQuorum 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 := DefaultFinalityQuorum()
cq := DefaultConsensusQuorum()
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 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.
// (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.
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: ckptHeight, Signature: authority.sign(ckptID, ckptHeight)},
CheckpointVerifier: authority,
TrustedBeacons: equalBeacons(beacons, equalStake),
MinResponses: 3,
Checkpoint: &Checkpoint{ID: ckptID, Height: 1_082_796},
}
// 1 reachable beacon — below the floor — but a SIGNED checkpoint is pinned.
// 1 reachable beacon — below the floor — but a 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 SIGNED checkpoint → anchor to the checkpoint")
require.True(t, f.FromCheckpoint, "below the floor with a pinned checkpoint → anchor to the checkpoint")
require.Equal(t, ckptID, f.ID)
require.Equal(t, ckptHeight, f.Height)
require.Equal(t, uint64(1_082_796), f.Height)
// Without the checkpoint the same 1-of-5 is rejected (the default — never trust the lone beacon).
policy.Checkpoint = nil
@@ -401,56 +363,6 @@ 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
+4 -35
View File
@@ -46,48 +46,17 @@ 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.
//
// s.chains is keyed by NET id, not chain id. Reporting the key named the net
// instead of the chain: every primary-network chain that failed to converge
// surfaced in /v1/health as the single ID
// "11111111111111111111111111111111LpoYY" — constants.PrimaryNetworkID, i.e.
// ids.Empty — a "chain" the chain manager has never heard of, so the operator
// chasing it got "there is no chain with alias/ID". Worse, N stuck chains
// collapsed into one indistinguishable entry. Ask each net which of ITS chains
// are still bootstrapping; the net owns that set.
func (s *Nets) Bootstrapping() []ids.ID {
s.lock.RLock()
defer s.lock.RUnlock()
chainsBootstrapping := make([]ids.ID, 0, len(s.chains))
for _, chain := range s.chains {
chainsBootstrapping = append(chainsBootstrapping, chain.Bootstrapping()...)
for chainID, chain := range s.chains {
if !chain.IsBootstrapped() {
chainsBootstrapping = append(chainsBootstrapping, chainID)
}
}
return chainsBootstrapping
+2 -44
View File
@@ -158,54 +158,12 @@ func TestNetsBootstrapping(t *testing.T) {
chain, ok := chains.GetOrCreate(netID)
require.True(ok)
// Start bootstrapping. What comes back is the CHAIN that is syncing, never
// the net that holds it — this assertion used to demand netID, which is
// how the phantom "11111111111111111111111111111111LpoYY" survived review.
// Start bootstrapping
chain.AddChain(chainID)
bootstrapping := chains.Bootstrapping()
require.Equal([]ids.ID{chainID}, bootstrapping)
require.NotContains(bootstrapping, netID)
require.Contains(bootstrapping, netID)
// Finish bootstrapping
chain.Bootstrapped(chainID)
require.Empty(chains.Bootstrapping())
}
// The "bootstrapped" health check publishes Nets.Bootstrapping() verbatim as
// its message. s.chains is keyed by NET id, so returning the key reported
// constants.PrimaryNetworkID (ids.Empty, cb58
// "11111111111111111111111111111111LpoYY") as though it were an unbootstrapped
// CHAIN. Operators saw a chain ID the chain manager denies exists, and every
// stuck chain on the net collapsed into that one phantom entry.
func TestNetsBootstrappingReportsChainsNotNets(t *testing.T) {
require := require.New(t)
chains, err := NewNets(ids.EmptyNodeID, map[ids.ID]nets.Config{
constants.PrimaryNetworkID: {},
})
require.NoError(err)
primary, _ := chains.GetOrCreate(constants.PrimaryNetworkID)
cChainID := ids.GenerateTestID()
dChainID := ids.GenerateTestID()
primary.AddChain(cChainID)
primary.AddChain(dChainID)
bootstrapping := chains.Bootstrapping()
// The phantom: never the net's own ID.
require.NotContains(bootstrapping, constants.PrimaryNetworkID)
require.NotContains(
bootstrapping,
ids.Empty,
"health check reported the primary NET id as an unbootstrapped chain",
)
// Both stuck chains must be individually nameable.
require.ElementsMatch([]ids.ID{cChainID, dChainID}, bootstrapping)
primary.Bootstrapped(cChainID)
require.Equal([]ids.ID{dChainID}, chains.Bootstrapping())
primary.Bootstrapped(dChainID)
require.Empty(chains.Bootstrapping())
}
-139
View File
@@ -1,139 +0,0 @@
// 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))
}
}
+61 -375
View File
@@ -58,7 +58,6 @@ import (
"github.com/luxfi/constants"
"github.com/luxfi/container/buffer"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/crypto/mldsa"
"github.com/luxfi/filesystem/perms"
"github.com/luxfi/log"
"github.com/luxfi/math/set"
@@ -69,7 +68,6 @@ import (
"github.com/luxfi/node/upgrade"
"github.com/luxfi/node/vms"
validators "github.com/luxfi/validators"
version "github.com/luxfi/version"
"github.com/luxfi/vm/fx"
// "github.com/luxfi/node/vms/metervm" // Temporarily disabled - needs consensus package updates
@@ -103,16 +101,6 @@ const (
defaultChannelSize = 1
initialQueueSize = 3
// vmStartupTimeout bounds each VM startup/lifecycle operation (Initialize,
// Linearize, SetState, CreateHandlers, router AddChain, state-sync). It must
// be generous enough to cover a cold coreth "Regenerate historical state"
// pass after an unclean shutdown (observed 67-134s on mainnet C-Chain),
// which the previous hardcoded 30s budget blew — the context was cancelled
// mid-init, so the VM was marked failed and its C-Chain route never
// registered (the recurring post-restart 404 that needed a second restart).
// Bounded so a genuinely hung VM still surfaces rather than hanging forever.
vmStartupTimeout = 10 * time.Minute
luxNamespace = constants.PlatformName + utilmetric.NamespaceSeparator + "lux"
handlerNamespace = constants.PlatformName + utilmetric.NamespaceSeparator + "handler"
meterchainvmNamespace = constants.PlatformName + utilmetric.NamespaceSeparator + "meterchainvm"
@@ -172,20 +160,6 @@ 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
@@ -245,7 +219,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., /v1/bc/zoo/rpc)
// Name of the chain (used for HTTP routing alias, e.g., /ext/bc/zoo/rpc)
Name string
}
@@ -365,19 +339,8 @@ type ManagerConfig struct {
SybilProtectionEnabled bool
StakingTLSSigner crypto.Signer
StakingTLSCert *staking.Certificate
// Strict-PQ proposer identity. When StakingMLDSASigner is non-nil, chains wrap
// their proposervm with ML-DSA-65 block signing so the signed block's
// Proposer() matches the ML-DSA-keyed validator set. Nil ⇒ classical TLS-leaf.
StakingMLDSASigner *mldsa.PrivateKey
StakingMLDSAPub []byte
// ProposerWindowDuration overrides the proposervm proposer-slot spacing (0 ⇒
// the 5s mainnet default). Small local/dev nets set it low for fast cadence.
ProposerWindowDuration time.Duration
// ProposerMinBlockDelay overrides the proposervm minimum block delay (0 ⇒ the
// 1s default). High-throughput / DEX nets set it low (e.g. 1ms).
ProposerMinBlockDelay time.Duration
StakingBLSKey bls.Signer
TracingEnabled bool
StakingBLSKey bls.Signer
TracingEnabled bool
// Must not be used unless [TracingEnabled] is true as this may be nil.
Tracer trace.Tracer
Log log.Logger
@@ -780,7 +743,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
// /v1/health return 503 for any opted-out chain, which made
// /ext/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.
@@ -865,7 +828,7 @@ func (m *manager) createChain(chainParams ChainParameters) {
m.Log.Info("VM implements CreateHandlers, calling it",
log.Stringer("chainID", chainParams.ID),
)
ctx, cancel := context.WithTimeout(context.Background(), vmStartupTimeout)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
handlers, err := vm.CreateHandlers(ctx)
if err != nil {
@@ -886,13 +849,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 /v1/<base><endpoint>
// AddRoute will build the full path as /ext/<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., /v1/bc/zoo/rpc)
// Also register with chain name alias for user-friendly routing (e.g., /ext/bc/zoo/rpc)
if chainParams.Name != "" {
nameLower := strings.ToLower(chainParams.Name)
nameBase := fmt.Sprintf("bc/%s", nameLower)
@@ -932,7 +895,7 @@ func (m *manager) createChain(chainParams ChainParameters) {
// Register chain with the router for message routing
if m.ManagerConfig.Router != nil {
routeCtx, routeCancel := context.WithTimeout(context.Background(), vmStartupTimeout)
routeCtx, routeCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer routeCancel()
m.ManagerConfig.Router.AddChain(routeCtx, chainParams.ID, chain.Handler)
}
@@ -965,16 +928,16 @@ 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("║ → /v1/bc/" + chainParams.ID.String())
m.Log.Info("║ → /ext/bc/" + chainParams.ID.String())
if chainAlias != chainParams.ID.String() {
m.Log.Info("║ → /v1/bc/" + chainAlias)
m.Log.Info("║ → /ext/bc/" + chainAlias)
}
m.Log.Info("╚══════════════════════════════════════════════════════════════════╝")
// Tell the chain to start processing messages.
// If the X, P, or C Chain panics, do not attempt to recover
if chain.Engine != nil {
startCtx, startCancel := context.WithTimeout(context.Background(), vmStartupTimeout)
startCtx, startCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer startCancel()
chain.Engine.Start(startCtx, !m.CriticalChains.Contains(chainParams.ID))
@@ -1132,27 +1095,18 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
}
// expectsStakedBeacons gates the EMPTY-beacon-set behavior of the bootstrap frontier
// quorum (see blockHandler.expectsStakedBeacons). True for a native NON-platform chain
// (C/X/Q/...) on a SYBIL-PROTECTED (real staked) network — those sync against the STAKED
// quorum (see blockHandler.expectsStakedBeacons). True ONLY for a native NON-platform
// chain (C/X/Q/...) on a sybil-protected network — those sync against the STAKED
// primary-network validator set (m.Validators, populated by the already-bootstrapped
// P-chain), so an empty set means "not yet loaded / misconfig → wait then fail safe",
// NOT "single-node → done". Driven by sybil-protection, NOT --skip-bootstrap: a production
// validator sets skip-bootstrap and that must NOT make it masquerade as single-node and
// wedge behind at its stale local tip (tasks #66/#74). See chainExpectsStakedBeacons.
// Discriminate on the validating Net (chainParams.ChainID), NOT the blockchain ID:
// deployed C/X/Q carry HASH blockchain IDs, and ids.IsNativeChain only matches the
// symbolic 111...C alias form (no deployed chain has it). Keying off the blockchain ID
// (the prior ids.IsNativeChain(chainParams.ID)) was therefore ALWAYS false for the real
// C-Chain, leaving this fix inert — the exact wedge #66/#74 set out to kill fired anyway.
isPrimaryNetworkChain := chainValidatesOnPrimaryNetwork(chainParams.ChainID)
expectsStakedBeacons := chainExpectsStakedBeacons(m.SybilProtectionEnabled, isPrimaryNetworkChain, isPlatformChain)
// NOT "single-node → done". The P-chain (CustomBeacons may be empty under endpoint-only
// --bootstrap-nodes) and skip-bootstrap keep the "empty ⇒ nothing to sync to" behavior.
// Computed BEFORE the skip-bootstrap override so it is false in single-node mode.
expectsStakedBeacons := !m.SkipBootstrap && ids.IsNativeChain(chainParams.ID) && !isPlatformChain
// skip-bootstrap forces empty beacons (single-node immediate-start) for every chain EXCEPT
// one that syncs against the staked set — those MUST always catch a behind validator up from
// its peers, so emptying their beacons (the prior UNCONDITIONAL override) is precisely the
// rejoin wedge this fixes. A genuine single-node / dev net runs sybil-protection OFF, so its
// native chains still fall here and get the empty-beacon immediate-start path.
if m.SkipBootstrap && !expectsStakedBeacons {
// In skip-bootstrap mode, use empty beacons for all chains
// This enables single-node development mode
if m.SkipBootstrap {
beacons = &emptyValidatorManager{}
m.Log.Info("skip-bootstrap enabled - using empty beacons for single-node mode")
}
@@ -1208,7 +1162,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 proposervm's
// linear chains in proposervm so block production follows the Snowman++
// 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
@@ -1245,10 +1199,6 @@ 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
})
@@ -1293,31 +1243,24 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
if regErr != nil {
return nil, fmt.Errorf("failed to register proposervm metrics for chain %s: %w", chainParams.ID, regErr)
}
minBlkDelay := proposervm.DefaultMinBlockDelay
if m.ProposerMinBlockDelay > 0 {
minBlkDelay = m.ProposerMinBlockDelay
}
engineVM = proposervm.New(vmTyped, proposervm.Config{
Upgrades: m.Upgrades,
NetworkID: networkID, // CRITICAL-3: windower uses the SAME set ID as the cert side
MinBlkDelay: minBlkDelay,
NumHistoricalBlocks: proposervm.DefaultNumHistoricalBlocks,
StakingLeafSigner: m.StakingTLSSigner,
StakingCertLeaf: m.StakingTLSCert,
StakingMLDSASigner: m.StakingMLDSASigner,
StakingMLDSAPub: m.StakingMLDSAPub,
ProposerWindowDuration: m.ProposerWindowDuration,
Registerer: proposervmReg,
Upgrades: m.Upgrades,
NetworkID: networkID, // CRITICAL-3: windower uses the SAME set ID as the cert side
MinBlkDelay: proposervm.DefaultMinBlockDelay,
NumHistoricalBlocks: proposervm.DefaultNumHistoricalBlocks,
StakingLeafSigner: m.StakingTLSSigner,
StakingCertLeaf: m.StakingTLSCert,
Registerer: proposervmReg,
})
m.Log.Info("wrapping chain VM in proposervm for single-proposer-per-height block production",
log.Stringer("chainID", chainParams.ID),
log.Int("K", consensusParams.K),
log.Stringer("windowerNetworkID", networkID),
log.Duration("minBlockDelay", minBlkDelay))
log.Duration("minBlockDelay", proposervm.DefaultMinBlockDelay))
}
m.Log.Info("initializing VM", log.Stringer("chainID", chainParams.ID))
initCtx, initCancel := context.WithTimeout(context.Background(), vmStartupTimeout)
initCtx, initCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer initCancel()
// Initialize THROUGH engineVM. proposervm.Initialize builds its windower
// from chainRuntime.ValidatorState, then initializes the inner VM with the
@@ -1385,7 +1328,7 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
}); ok {
m.Log.Info("linearizing DAG-native VM into linear block mode",
log.Stringer("chainID", chainParams.ID))
linCtx, linCancel := context.WithTimeout(context.Background(), vmStartupTimeout)
linCtx, linCancel := context.WithTimeout(context.Background(), 30*time.Second)
if err := linearVM.Linearize(linCtx, ids.Empty, toEngine); err != nil {
linCancel()
m.Log.Error("failed to linearize VM",
@@ -1418,7 +1361,7 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
}); ok {
m.Log.Info("transitioning VM to bootstrapping (initial sync gates normal operation)",
log.Stringer("chainID", chainParams.ID))
stateCtx, stateCancel := context.WithTimeout(context.Background(), vmStartupTimeout)
stateCtx, stateCancel := context.WithTimeout(context.Background(), 30*time.Second)
if err := stateVM.SetState(stateCtx, uint32(vm.Bootstrapping)); err != nil {
stateCancel()
m.Log.Error("failed to transition VM to bootstrapping",
@@ -1562,7 +1505,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 proposervm mechanism newPChainHeightVM was a stand-in
// is precisely the Snowman++ 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 {
@@ -1573,56 +1516,8 @@ 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
@@ -1637,7 +1532,7 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
m.Log.Info("consensus engine started with Lux consensus (Photon → Wave → Focus)",
log.Stringer("chainID", chainParams.ID))
if blockBuilder != nil {
syncCtx, syncCancel := context.WithTimeout(context.Background(), vmStartupTimeout)
syncCtx, syncCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer syncCancel()
lastAcceptedID, height, err := consensuschain.SyncStateFromVM(syncCtx, blockBuilder, consensusEngine.Transitive)
if err != nil {
@@ -1705,10 +1600,7 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
// through (blockBuilder == the P-chain-height wrapper on K>1, the inner VM
// on K==1), so the container bytes it parses match the bytes the engine
// framed — one codec, no raw-vs-wrapped split.
// engineVM is the connectable VM (the proposervm on a wrapped chain, the
// inner VM otherwise); it carries Connected/Disconnected, which the router
// dispatches so the P-chain uptime tracker observes validator connectivity.
bh := newBlockHandler(blockBuilder, engineVM, m.Log, consensusEngine, m.Net, m.MsgCreator, chainParams.ID, networkID, beacons, m.NodeID, expectsStakedBeacons)
bh := newBlockHandler(blockBuilder, m.Log, consensusEngine, m.Net, m.MsgCreator, chainParams.ID, networkID, beacons, m.NodeID, expectsStakedBeacons)
// Gate this native chain's bootstrap frontier-TRUST on the P-chain having finished its
// initial sync, so the staked beacon set (and thus the stake-majority floor denominator)
// is the TRUE full validator set, not a partial mid-replay set. Wired ONLY for native
@@ -1909,7 +1801,7 @@ func (m *manager) createDAG(
}
// Create a context for VM initialization with timeout
initCtx, cancelInit := context.WithTimeout(context.Background(), vmStartupTimeout)
initCtx, cancelInit := context.WithTimeout(context.Background(), 30*time.Second)
defer cancelInit() // Ensure cleanup on function exit
// Initialize VM if it supports Initialize
@@ -2077,7 +1969,7 @@ func (m *manager) createDAG(
// Register HTTP handlers for DAG VMs (exchangevm, qvm, etc.)
adapter := &dagVMAdapter{underlying: vmImpl}
dagHandlerCtx, dagHandlerCancel := context.WithTimeout(context.Background(), vmStartupTimeout)
dagHandlerCtx, dagHandlerCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer dagHandlerCancel()
handlers, err := adapter.CreateHandlers(dagHandlerCtx)
if err != nil {
@@ -2188,7 +2080,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 /v1/health/liveness — would never restart it, so a stop here is a permanent
// always-green /ext/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 }
@@ -2360,17 +2252,6 @@ 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]
@@ -2378,7 +2259,9 @@ func (m *manager) IsBootstrapped(id ids.ID) bool {
if !exists {
return false
}
return m.Nets.IsChainBootstrapped(id)
// Bootstrapped chains start in NormalOp
return true
}
func (m *manager) GetChains() []ChainInfo {
@@ -2388,12 +2271,10 @@ 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,
// 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),
ID: id,
Name: info.Name,
VMID: info.VMID,
Bootstrapped: true,
})
}
return result
@@ -2696,43 +2577,6 @@ func (m *manager) getOrMakeVMGatherer(vmID ids.ID) (metrics.MultiGatherer, error
return vmGatherer, nil
}
// chainExpectsStakedBeacons reports whether a chain must sync its bootstrap frontier against the
// STAKED primary-network validator set — the ⅔-by-stake quorum that names the network frontier
// (blockHandler.expectsStakedBeacons, the C1 forged-chain gate). When true, --skip-bootstrap does
// NOT empty the chain's beacon set (buildChain below), so a behind validator always catches up
// from its peers.
//
// It is driven by SYBIL PROTECTION — the true "this is a real staked network" signal — and NOT by
// --skip-bootstrap. Production validators set --skip-bootstrap to skip the initial bootstrap WAIT,
// but that flag must NEVER disable peer-sync on a staked network. Keying this decision off
// --skip-bootstrap (the prior `!m.SkipBootstrap && ...`) is exactly what wedged a behind native
// chain (C/X/Q...): with skip-bootstrap set, the chain got expectsStakedBeacons=false + empty
// beacons, so FrontierTip reported FrontierNoBeacons ("nothing to sync to"), the node named its
// STALE local last-accepted the network frontier, went live there, and never caught up across
// restarts until a manual chaindata wipe (tasks #66/#74). A genuine single-node / dev network runs
// sybil-protection OFF, so it still takes the empty-beacon immediate-start path. The platform chain
// anchors to its own CustomBeacons (not the staked set), so it is excluded here as before; a
// single-VALIDATOR staked net (self-only set) is handled downstream by FrontierTip's
// hasExternalBeacons rule, which reads the LOADED set.
func chainExpectsStakedBeacons(sybilProtectionEnabled, isPrimaryNetworkChain, isPlatformChain bool) bool {
return sybilProtectionEnabled && isPrimaryNetworkChain && !isPlatformChain
}
// chainValidatesOnPrimaryNetwork reports whether a chain's validating Net IS the primary
// network — the correct discriminator for the "native" C/X/Q/... set that syncs its bootstrap
// frontier against the primary-network STAKED validator set. It keys off the validating Net
// (subnet) ID, NOT the blockchain ID: deployed C/X/Q carry HASH blockchain IDs, whereas
// ids.IsNativeChain only ever matches the symbolic 111...C alias form that NO deployed chain
// uses (only the P-chain, at PlatformChainID=111...P, has a symbolic blockchain ID — and it is
// excluded as the platform chain anyway). Keying the durable-rejoin discriminator off the
// blockchain ID (the prior ids.IsNativeChain(chainParams.ID)) therefore silently excluded every
// real C/X/Q and left the fix inert across restarts (#66/#74). The validating Net is
// PrimaryNetworkID for C/X/Q and each sovereign L1's own net ID for an L2, so this correctly
// keeps the empty-beacon single-node path for L2s while re-enabling peer-sync for C/X/Q.
func chainValidatesOnPrimaryNetwork(validatingNetID ids.ID) bool {
return validatingNetID == constants.PrimaryNetworkID
}
// emptyValidatorManager implements validators.Manager with no validators
type emptyValidatorManager struct{}
@@ -2848,15 +2692,6 @@ type blockHandler struct {
chainID ids.ID // Chain ID for message routing
networkID ids.ID // Network ID for validator routing
// connector receives peer connect/disconnect notifications routed from the
// node's chainRouter and forwards them to this chain's VM (e.g. the P-chain
// uptime tracker; other VMs use them for their own peer sets). connectedNodes
// dedups delivery so a peer the router dispatches once per tracked network is
// forwarded to the VM exactly once. A nil connector makes forwarding a no-op.
connector chain.ChainVM
connMu sync.Mutex
connectedNodes set.Set[ids.NodeID]
// Context sync support - when a block fails verification due to missing context,
// we request the prerequisite blocks from the peer to catch up
pendingContext map[ids.ID]contextRequest // Map from blockID to pending context request
@@ -2888,21 +2723,13 @@ 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 /v1/health/liveness) would otherwise be a permanent brick.
// poll the always-green /ext/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 + bsAheadBeacons
bsMu sync.Mutex // guards bsFrontierCh + bsAncestorCh
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).
@@ -2999,7 +2826,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 /v1/health/liveness). A STRUCTURAL failure (deep
// it (all luxd probes poll the always-green /ext/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
@@ -3031,10 +2858,9 @@ type contextRequest struct {
timestamp time.Time
}
func newBlockHandler(vm consensuschain.BlockBuilder, connector chain.ChainVM, logger log.Logger, engine *consensuschain.Runtime, net network.Network, msgCreator message.OutboundMsgBuilder, chainID ids.ID, networkID ids.ID, beacons validators.Manager, selfNodeID ids.NodeID, expectsStakedBeacons bool) *blockHandler {
func newBlockHandler(vm consensuschain.BlockBuilder, logger log.Logger, engine *consensuschain.Runtime, net network.Network, msgCreator message.OutboundMsgBuilder, chainID ids.ID, networkID ids.ID, beacons validators.Manager, selfNodeID ids.NodeID, expectsStakedBeacons bool) *blockHandler {
return &blockHandler{
vm: vm,
connector: connector,
logger: logger,
engine: engine,
net: net,
@@ -3047,7 +2873,6 @@ func newBlockHandler(vm consensuschain.BlockBuilder, connector chain.ChainVM, lo
pendingContext: make(map[ids.ID]contextRequest),
maxContextBlocks: 256, // Default max context blocks to request/serve
pendingQbits: make(map[ids.ID][]QbitEvent),
connectedNodes: set.NewSet[ids.NodeID](16),
bsAncestorCh: make(map[uint32]chan [][]byte),
}
}
@@ -3269,45 +3094,14 @@ func (b *blockHandler) requestContext(ctx context.Context, nodeID ids.NodeID, bl
return
}
// 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
}
nodeSet := set.NewSet[ids.NodeID](1)
nodeSet.Add(nodeID)
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()))
}
@@ -3397,31 +3191,10 @@ 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).
//
// 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.
// Collect context blocks (walk parent chain)
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
@@ -3452,14 +3225,6 @@ 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
@@ -3495,8 +3260,6 @@ 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
@@ -3634,28 +3397,15 @@ func (b *blockHandler) AcceptedFrontier(ctx context.Context, nodeID ids.NodeID,
if b.deliverBootstrapFrontier(nodeID, containerID) {
return 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 _, err := b.vm.GetBlock(ctx, containerID); err == nil {
return nil // we already have the peer's tip — not behind
}
if b.engine != nil {
if _, found := b.engine.GetPendingBlock(containerID); found {
return nil // already tracked (pending) — the live path is handling it
return nil // already tracked
}
}
b.requestContext(ctx, nodeID, containerID) // behind (missing block OR its cert) → fetch the gap
b.requestContext(ctx, nodeID, containerID) // behind → fetch the gap
return nil
}
@@ -4066,69 +3816,9 @@ func (b *blockHandler) GetStateSummary(ctx context.Context, nodeID ids.NodeID, r
func (b *blockHandler) StateSummary(ctx context.Context, nodeID ids.NodeID, requestID uint32, summary []byte) error {
return nil
}
// Connected satisfies handler.Handler, whose interface carries no peer version.
// The real delivery path is ConnectedWithVersion (chainRouter uses it via the
// versionedConnector capability); this nil-version entry exists only for the
// interface contract and any non-router caller.
func (b *blockHandler) Connected(ctx context.Context, nodeID ids.NodeID) error {
return b.connect(ctx, nodeID, nil)
}
// ConnectedWithVersion forwards a peer connection WITH its real application
// version to this chain's VM. chainRouter invokes this (detecting the
// versionedConnector capability) so the version survives to the inner VM:
// proposervm promotes Connected to the C-Chain (coreth), whose state-sync peer
// tracker compares peer versions — a nil version there dereferences nil and
// panics a state-syncing node (a fresh join OR a validator rejoining after
// falling behind). This mirrors avalanchego, which delivers msg.NodeVersion to
// engine.Connected.
func (b *blockHandler) ConnectedWithVersion(ctx context.Context, nodeID ids.NodeID, nodeVersion *version.Application) error {
return b.connect(ctx, nodeID, nodeVersion)
}
// connect forwards a peer connection to this chain's VM exactly once, carrying
// nodeVersion (nil only on the interface path). The P-chain VM records it in its
// uptime tracker; the C-Chain/X-Chain VMs store the version for their peer sets.
// A nil connector makes this a no-op. On a forwarding error the dedup entry is
// rolled back so a subsequent dispatch of the same connection retries.
func (b *blockHandler) connect(ctx context.Context, nodeID ids.NodeID, nodeVersion *version.Application) error {
if b.connector == nil {
return nil
}
b.connMu.Lock()
if b.connectedNodes.Contains(nodeID) {
b.connMu.Unlock()
return nil
}
b.connectedNodes.Add(nodeID)
b.connMu.Unlock()
if err := b.connector.Connected(ctx, nodeID, nodeVersion); err != nil {
b.connMu.Lock()
b.connectedNodes.Remove(nodeID)
b.connMu.Unlock()
return err
}
return nil
}
// Disconnected forwards a peer disconnection to this chain's VM exactly once.
func (b *blockHandler) Disconnected(ctx context.Context, nodeID ids.NodeID) error {
if b.connector == nil {
return nil
}
b.connMu.Lock()
if !b.connectedNodes.Contains(nodeID) {
b.connMu.Unlock()
return nil
}
b.connectedNodes.Remove(nodeID)
b.connMu.Unlock()
return b.connector.Disconnected(ctx, nodeID)
}
func (b *blockHandler) HealthCheck(ctx context.Context) (interface{}, error) { return nil, nil }
func (b *blockHandler) Connected(ctx context.Context, nodeID ids.NodeID) error { return nil }
func (b *blockHandler) Disconnected(ctx context.Context, nodeID ids.NodeID) error { return nil }
func (b *blockHandler) HealthCheck(ctx context.Context) (interface{}, error) { return nil, nil }
func (b *blockHandler) Stop(ctx context.Context) {
if b.pollerCancel != nil {
b.pollerCancel()
@@ -4415,10 +4105,6 @@ 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
+2 -8
View File
@@ -123,14 +123,8 @@ 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 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.
// bootstrapped (created and tracked) on this node. If not, we cannot decide
// yet — signal the caller to defer.
if !m.IsBootstrapped(m.XChainID) {
return false, false
}
+5 -24
View File
@@ -9,12 +9,10 @@ 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"
@@ -165,10 +163,8 @@ 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 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.
// installed as the X-Chain's tracked VM so IsBootstrapped(XChainID) is true and
// xChainUTXOReader() resolves to it.
func newGateManager(
xChainID ids.ID,
stakingAddr ids.ShortID,
@@ -176,29 +172,17 @@ 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
}
@@ -294,14 +278,11 @@ 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 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.
// X-Chain is in m.chains (IsBootstrapped true) but its VM does not
// satisfy xChainUTXOReader. The gate must fail closed (ready, !authz),
// not panic.
m := newGateManager(xChainID, addr, collection, nil, nil)
m.chains[xChainID] = &chainInfo{Name: "X-Chain", VM: struct{}{}}
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,69 +177,6 @@ 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) {
+1 -15
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
// proposer window). It is the SINGLE policy gate (the manager calls it once);
// Snowman++ 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,16 +267,6 @@ 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) -----------------------
@@ -374,15 +364,11 @@ 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
+244
View File
@@ -0,0 +1,244 @@
# Robust RPC Handler Registration System
## Overview
This package provides a bulletproof RPC handler registration system for the Lux node, designed to handle the complexities of local development where nodes are frequently restarted. It replaces the fragile inline registration logic with a robust, maintainable solution.
## Key Features
### 🔄 Automatic Retry Logic
- Exponential backoff for transient failures
- Configurable retry count and wait times
- Context-aware cancellation support
### ✅ Built-in Health Checks
- Automatic validation after registration
- Batch health checking for all chains
- Detailed diagnostics for failures
### 🎯 Single Source of Truth
- Centralized route construction logic
- Consistent path formatting
- No duplicate code or magic strings
### 🛡️ Defensive Programming
- Nil checks on all inputs
- Handler validation before registration
- Graceful degradation on failures
### 📊 Developer-Friendly Debugging
- Clear, actionable error messages
- Comprehensive logging at appropriate levels
- Built-in diagnostic tools
## Architecture
```
┌─────────────────────┐
│ Chain Manager │
└──────────┬──────────┘
│ Creates Chain
┌─────────────────────┐
│ ChainHandlerRegistrar│
└──────────┬──────────┘
│ Extracts Handlers
┌─────────────────────┐
│ Handler Manager │
└──────────┬──────────┘
│ Registers with Retries
┌─────────────────────┐
│ API Server │
└─────────────────────┘
```
## Usage
### Basic Integration
Replace the handler registration code in `chains/manager.go` (lines 941-990) with:
```go
// Create robust registrar
registrar := rpc.NewChainHandlerRegistrar(
m.Server,
m.Log,
m.CChainID,
m.PChainID,
)
// Register handlers
if err := registrar.RegisterChainHandlers(ctx, chainParams.ID, chain.VM); err != nil {
m.Log.Error("Failed to register handlers", log.Err(err))
// Decide if this should be fatal or not
}
```
### Configuration
```go
// Development environment - fail fast
registrar.SetRetryConfig(2, 50*time.Millisecond)
// Production environment - more robust
registrar.SetRetryConfig(5, 200*time.Millisecond)
```
### Debugging
```go
// Get route information
info, exists := registrar.GetRouteInfo(chainID)
if exists {
fmt.Printf("Chain %s routes: %v\n", chainID, info.Endpoints)
}
// Run health checks
results := registrar.HealthCheckAll()
for chainID, healthy := range results {
fmt.Printf("Chain %s: %v\n", chainID, healthy)
}
// Validate specific endpoint
err := registrar.ValidateEndpoint(chainID, "/rpc")
```
### Using the Debug Tool
```go
// Quick diagnosis from CLI
rpc.QuickDiagnose("localhost:9650", chainID, "C")
// Programmatic diagnosis
tool := rpc.NewDebugTool("localhost:9650", logger)
report := tool.DiagnoseEndpoint(chainID, "C")
fmt.Println(report.String())
```
## Components
### HandlerManager (`handler_manager.go`)
Core registration logic with retry mechanism and health checks.
**Key Methods:**
- `RegisterChainHandlers()` - Main registration entry point
- `HealthCheckRoute()` - Validates handler responsiveness
- `GetRouteInfo()` - Retrieves registration details
### ChainHandlerRegistrar (`chain_integration.go`)
Bridge between chain manager and handler manager.
**Key Methods:**
- `RegisterChainHandlers()` - Extracts and registers handlers
- `ValidateEndpoint()` - Tests specific endpoints
- `GetAllRoutes()` - Returns all registered routes
### DebugTool (`debug_tool.go`)
Comprehensive endpoint diagnostics for developers.
**Key Methods:**
- `DiagnoseEndpoint()` - Full endpoint analysis
- `QuickDiagnose()` - CLI-friendly diagnosis
## Error Handling
The system uses clear, actionable errors:
```go
errNilHandler = errors.New("handler is nil")
errNilServer = errors.New("server is nil")
errEmptyEndpoint = errors.New("endpoint is empty")
errRegistrationFailed = errors.New("handler registration failed")
errHealthCheckFailed = errors.New("health check failed")
```
Each error includes context about what failed and why.
## Testing
Comprehensive test coverage including:
- Successful registration scenarios
- Validation failure cases
- Retry logic verification
- Health check validation
- Context cancellation
- Performance benchmarks
Run tests:
```bash
go test ./chains/rpc/... -v
```
## Common Issues and Solutions
### Issue: Handlers not accessible after registration
**Solution:** Check health status with `HealthCheckAll()` and review debug output.
### Issue: Registration fails with "already exists"
**Solution:** The retry logic handles this. If persistent, check for duplicate registration attempts.
### Issue: Slow registration during development
**Solution:** Reduce retry count and wait time using `SetRetryConfig()`.
### Issue: Can't find the correct endpoint URL
**Solution:** Use `DebugTool.DiagnoseEndpoint()` to test all URL patterns.
## Migration Guide
1. **Update imports:**
```go
import "github.com/luxfi/node/chains/rpc"
```
2. **Replace inline registration (lines 941-990 in manager.go):**
```go
// Old code: complex type checking and manual registration
// New code: single function call
registrar := rpc.NewChainHandlerRegistrar(...)
registrar.RegisterChainHandlers(...)
```
3. **Add health monitoring (optional):**
```go
go func() {
time.Sleep(5 * time.Second)
registrar.HealthCheckAll()
}()
```
4. **Add debugging endpoints (optional):**
```go
http.HandleFunc("/debug/handlers", func(w http.ResponseWriter, r *http.Request) {
routes := registrar.GetAllRoutes()
json.NewEncoder(w).Encode(routes)
})
```
## Performance
- Registration: ~1ms per handler (without retries)
- Health check: ~10ms per chain
- Memory overhead: ~1KB per registered chain
- No goroutine leaks or resource issues
## Future Improvements
Potential enhancements:
- Metrics integration for registration success/failure rates
- Automatic re-registration on failure
- WebSocket-specific health checks
- gRPC handler support
- Handler versioning for upgrades
## Philosophy
This implementation follows core Go principles:
- **Explicit over implicit** - Clear registration flow
- **Errors are values** - Proper error handling throughout
- **Simple over clever** - Straightforward retry logic
- **Composition over inheritance** - Small, focused components
- **Documentation is code** - Self-documenting with clear names
The system is designed to be bulletproof for development while remaining simple to understand and maintain.
+168
View File
@@ -0,0 +1,168 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpc
import (
"context"
"fmt"
"net/http"
"time"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/node/server/http"
"github.com/luxfi/node/vms"
)
// ChainHandlerRegistrar provides a clean interface for chain manager to register handlers.
// This replaces the inline registration logic with a more robust, testable solution.
type ChainHandlerRegistrar struct {
manager *HandlerManager
server server.Server
log log.Logger
cChainID ids.ID // Special handling for C-Chain
pChainID ids.ID // Platform chain ID for validation
}
// NewChainHandlerRegistrar creates a registrar for chain handler registration.
// Encapsulates all the registration logic in one place.
func NewChainHandlerRegistrar(
server server.Server,
logger log.Logger,
cChainID ids.ID,
pChainID ids.ID,
) *ChainHandlerRegistrar {
return &ChainHandlerRegistrar{
manager: NewHandlerManager(server, logger),
server: server,
log: logger,
cChainID: cChainID,
pChainID: pChainID,
}
}
// RegisterChainHandlers is the main entry point from chain manager.
// Handles all the complexity of VM type checking and handler extraction.
func (r *ChainHandlerRegistrar) RegisterChainHandlers(
ctx context.Context,
chainID ids.ID,
vm interface{},
) error {
r.log.Info("Attempting to register chain handlers",
log.Stringer("chainID", chainID),
log.String("vmType", fmt.Sprintf("%T", vm)))
// Don't register handlers for Platform VM
if chainID == r.pChainID {
r.log.Debug("Skipping handler registration for Platform VM")
return nil
}
// Extract handlers from VM
handlers, err := r.extractHandlers(ctx, vm)
if err != nil {
return fmt.Errorf("failed to extract handlers: %w", err)
}
if len(handlers) == 0 {
r.log.Info("VM does not provide any handlers",
log.Stringer("chainID", chainID))
return nil
}
// Determine chain alias (special case for C-Chain)
alias := r.getChainAlias(chainID)
// Register with robust handler manager
return r.manager.RegisterChainHandlers(ctx, chainID, alias, handlers)
}
// extractHandlers attempts to get handlers from the VM using multiple strategies.
// Handles different VM wrapper types gracefully.
func (r *ChainHandlerRegistrar) extractHandlers(
ctx context.Context,
vm interface{},
) (map[string]http.Handler, error) {
// First try direct interface check
if provider, ok := vm.(vms.HandlerProvider); ok {
r.log.Debug("VM directly implements HandlerProvider")
return provider.CreateHandlers(ctx)
}
// Try using the delegate helper (handles wrapped VMs)
handlers, err := vms.DelegateHandlers(ctx, vm)
if err != nil {
return nil, fmt.Errorf("handler delegation failed: %w", err)
}
if len(handlers) > 0 {
r.log.Debug("Successfully extracted handlers via delegation",
log.Int("count", len(handlers)))
}
return handlers, nil
}
// getChainAlias returns the appropriate alias for a chain.
// C-Chain gets special treatment, others use their ID.
func (r *ChainHandlerRegistrar) getChainAlias(chainID ids.ID) string {
if chainID == r.cChainID {
return "C"
}
// Could extend this for X-Chain and P-Chain if needed
return ""
}
// GetRouteInfo returns information about a specific chain's registered routes.
// Useful for debugging and operational visibility.
func (r *ChainHandlerRegistrar) GetRouteInfo(chainID ids.ID) (*RouteInfo, bool) {
return r.manager.GetRouteInfo(chainID)
}
// GetAllRoutes returns all registered routes across all chains.
// Complete visibility for monitoring and debugging.
func (r *ChainHandlerRegistrar) GetAllRoutes() map[string]*RouteInfo {
return r.manager.GetAllRoutes()
}
// HealthCheckAll performs health checks on all registered routes.
// Returns a map of chainID -> healthy status.
func (r *ChainHandlerRegistrar) HealthCheckAll() map[string]bool {
return r.manager.HealthCheckAll()
}
// SetRetryConfig allows tuning of retry behavior for different environments.
// Production might want more retries, dev might want faster failures.
func (r *ChainHandlerRegistrar) SetRetryConfig(maxRetries int, initialWait time.Duration) {
r.manager.SetRetryConfig(maxRetries, initialWait)
}
// ValidateEndpoint performs a test request against a specific endpoint.
// Useful for debugging specific handler issues.
func (r *ChainHandlerRegistrar) ValidateEndpoint(
chainID ids.ID,
endpoint string,
) error {
info, exists := r.manager.GetRouteInfo(chainID)
if !exists {
return fmt.Errorf("no routes registered for chain %s", chainID)
}
// Build the full URL
fullURL := fmt.Sprintf("/ext/%s%s", info.Base, endpoint)
r.log.Info("Validating endpoint",
log.Stringer("chainID", chainID),
log.String("url", fullURL))
// Validates endpoint registration
for _, registered := range info.Endpoints {
if registered == endpoint {
return nil
}
}
return fmt.Errorf("endpoint %s not found in registered endpoints: %v",
endpoint, info.Endpoints)
}
+302
View File
@@ -0,0 +1,302 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpc
import (
"bytes"
"github.com/go-json-experiment/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/luxfi/ids"
"github.com/luxfi/log"
)
// DebugTool provides utilities for debugging RPC handler issues.
// Developer-friendly diagnostics with clear, actionable output.
type DebugTool struct {
baseURL string
client *http.Client
log log.Logger
}
// NewDebugTool creates a debug tool for RPC endpoint testing.
func NewDebugTool(baseURL string, logger log.Logger) *DebugTool {
if !strings.HasPrefix(baseURL, "http") {
baseURL = "http://" + baseURL
}
return &DebugTool{
baseURL: strings.TrimSuffix(baseURL, "/"),
client: &http.Client{
Timeout: 10 * time.Second,
},
log: logger,
}
}
// DiagnoseEndpoint performs comprehensive diagnostics on an RPC endpoint.
// Returns detailed information about what's working and what's not.
func (d *DebugTool) DiagnoseEndpoint(chainID ids.ID, alias string) *DiagnosticReport {
report := &DiagnosticReport{
ChainID: chainID,
Alias: alias,
Timestamp: time.Now(),
Tests: make([]TestResult, 0),
}
// Test different URL patterns
urlPatterns := d.getURLPatterns(chainID, alias)
for _, pattern := range urlPatterns {
result := d.testEndpoint(pattern)
report.Tests = append(report.Tests, result)
}
// Test common RPC methods
if bestURL := report.GetBestURL(); bestURL != "" {
report.RPCTests = d.testRPCMethods(bestURL)
}
return report
}
// getURLPatterns returns all possible URL patterns to test.
func (d *DebugTool) getURLPatterns(chainID ids.ID, alias string) []string {
patterns := []string{
fmt.Sprintf("%s/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()),
}
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),
)
}
// Also test without /ext prefix (some setups might differ)
patterns = append(patterns,
fmt.Sprintf("%s/bc/%s/rpc", d.baseURL, chainID.String()),
)
return patterns
}
// testEndpoint tests a single endpoint URL.
func (d *DebugTool) testEndpoint(url string) TestResult {
result := TestResult{
URL: url,
Timestamp: time.Now(),
}
// First try a simple GET
resp, err := d.client.Get(url)
if err != nil {
result.Error = fmt.Sprintf("GET failed: %v", err)
result.Success = false
return result
}
defer resp.Body.Close()
result.StatusCode = resp.StatusCode
// Read body for debugging
bodyBytes, _ := io.ReadAll(resp.Body)
result.Response = string(bodyBytes)
// Now try a POST with JSON-RPC
rpcReq := map[string]interface{}{
"jsonrpc": "2.0",
"method": "web3_clientVersion",
"params": []interface{}{},
"id": 1,
}
jsonBytes, _ := json.Marshal(rpcReq)
postResp, err := d.client.Post(url, "application/json", bytes.NewReader(jsonBytes))
if err != nil {
result.Error = fmt.Sprintf("POST failed: %v", err)
result.Success = false
return result
}
defer postResp.Body.Close()
result.StatusCode = postResp.StatusCode
// Check if we got a valid JSON-RPC response
var rpcResp map[string]interface{}
if err := json.UnmarshalRead(postResp.Body, &rpcResp); err == nil {
if _, hasResult := rpcResp["result"]; hasResult {
result.Success = true
result.Response = fmt.Sprintf("Valid JSON-RPC response: %v", rpcResp["result"])
} else if errObj, hasError := rpcResp["error"]; hasError {
result.Success = false
result.Response = fmt.Sprintf("JSON-RPC error: %v", errObj)
}
}
return result
}
// testRPCMethods tests common RPC methods against an endpoint.
func (d *DebugTool) testRPCMethods(url string) []RPCTest {
methods := []string{
"web3_clientVersion",
"eth_blockNumber",
"eth_chainId",
"net_version",
"eth_syncing",
}
tests := make([]RPCTest, 0, len(methods))
for _, method := range methods {
test := RPCTest{
Method: method,
URL: url,
}
req := map[string]interface{}{
"jsonrpc": "2.0",
"method": method,
"params": []interface{}{},
"id": 1,
}
jsonBytes, _ := json.Marshal(req)
resp, err := d.client.Post(url, "application/json", bytes.NewReader(jsonBytes))
if err != nil {
test.Error = err.Error()
test.Success = false
} else {
defer resp.Body.Close()
var result map[string]interface{}
if err := json.UnmarshalRead(resp.Body, &result); err == nil {
if res, ok := result["result"]; ok {
test.Success = true
test.Result = fmt.Sprintf("%v", res)
} else if errObj, ok := result["error"]; ok {
test.Error = fmt.Sprintf("%v", errObj)
}
}
}
tests = append(tests, test)
}
return tests
}
// DiagnosticReport contains comprehensive endpoint diagnostic information.
type DiagnosticReport struct {
ChainID ids.ID
Alias string
Timestamp time.Time
Tests []TestResult
RPCTests []RPCTest
}
// TestResult represents a single endpoint test result.
type TestResult struct {
URL string
Success bool
StatusCode int
Response string
Error string
Timestamp time.Time
}
// RPCTest represents a test of a specific RPC method.
type RPCTest struct {
Method string
URL string
Success bool
Result string
Error string
}
// GetBestURL returns the first working URL from the tests.
func (r *DiagnosticReport) GetBestURL() string {
for _, test := range r.Tests {
if test.Success {
return test.URL
}
}
return ""
}
// String returns a human-readable report.
func (r *DiagnosticReport) String() string {
var b strings.Builder
b.WriteString(fmt.Sprintf("=== RPC Endpoint Diagnostic Report ===\n"))
b.WriteString(fmt.Sprintf("Chain ID: %s\n", r.ChainID))
if r.Alias != "" {
b.WriteString(fmt.Sprintf("Alias: %s\n", r.Alias))
}
b.WriteString(fmt.Sprintf("Timestamp: %s\n\n", r.Timestamp.Format(time.RFC3339)))
b.WriteString("=== Endpoint Tests ===\n")
for _, test := range r.Tests {
status := "❌ FAILED"
if test.Success {
status = "✅ SUCCESS"
}
b.WriteString(fmt.Sprintf("\n%s %s\n", status, test.URL))
b.WriteString(fmt.Sprintf(" Status Code: %d\n", test.StatusCode))
if test.Error != "" {
b.WriteString(fmt.Sprintf(" Error: %s\n", test.Error))
}
if test.Response != "" && len(test.Response) < 200 {
b.WriteString(fmt.Sprintf(" Response: %s\n", test.Response))
}
}
if len(r.RPCTests) > 0 {
b.WriteString("\n=== RPC Method Tests ===\n")
for _, test := range r.RPCTests {
status := "❌"
if test.Success {
status = "✅"
}
b.WriteString(fmt.Sprintf("%s %s: ", status, test.Method))
if test.Success {
b.WriteString(test.Result)
} else {
b.WriteString(test.Error)
}
b.WriteString("\n")
}
}
b.WriteString("\n=== Recommendations ===\n")
if bestURL := r.GetBestURL(); bestURL != "" {
b.WriteString(fmt.Sprintf("✅ Use this endpoint: %s\n", bestURL))
} else {
b.WriteString("❌ No working endpoints found. Check:\n")
b.WriteString(" 1. Is the node running?\n")
b.WriteString(" 2. Is the chain bootstrapped?\n")
b.WriteString(" 3. Are handlers properly registered?\n")
b.WriteString(" 4. Check node logs for handler registration errors\n")
b.WriteString(" 5. Try restarting the node\n")
}
return b.String()
}
// QuickDiagnose performs a quick endpoint check and prints results.
// Convenience function for CLI tools.
func QuickDiagnose(nodeURL string, chainID ids.ID, alias string) {
logger := log.NewNoOpLogger()
tool := NewDebugTool(nodeURL, logger)
report := tool.DiagnoseEndpoint(chainID, alias)
fmt.Println(report.String())
}
+324
View File
@@ -0,0 +1,324 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package rpc provides robust RPC handler registration with retries, health checks, and clear debugging.
// Follows Go principles: fail fast with clear errors, single responsibility, minimal dependencies.
package rpc
import (
"context"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"sync"
"time"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/node/server/http"
)
var (
// Errors follow Go convention: lowercase, descriptive, actionable
errNilHandler = errors.New("handler is nil")
errNilServer = errors.New("server is nil")
errEmptyEndpoint = errors.New("endpoint is empty")
errRegistrationFailed = errors.New("handler registration failed")
errHealthCheckFailed = errors.New("health check failed")
)
// HandlerManager manages RPC handler registration with robust error handling and health checks.
// Single responsibility: reliable handler registration with observability.
type HandlerManager struct {
server server.Server
log log.Logger
mu sync.RWMutex
routes map[string]*RouteInfo // chainID -> route info
retries int // max registration retries
retryWait time.Duration // initial retry wait time
}
// RouteInfo contains complete information about a registered route.
// Everything needed for debugging in one place.
type RouteInfo struct {
ChainID ids.ID
ChainAlias string
Base string // e.g., "bc/C" or "bc/<chainID>"
Endpoints []string // e.g., ["/rpc", "/ws"]
Handler http.Handler
Healthy bool
LastCheck time.Time
}
// NewHandlerManager creates a handler manager with sensible defaults.
// Simple factory, no magic.
func NewHandlerManager(server server.Server, logger log.Logger) *HandlerManager {
return &HandlerManager{
server: server,
log: logger,
routes: make(map[string]*RouteInfo),
retries: 3,
retryWait: 100 * time.Millisecond,
}
}
// RegisterChainHandlers registers all handlers for a chain with retry logic and health checks.
// This is the main entry point - handles everything needed for robust registration.
func (m *HandlerManager) RegisterChainHandlers(
ctx context.Context,
chainID ids.ID,
chainAlias string,
handlers map[string]http.Handler,
) error {
if m.server == nil {
return errNilServer
}
m.log.Info("Starting chain handler registration",
log.Stringer("chainID", chainID),
log.String("alias", chainAlias),
log.Int("handlerCount", len(handlers)))
// Validate handlers first - fail fast
if err := m.validateHandlers(handlers); err != nil {
return fmt.Errorf("handler validation failed: %w", err)
}
// Build route info
info := &RouteInfo{
ChainID: chainID,
ChainAlias: chainAlias,
Endpoints: make([]string, 0, len(handlers)),
}
// Determine base paths
bases := m.getBasePaths(chainID, chainAlias)
// Register each handler with retries
var registrationErrors []error
for endpoint, handler := range handlers {
info.Endpoints = append(info.Endpoints, endpoint)
for _, base := range bases {
if err := m.registerWithRetry(ctx, base, endpoint, handler); err != nil {
registrationErrors = append(registrationErrors,
fmt.Errorf("failed to register %s%s: %w", base, endpoint, err))
m.log.Error("Handler registration failed",
log.String("base", base),
log.String("endpoint", endpoint),
log.Err(err))
} else {
m.log.Info("Handler registered successfully",
log.String("route", fmt.Sprintf("/ext/%s%s", base, endpoint)),
log.Stringer("chainID", chainID))
}
}
}
// Store route info for monitoring
m.mu.Lock()
info.Base = bases[0] // Primary base
info.Handler = handlers["/rpc"] // Store primary handler for health checks
m.routes[chainID.String()] = info
m.mu.Unlock()
// Run health checks
if err := m.healthCheckRoute(info); err != nil {
m.log.Warn("Health check failed for newly registered chain",
log.Stringer("chainID", chainID),
log.Err(err))
}
// Return aggregate error if any registrations failed
if len(registrationErrors) > 0 {
return fmt.Errorf("%w: %v", errRegistrationFailed, registrationErrors)
}
m.log.Info("Chain handler registration completed",
log.Stringer("chainID", chainID),
log.String("routes", strings.Join(m.getFullRoutes(bases, info.Endpoints), ", ")))
return nil
}
// validateHandlers ensures all handlers are valid before attempting registration.
// Fail fast with clear errors - no silent failures.
func (m *HandlerManager) validateHandlers(handlers map[string]http.Handler) error {
if len(handlers) == 0 {
return errors.New("no handlers provided")
}
for endpoint, handler := range handlers {
if handler == nil {
return fmt.Errorf("%w for endpoint %s", errNilHandler, endpoint)
}
if endpoint == "" {
return errEmptyEndpoint
}
// Ensure endpoint starts with /
if !strings.HasPrefix(endpoint, "/") {
return fmt.Errorf("endpoint %s must start with /", endpoint)
}
}
return nil
}
// getBasePaths returns all base paths for a chain (with and without alias).
// Single source of truth for path construction.
func (m *HandlerManager) getBasePaths(chainID ids.ID, chainAlias string) []string {
bases := []string{}
// If we have an alias (like "C" for C-Chain), use it as primary
if chainAlias != "" && chainAlias != chainID.String() {
bases = append(bases, fmt.Sprintf("bc/%s", chainAlias))
}
// Always include the full chain ID path
bases = append(bases, fmt.Sprintf("bc/%s", chainID.String()))
return bases
}
// getFullRoutes constructs full route paths for logging.
// Clear, complete information for operators.
func (m *HandlerManager) getFullRoutes(bases []string, endpoints []string) []string {
routes := []string{}
for _, base := range bases {
for _, endpoint := range endpoints {
routes = append(routes, fmt.Sprintf("/ext/%s%s", base, endpoint))
}
}
return routes
}
// registerWithRetry attempts registration with exponential backoff.
// Handles transient failures gracefully.
func (m *HandlerManager) registerWithRetry(
ctx context.Context,
base string,
endpoint string,
handler http.Handler,
) error {
wait := m.retryWait
var lastErr error
for attempt := 0; attempt < m.retries; attempt++ {
// Check context cancellation
select {
case <-ctx.Done():
return ctx.Err()
default:
}
// Try registration
if err := m.server.AddRoute(handler, base, endpoint); err == nil {
return nil // Success!
} else {
lastErr = err
m.log.Debug("Registration attempt failed, retrying",
log.Int("attempt", attempt+1),
log.String("base", base),
log.String("endpoint", endpoint),
log.Err(err))
}
// Don't wait after last attempt
if attempt < m.retries-1 {
select {
case <-time.After(wait):
wait *= 2 // Exponential backoff
case <-ctx.Done():
return ctx.Err()
}
}
}
return fmt.Errorf("failed after %d attempts: %w", m.retries, lastErr)
}
// healthCheckRoute performs a basic health check on a registered route.
// Validates that handlers are actually responding.
func (m *HandlerManager) healthCheckRoute(info *RouteInfo) error {
if info.Handler == nil {
return fmt.Errorf("no handler to check for chain %s", info.ChainID)
}
// Create a test request
req := httptest.NewRequest("POST", "/", strings.NewReader(`{"jsonrpc":"2.0","method":"web3_clientVersion","params":[],"id":1}`))
req.Header.Set("Content-Type", "application/json")
// Record the response
recorder := httptest.NewRecorder()
// Call the handler
info.Handler.ServeHTTP(recorder, req)
// Check response
info.LastCheck = time.Now()
if recorder.Code == http.StatusOK || recorder.Code == http.StatusMethodNotAllowed {
info.Healthy = true
m.log.Debug("Health check passed",
log.Stringer("chainID", info.ChainID),
log.Int("status", recorder.Code))
return nil
}
info.Healthy = false
return fmt.Errorf("%w: status %d", errHealthCheckFailed, recorder.Code)
}
// GetRouteInfo returns information about a registered chain's routes.
// Useful for debugging and monitoring.
func (m *HandlerManager) GetRouteInfo(chainID ids.ID) (*RouteInfo, bool) {
m.mu.RLock()
defer m.mu.RUnlock()
info, exists := m.routes[chainID.String()]
return info, exists
}
// GetAllRoutes returns all registered route information.
// Complete visibility for operators.
func (m *HandlerManager) GetAllRoutes() map[string]*RouteInfo {
m.mu.RLock()
defer m.mu.RUnlock()
// Return a copy to prevent external modification
routes := make(map[string]*RouteInfo, len(m.routes))
for k, v := range m.routes {
routes[k] = v
}
return routes
}
// HealthCheckAll performs health checks on all registered routes.
// Batch operation for monitoring systems.
func (m *HandlerManager) HealthCheckAll() map[string]bool {
m.mu.RLock()
routes := make([]*RouteInfo, 0, len(m.routes))
for _, info := range m.routes {
routes = append(routes, info)
}
m.mu.RUnlock()
results := make(map[string]bool)
for _, info := range routes {
err := m.healthCheckRoute(info)
results[info.ChainID.String()] = err == nil
}
return results
}
// SetRetryConfig allows customization of retry behavior.
// Flexibility for different deployment scenarios.
func (m *HandlerManager) SetRetryConfig(maxRetries int, initialWait time.Duration) {
if maxRetries > 0 {
m.retries = maxRetries
}
if initialWait > 0 {
m.retryWait = initialWait
}
}
+291
View File
@@ -0,0 +1,291 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpc
import (
"context"
"errors"
"net/http"
"testing"
"time"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/node/server/http"
"github.com/luxfi/runtime"
"github.com/luxfi/vm"
"github.com/stretchr/testify/require"
)
// mockServer implements a test server for handler registration
type mockServer struct {
routes map[string]http.Handler
failCount int
maxFailures int
returnError error
aliases map[string][]string
}
func newMockServer() *mockServer {
return &mockServer{
routes: make(map[string]http.Handler),
maxFailures: 0,
aliases: make(map[string][]string),
}
}
func (s *mockServer) AddRoute(handler http.Handler, base, endpoint string) error {
// Simulate transient failures for retry testing
if s.failCount < s.maxFailures {
s.failCount++
return errors.New("transient failure")
}
// Return configured error if any
if s.returnError != nil {
return s.returnError
}
// Store the route
key := base + endpoint
s.routes[key] = handler
return nil
}
func (s *mockServer) AddAliases(endpoint string, aliases ...string) error {
s.aliases[endpoint] = aliases
return nil
}
func (s *mockServer) AddRouteWithReadLock(handler http.Handler, base, endpoint string) error {
return s.AddRoute(handler, base, endpoint)
}
func (s *mockServer) AddAliasesWithReadLock(endpoint string, aliases ...string) error {
return s.AddAliases(endpoint, aliases...)
}
func (s *mockServer) Dispatch() error { return nil }
func (s *mockServer) RegisterChain(chainName string, rt *runtime.Runtime, vm vm.VM) {
}
func (s *mockServer) Shutdown() error { return nil }
func (s *mockServer) SetRootInfoProvider(_ server.RootInfoProvider) {}
func TestHandlerManager_RegisterChainHandlers(t *testing.T) {
tests := []struct {
name string
chainID ids.ID
chainAlias string
handlers map[string]http.Handler
serverError error
expectError bool
expectRoutes int
}{
{
name: "successful registration with alias",
chainID: ids.GenerateTestID(),
chainAlias: "C",
handlers: map[string]http.Handler{
"/rpc": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}),
"/ws": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}),
},
expectError: false,
expectRoutes: 4, // 2 endpoints × 2 bases (alias + ID)
},
{
name: "successful registration without alias",
chainID: ids.GenerateTestID(),
handlers: map[string]http.Handler{
"/rpc": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}),
},
expectError: false,
expectRoutes: 1,
},
{
name: "nil handler validation",
chainID: ids.GenerateTestID(),
chainAlias: "X",
handlers: map[string]http.Handler{"/rpc": nil},
expectError: true,
},
{
name: "empty endpoint validation",
chainID: ids.GenerateTestID(),
handlers: map[string]http.Handler{"": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})},
expectError: true,
},
{
name: "no handlers provided",
chainID: ids.GenerateTestID(),
handlers: map[string]http.Handler{},
expectError: true,
},
{
name: "invalid endpoint format",
chainID: ids.GenerateTestID(),
handlers: map[string]http.Handler{"rpc": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup
server := newMockServer()
server.returnError = tt.serverError
logger := log.NewNoOpLogger()
manager := NewHandlerManager(server, logger)
// Execute
ctx := context.Background()
err := manager.RegisterChainHandlers(ctx, tt.chainID, tt.chainAlias, tt.handlers)
// Verify
if tt.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Len(t, server.routes, tt.expectRoutes)
// Verify route info was stored
info, exists := manager.GetRouteInfo(tt.chainID)
require.True(t, exists)
require.Equal(t, tt.chainID, info.ChainID)
require.Equal(t, tt.chainAlias, info.ChainAlias)
}
})
}
}
func TestHandlerManager_RetryLogic(t *testing.T) {
// Setup server that fails twice then succeeds
server := newMockServer()
server.maxFailures = 2
logger := log.NewNoOpLogger()
manager := NewHandlerManager(server, logger)
manager.SetRetryConfig(3, 10*time.Millisecond)
// Create test handler
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
// Register with retries
ctx := context.Background()
chainID := ids.GenerateTestID()
require.NoError(t, manager.RegisterChainHandlers(ctx, chainID, "TEST", map[string]http.Handler{
"/rpc": handler,
}))
require.Equal(t, 2, server.failCount) // Failed twice, succeeded on third try
require.Len(t, server.routes, 2) // Both alias and ID routes
}
func TestHandlerManager_HealthCheck(t *testing.T) {
server := newMockServer()
logger := log.NewNoOpLogger()
manager := NewHandlerManager(server, logger)
// Register a healthy handler
healthyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"jsonrpc":"2.0","result":"test","id":1}`))
})
// Register an unhealthy handler
unhealthyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
})
ctx := context.Background()
chainID1 := ids.GenerateTestID()
chainID2 := ids.GenerateTestID()
// Register healthy chain
require.NoError(t, manager.RegisterChainHandlers(ctx, chainID1, "", map[string]http.Handler{
"/rpc": healthyHandler,
}))
// Register unhealthy chain
// Registration succeeds even if health check fails
require.NoError(t, manager.RegisterChainHandlers(ctx, chainID2, "", map[string]http.Handler{
"/rpc": unhealthyHandler,
})) // Registration should succeed regardless of handler health
// Check health status
results := manager.HealthCheckAll()
require.True(t, results[chainID1.String()])
require.False(t, results[chainID2.String()])
}
func TestHandlerManager_GetBasePaths(t *testing.T) {
manager := &HandlerManager{}
chainID := ids.GenerateTestID()
// Test with alias
bases := manager.getBasePaths(chainID, "C")
require.Equal(t, []string{"bc/C", "bc/" + chainID.String()}, bases)
// Test without alias
bases = manager.getBasePaths(chainID, "")
require.Equal(t, []string{"bc/" + chainID.String()}, bases)
// Test when alias equals chain ID (shouldn't duplicate)
bases = manager.getBasePaths(chainID, chainID.String())
require.Equal(t, []string{"bc/" + chainID.String()}, bases)
}
func TestHandlerManager_ContextCancellation(t *testing.T) {
// Create a server that delays to test cancellation
server := &mockServer{
routes: make(map[string]http.Handler),
returnError: errors.New("slow server"),
}
logger := log.NewNoOpLogger()
manager := NewHandlerManager(server, logger)
manager.SetRetryConfig(10, 100*time.Millisecond) // Many retries with delays
// Create cancelled context
ctx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
// Try to register - should fail with context error
chainID := ids.GenerateTestID()
err := manager.RegisterChainHandlers(ctx, chainID, "", map[string]http.Handler{
"/rpc": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}),
})
require.Error(t, err)
require.Contains(t, err.Error(), "context canceled")
}
// Benchmark to ensure performance doesn't degrade
func BenchmarkHandlerRegistration(b *testing.B) {
server := newMockServer()
logger := log.NewNoOpLogger()
manager := NewHandlerManager(server, logger)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
ctx := context.Background()
b.ResetTimer()
for i := 0; i < b.N; i++ {
chainID := ids.GenerateTestID()
manager.RegisterChainHandlers(ctx, chainID, "TEST", map[string]http.Handler{
"/rpc": handler,
"/ws": handler,
})
}
}
+165
View File
@@ -0,0 +1,165 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpc
import (
"context"
"fmt"
"time"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/node/server/http"
)
// IntegrationExample shows how to modify the existing createChain function in manager.go.
// This replaces lines 941-990 with cleaner, more robust code.
func IntegrationExample(
ctx context.Context,
chainID ids.ID,
vm interface{},
server server.Server,
logger log.Logger,
cChainID ids.ID,
pChainID ids.ID,
isDevMode bool,
) error {
// BEFORE: 50+ lines of complex type checking and error-prone registration
// AFTER: Clean, robust registration with proper error handling
// Step 1: Create the registrar
registrar := NewChainHandlerRegistrar(server, logger, cChainID, pChainID)
// Step 2: Configure based on environment
if isDevMode {
// Development: Fast failures for quick iteration
registrar.SetRetryConfig(2, 50*time.Millisecond)
logger.Info("Using development handler registration settings")
} else {
// Production: More robust with retries
registrar.SetRetryConfig(5, 200*time.Millisecond)
logger.Info("Using production handler registration settings")
}
// Step 3: Register handlers (replaces all the complex VM type checking)
startTime := time.Now()
err := registrar.RegisterChainHandlers(ctx, chainID, vm)
duration := time.Since(startTime)
// Step 4: Handle registration result
if err != nil {
// Log error but don't fail chain creation
// Handlers are not critical for chain operation
logger.Error("RPC handler registration failed",
log.Stringer("chainID", chainID),
log.Err(err),
log.Duration("duration", duration),
log.String("action", "Chain will operate without HTTP/RPC access"))
// Could emit metrics here if available
// metric.HandlerRegistrationFailed.Inc()
// Non-fatal: return nil to allow chain to continue
// Change to 'return err' if you want this to be fatal
return nil
}
// Step 5: Log success with useful information
if info, exists := registrar.GetRouteInfo(chainID); exists {
logger.Info("RPC handlers registered successfully",
log.Stringer("chainID", chainID),
log.String("alias", info.ChainAlias),
log.Strings("endpoints", info.Endpoints),
log.Duration("duration", duration),
log.Bool("healthCheckPassed", info.Healthy))
// Print developer-friendly message
if isDevMode && len(info.Endpoints) > 0 {
baseURL := "http://localhost:9630"
fmt.Printf("\n✅ Chain %s RPC endpoints ready:\n", chainID)
for _, endpoint := range info.Endpoints {
if info.ChainAlias != "" {
fmt.Printf(" %s/ext/bc/%s%s\n", baseURL, info.ChainAlias, endpoint)
}
fmt.Printf(" %s/ext/bc/%s%s\n", baseURL, chainID, endpoint)
}
fmt.Println()
}
}
// Step 6: Schedule async health monitoring (optional)
if !isDevMode {
go monitorHandlerHealth(ctx, registrar, chainID, logger)
}
return nil
}
// monitorHandlerHealth runs periodic health checks in the background.
// This helps detect and log handler issues early.
func monitorHandlerHealth(
ctx context.Context,
registrar *ChainHandlerRegistrar,
chainID ids.ID,
logger log.Logger,
) {
// Initial delay to let chain fully initialize
select {
case <-time.After(10 * time.Second):
case <-ctx.Done():
return
}
// Run initial health check
checkHealth(registrar, chainID, logger)
// Periodic health checks
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for {
select {
case <-ticker.C:
checkHealth(registrar, chainID, logger)
case <-ctx.Done():
return
}
}
}
// checkHealth performs a health check and logs results.
func checkHealth(registrar *ChainHandlerRegistrar, chainID ids.ID, logger log.Logger) {
results := registrar.HealthCheckAll()
for chainIDStr, healthy := range results {
if chainIDStr == chainID.String() {
if healthy {
logger.Debug("Handler health check passed",
log.String("chainID", chainIDStr))
} else {
logger.Warn("Handler health check failed",
log.String("chainID", chainIDStr),
log.String("action", "Will continue monitoring"))
}
}
}
}
// MinimalIntegration shows the absolute minimum code needed.
// This is what you'd actually put in manager.go.
func MinimalIntegration(
ctx context.Context,
chainID ids.ID,
vm interface{},
server server.Server,
logger log.Logger,
cChainID ids.ID,
) error {
// Just three lines to replace 50+ lines of complex code!
registrar := NewChainHandlerRegistrar(server, logger, cChainID, ids.Empty)
if err := registrar.RegisterChainHandlers(ctx, chainID, vm); err != nil {
logger.Error("Handler registration failed", log.Err(err))
}
return nil // Non-fatal
}
+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/v1/health"]
test: ["CMD", "curl", "-sf", "http://localhost:9630/ext/health"]
interval: 30s
timeout: 10s
retries: 5
-8
View File
@@ -1762,14 +1762,6 @@ func GetNodeConfig(v *viper.Viper) (node.Config, error) {
if nodeConfig.HealthCheckFreq < 0 {
return node.Config{}, fmt.Errorf("%s must be positive", HealthCheckFreqKey)
}
nodeConfig.ProposerWindowDuration = v.GetDuration(ProposerVMWindowDurationKey)
if nodeConfig.ProposerWindowDuration < 0 {
return node.Config{}, fmt.Errorf("%s must not be negative", ProposerVMWindowDurationKey)
}
nodeConfig.ProposerMinBlockDelay = v.GetDuration(ProposerVMMinBlockDelayKey)
if nodeConfig.ProposerMinBlockDelay < 0 {
return node.Config{}, fmt.Errorf("%s must not be negative", ProposerVMMinBlockDelayKey)
}
// Halflife of continuous averager used in health checks
healthCheckAveragerHalflife := v.GetDuration(HealthCheckAveragerHalflifeKey)
if healthCheckAveragerHalflife <= 0 {
+6 -5
View File
@@ -24,6 +24,7 @@ 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"
@@ -535,8 +536,8 @@ func TestGetNetConfigsFromFlags(t *testing.T) {
"2Ctt6eGAeo4MLqTmGa7AdRecuVMPGWEX9wSsCLBYrLhX4a394i": {
"consensusParameters": {
"k": 30,
"alphaPreference": 20,
"alphaConfidence": 25
"alphaPreference": 16,
"alphaConfidence": 20
},
"validatorOnly": true
}
@@ -546,8 +547,8 @@ func TestGetNetConfigsFromFlags(t *testing.T) {
config, ok := given[id]
require.True(ok)
require.True(config.ValidatorOnly)
require.Equal(20, config.ConsensusParameters.AlphaPreference)
require.Equal(25, config.ConsensusParameters.AlphaConfidence)
require.Equal(16, config.ConsensusParameters.AlphaPreference)
require.Equal(20, config.ConsensusParameters.AlphaConfidence)
require.Equal(30, config.ConsensusParameters.K)
// must still respect defaults (MainnetParameters.MaxOutstandingItems = 1024)
require.Equal(1024, config.ConsensusParameters.MaxOutstandingItems)
@@ -732,7 +733,7 @@ func TestResolveUTXOAssetID_POnlyFallback(t *testing.T) {
require := require.New(t)
pOnly := &pchaingenesis.Genesis{Chains: nil}
pOnlyBytes, err := pOnly.Bytes()
pOnlyBytes, err := pchaingenesis.Codec.Marshal(pchaintxs.CodecVersion, pOnly)
require.NoError(err)
gotID, err := resolveUTXOAssetID(42, pOnlyBytes)
-1
View File
@@ -369,7 +369,6 @@ func addNodeFlags(fs *pflag.FlagSet) {
// ProposerVM
fs.Bool(ProposerVMUseCurrentHeightKey, false, "Have the ProposerVM always report the last accepted P-chain block height")
fs.Duration(ProposerVMMinBlockDelayKey, proposervm.DefaultMinBlockDelay, "Minimum delay to enforce when building a chain++ block for the primary network chains and the default minimum delay for chains")
fs.Duration(ProposerVMWindowDurationKey, 0, "Proposer-slot spacing for block production (0 uses the 5s mainnet default); shrink for fast cadence on small/local networks")
// Metrics
fs.Bool(MeterVMsEnabledKey, true, "Enable Meter VMs to track VM performance with more granularity")
-1
View File
@@ -187,7 +187,6 @@ const (
ConsensusFrontierPollFrequencyKey = "consensus-frontier-poll-frequency"
ProposerVMUseCurrentHeightKey = "proposervm-use-current-height"
ProposerVMMinBlockDelayKey = "proposervm-min-block-delay"
ProposerVMWindowDurationKey = "proposervm-window-duration"
FdLimitKey = "fd-limit"
IndexEnabledKey = "index-enabled"
IndexAllowIncompleteKey = "index-allow-incomplete"
-10
View File
@@ -177,16 +177,6 @@ type Config struct {
// Health
HealthCheckFreq time.Duration `json:"healthCheckFreq"`
// ProposerWindowDuration overrides the proposervm proposer-slot spacing.
// Zero keeps the 5s mainnet default; small local/dev nets set it low (e.g.
// 1s) so block cadence is not floored at 5s per proposer slot.
ProposerWindowDuration time.Duration `json:"proposerWindowDuration"`
// ProposerMinBlockDelay is the proposervm minimum delay between consecutive
// blocks (the hard cadence floor). Zero keeps the 1s default; high-throughput
// / DEX nets set it low (e.g. 1ms) to approach the consensus-finality floor.
ProposerMinBlockDelay time.Duration `json:"proposerMinBlockDelay"`
// Network configuration
NetworkConfig network.Config `json:"networkConfig"`
+5 -5
View File
@@ -133,12 +133,12 @@ deploy:
### Prometheus Metrics
The node exposes metrics at `http://localhost:9630/v1/metrics`
The node exposes metrics at `http://localhost:9630/ext/metrics`
### Health Checks
- Liveness: `http://localhost:9630/v1/health`
- Readiness: `http://localhost:9630/v1/info`
- Liveness: `http://localhost:9630/ext/health`
- Readiness: `http://localhost:9630/ext/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/v1/info
http://localhost:9630/ext/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/v1/bc/C/rpc
http://localhost:9630/ext/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/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 " - 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 ""
# 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/v1/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/ext/health
</code>
</pre>
</div>
+20 -20
View File
@@ -31,7 +31,7 @@ Or in configuration:
## Endpoint
```
http://localhost:9630/v1/admin
http://localhost:9630/ext/admin
```
## Methods
@@ -54,7 +54,7 @@ curl -X POST --data '{
"chain":"2S53R2ub94CV5vmSRAjqPYmRxvuiFunCb1gN2CAw3DQBfPWghX"
},
"id":1
}' -H 'content-type:application/json;' http://localhost:9630/v1/admin
}' -H 'content-type:application/json;' http://localhost:9630/ext/admin
```
---
@@ -85,7 +85,7 @@ curl -X POST --data '{
"chain":"2S53R2ub94CV5vmSRAjqPYmRxvuiFunCb1gN2CAw3DQBfPWghX"
},
"id":1
}' -H 'content-type:application/json;' http://localhost:9630/v1/admin
}' -H 'content-type:application/json;' http://localhost:9630/ext/admin
```
**Response:**
@@ -114,7 +114,7 @@ curl -X POST --data '{
"method":"admin.getLogLevel",
"params":{},
"id":1
}' -H 'content-type:application/json;' http://localhost:9630/v1/admin
}' -H 'content-type:application/json;' http://localhost:9630/ext/admin
```
**Response:**
@@ -146,7 +146,7 @@ curl -X POST --data '{
"logLevel":"debug"
},
"id":1
}' -H 'content-type:application/json;' http://localhost:9630/v1/admin
}' -H 'content-type:application/json;' http://localhost:9630/ext/admin
```
---
@@ -181,7 +181,7 @@ curl -X POST --data '{
"method":"admin.loadVMs",
"params":{},
"id":1
}' -H 'content-type:application/json;' http://localhost:9630/v1/admin
}' -H 'content-type:application/json;' http://localhost:9630/ext/admin
```
**Response:**
@@ -214,7 +214,7 @@ curl -X POST --data '{
"method":"admin.startCPUProfiler",
"params":{},
"id":1
}' -H 'content-type:application/json;' http://localhost:9630/v1/admin
}' -H 'content-type:application/json;' http://localhost:9630/ext/admin
```
---
@@ -232,7 +232,7 @@ curl -X POST --data '{
"method":"admin.stopCPUProfiler",
"params":{},
"id":1
}' -H 'content-type:application/json;' http://localhost:9630/v1/admin
}' -H 'content-type:application/json;' http://localhost:9630/ext/admin
```
---
@@ -250,7 +250,7 @@ curl -X POST --data '{
"method":"admin.memoryProfile",
"params":{},
"id":1
}' -H 'content-type:application/json;' http://localhost:9630/v1/admin
}' -H 'content-type:application/json;' http://localhost:9630/ext/admin
```
---
@@ -276,7 +276,7 @@ curl -X POST --data '{
"method":"admin.getConfig",
"params":{},
"id":1
}' -H 'content-type:application/json;' http://localhost:9630/v1/admin
}' -H 'content-type:application/json;' http://localhost:9630/ext/admin
```
**Response:**
@@ -311,7 +311,7 @@ curl -X POST --data '{
"method":"admin.shutdown",
"params":{},
"id":1
}' -H 'content-type:application/json;' http://localhost:9630/v1/admin
}' -H 'content-type:application/json;' http://localhost:9630/ext/admin
```
---
@@ -330,7 +330,7 @@ curl -X POST --data '{
"method":"admin.db.commit",
"params":{},
"id":1
}' -H 'content-type:application/json;' http://localhost:9630/v1/admin
}' -H 'content-type:application/json;' http://localhost:9630/ext/admin
```
## Profiling and Debugging
@@ -344,7 +344,7 @@ curl -X POST --data '{
"method":"admin.startCPUProfiler",
"params":{},
"id":1
}' http://localhost:9630/v1/admin
}' http://localhost:9630/ext/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/v1/admin
}' http://localhost:9630/ext/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/v1/admin
}' http://localhost:9630/ext/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/v1/admin
}" http://localhost:9630/ext/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/v1/health | jq -r '.healthy'
curl -s http://localhost:9630/ext/health | jq -r '.healthy'
}
restart_node() {
@@ -488,7 +488,7 @@ restart_node() {
"method":"admin.shutdown",
"params":{},
"id":1
}' http://localhost:9630/v1/admin
}' http://localhost:9630/ext/admin
sleep 10
@@ -514,7 +514,7 @@ CURRENT=$(curl -s -X POST --data '{
"method":"admin.getConfig",
"params":{},
"id":1
}' http://localhost:9630/v1/admin)
}' http://localhost:9630/ext/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/v1/admin
}' http://localhost:9630/ext/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/v1/health
http://localhost:9630/ext/health
```
## Health Check Types
@@ -20,7 +20,7 @@ http://localhost:9630/v1/health
Get the overall health status of the node:
```bash
curl http://localhost:9630/v1/health
curl http://localhost:9630/ext/health
```
**Response:**
@@ -61,7 +61,7 @@ curl http://localhost:9630/v1/health
Check if the node is ready to serve requests:
```bash
curl http://localhost:9630/v1/health/readiness
curl http://localhost:9630/ext/health/readiness
```
Returns:
@@ -73,7 +73,7 @@ Returns:
Check if the node is alive and running:
```bash
curl http://localhost:9630/v1/health/liveness
curl http://localhost:9630/ext/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/v1/health
}' -H 'content-type:application/json;' http://localhost:9630/ext/health
```
**Response:**
@@ -175,7 +175,7 @@ Configure per-chain health parameters:
Export health metrics in Prometheus format:
```bash
curl http://localhost:9630/v1/metrics | grep health
curl http://localhost:9630/ext/metrics | grep health
```
Metrics:
@@ -208,13 +208,13 @@ spec:
image: luxfi/node:latest
livenessProbe:
httpGet:
path: /v1/health/liveness
path: /ext/health/liveness
port: 9630
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /v1/health/readiness
path: /ext/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/v1/health | jq -r '.healthy')
HEALTH=$(curl -s http://localhost:9630/ext/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/v1/health)
RESPONSE=$(curl -s http://localhost:9630/ext/health)
# Parse each chain
for CHAIN in P X C Q; do
@@ -271,7 +271,7 @@ done
```
backend lux_nodes
option httpchk GET /v1/health
option httpchk GET /ext/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/v1/health;
proxy_pass http://lux_nodes/ext/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/v1/info
http://localhost:9630/ext/info
```
## Format
@@ -23,7 +23,7 @@ curl -X POST --data '{
"method": "info.<method>",
"params": {...},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
```
## Methods
@@ -41,7 +41,7 @@ curl -X POST --data '{
"method": "info.getNodeVersion",
"params": {},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
}' -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/info
}' -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/info
}' -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/info
}' -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/info
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
```
**Example Response:**
@@ -207,7 +207,7 @@ curl -X POST --data '{
"alias": "P"
},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
```
**Example Response:**
@@ -239,7 +239,7 @@ curl -X POST --data '{
"chain": "P"
},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
}' -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/info
}' -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/info
}' -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/info
}' -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/info
}' -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/info
}' -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/info | jq -r '.result.version')
}' -H 'content-type:application/json;' $NODE_URL/ext/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/v1/info | jq -r '.result.nodeID')
}' -H 'content-type:application/json;' $NODE_URL/ext/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/v1/info | jq -r '.result.numPeers')
}' -H 'content-type:application/json;' $NODE_URL/ext/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/v1/info | jq -r '.result.isBootstrapped')
}" -H 'content-type:application/json;' $NODE_URL/ext/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/v1/info 2>/dev/null | jq -r '.result.rewardingStakePercentage' 2>/dev/null)
}' -H 'content-type:application/json;' $NODE_URL/ext/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/v1/info | jq -r '.result.peers[]')
}' -H 'content-type:application/json;' $NODE_URL/ext/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/v1/info | jq -r '.result.isBootstrapped')
}" -H 'content-type:application/json;' $NODE_URL/ext/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/v1/info | jq -r '.result.numPeers')
}' -H 'content-type:application/json;' $NODE_URL/ext/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/v1/info | jq -r '.result.isBootstrapped')
}" -H 'content-type:application/json;' $NODE_URL/ext/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/v1/bc/P
http://localhost:9630/ext/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/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
```
**Example Response:**
@@ -114,7 +114,7 @@ curl -X POST --data '{
"limit": 100
},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
```
---
@@ -134,7 +134,7 @@ curl -X POST --data '{
"method": "platform.getCurrentValidators",
"params": {},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
```
---
@@ -212,7 +212,7 @@ curl -X POST --data '{
"height": 365000
},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
```
---
@@ -233,7 +233,7 @@ curl -X POST --data '{
"height": 365000
},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
```
---
@@ -255,7 +255,7 @@ curl -X POST --data '{
"addresses": ["P-lux1q8tgunsf7sxpl2qp9fz0xjs36kstm7vdjvpzcc"]
},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
```
---
@@ -274,7 +274,7 @@ curl -X POST --data '{
"method": "platform.getMinStake",
"params": {},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
```
---
@@ -327,7 +327,7 @@ curl -X POST --data '{
"txID": "2nmH8LithVbdjaXsxVQCQfXtzN9hBbmebrsaEYnLM9T32Uy3Y5"
},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
```
---
@@ -345,7 +345,7 @@ curl -X POST --data '{
"method": "platform.getTimestamp",
"params": {},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
```
---
@@ -363,7 +363,7 @@ curl -X POST --data '{
"method": "platform.getBlockchains",
"params": {},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
```
**Example Response:**
@@ -409,7 +409,7 @@ curl -X POST --data '{
"blockID": "vXSY7FK7NR65Y8BrJDKQH4a6vBdJuqAMvVzWj3Zxcap5J4ZE3"
},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
```
---
@@ -431,7 +431,7 @@ curl -X POST --data '{
"txID": "2nmH8LithVbdjaXsxVQCQfXtzN9hBbmebrsaEYnLM9T32Uy3Y5"
},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
```
---
@@ -452,7 +452,7 @@ curl -X POST --data '{
"txID": "2nmH8LithVbdjaXsxVQCQfXtzN9hBbmebrsaEYnLM9T32Uy3Y5"
},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
```
---
@@ -507,7 +507,7 @@ curl -X POST --data '{
"method": "platform.getChains",
"params": {},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
```
---
@@ -544,7 +544,7 @@ curl -X POST --data '{
"password": "mypassword"
},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
```
---
@@ -694,7 +694,7 @@ curl -s -X POST --data "{
\"nodeIDs\": [\"$NODE_ID\"]
},
\"id\": 1
}" -H 'content-type:application/json;' http://localhost:9630/v1/bc/P | jq '.result.validators[0]'
}" -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/bc/P)
}" -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/health
}' -H 'content-type:application/json;' http://localhost:9630/ext/health
# Get node info
curl -X POST --data '{
"jsonrpc":"2.0",
"id": 1,
"method": "info.getNodeVersion"
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
}' -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/info
}' -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/health
}' -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/info
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
```
### Metrics
@@ -328,7 +328,7 @@ curl -X POST --data '{
Access Prometheus metrics:
```bash
curl http://localhost:9630/v1/metrics
curl http://localhost:9630/ext/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/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/info
}' -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/info
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
```
Response:
@@ -155,7 +155,7 @@ curl -X POST --data '{
"password": "mypassword"
},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/v1/keystore
}' -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/bc/X
}' -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
```
### Monitor Performance Metrics
@@ -277,10 +277,10 @@ Key metrics to track:
```bash
# Node health
curl http://localhost:9630/v1/health
curl http://localhost:9630/ext/health
# Prometheus metrics
curl http://localhost:9630/v1/metrics | grep -E "uptime|stake|validator"
curl http://localhost:9630/ext/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/v1/health" > /dev/null; then
if ! curl -s "$NODE_URL/ext/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/v1/bc/P" | jq -r '.result.uptime')
}' "$NODE_URL/ext/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/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
```
## Chain Validation
@@ -373,7 +373,7 @@ curl -X POST --data '{
"weight": 1000
},
"id": 1
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/info
}' -H 'content-type:application/json;' http://localhost:9650/ext/info
# Get node ID
curl -X POST --data '{
"jsonrpc":"2.0",
"id": 1,
"method": "info.getNodeID"
}' -H 'content-type:application/json;' http://localhost:9650/v1/info
}' -H 'content-type:application/json;' http://localhost:9650/ext/info
```
## Chain Management
@@ -130,7 +130,7 @@ curl -X POST --data '{
"genesisData": "...",
"netID": "network-id"
}
}' -H 'content-type:application/json;' http://localhost:9650/v1/P
}' -H 'content-type:application/json;' http://localhost:9650/ext/P
```
### Validator Management
@@ -147,7 +147,7 @@ curl -X POST --data '{
"endTime": ...,
"stakeAmount": ...
}
}' -H 'content-type:application/json;' http://localhost:9650/v1/P
}' -H 'content-type:application/json;' http://localhost:9650/ext/P
```
## Configuration Reference
@@ -175,10 +175,10 @@ curl -X POST --data '{
```bash
# Prometheus metrics endpoint
curl http://localhost:9650/v1/metrics
curl http://localhost:9650/ext/metrics
# Health check
curl http://localhost:9650/v1/health
curl http://localhost:9650/ext/health
```
### Logs
@@ -193,7 +193,7 @@ curl -X POST --data '{
"id": 1,
"method": "admin.setLogLevel",
"params": {"logLevel": "debug"}
}' http://localhost:9650/v1/admin
}' http://localhost:9650/ext/admin
```
## Testing
@@ -595,10 +595,10 @@ curl -X POST --data '{
"jsonrpc":"2.0",
"method":"info.peers",
"id":1
}' http://localhost:9630/v1/info
}' http://localhost:9630/ext/info
# Check network metrics
curl http://localhost:9630/v1/metrics | grep network
curl http://localhost:9630/ext/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: '/v1/metrics'
metrics_path: '/ext/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/v1/metrics`.
The Lux node exposes 400+ Prometheus metrics at `http://localhost:9630/ext/metrics`.
#### Key Metric Categories
@@ -419,11 +419,11 @@ groups:
```bash
# Basic health check
curl http://localhost:9630/v1/health
curl http://localhost:9630/ext/health
# Detailed health with readiness/liveness
curl http://localhost:9630/v1/health/readiness
curl http://localhost:9630/v1/health/liveness
curl http://localhost:9630/ext/health/readiness
curl http://localhost:9630/ext/health/liveness
```
### Custom Health Script
@@ -443,7 +443,7 @@ send_alert() {
}
# Check if node is responsive
if ! curl -s "$NODE_URL/v1/health" > /dev/null; then
if ! curl -s "$NODE_URL/ext/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/v1/info | jq -r '.result.isBootstrapped')
}" -H 'content-type:application/json;' $NODE_URL/ext/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/v1/info | jq -r '.result.numPeers')
}' -H 'content-type:application/json;' $NODE_URL/ext/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/v1/metrics | grep -E "chain_height|network_peers"'
watch -n 1 'curl -s http://localhost:9630/ext/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/v1/health > /dev/null 2>&1; then
if curl -s http://localhost:9630/ext/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/v1/info 2>/dev/null | jq -r '.result.numPeers')
}' -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/info
curl -I http://localhost:9630/ext/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/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/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/v1/bc/P
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
```
## Log Analysis
@@ -518,7 +518,7 @@ curl -X POST --data '{
"jsonrpc":"2.0",
"method":"info.getNodeID",
"id":1
}' http://localhost:9630/v1/info
}' http://localhost:9630/ext/info
```
### Support Channels
@@ -1,163 +0,0 @@
# 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("/v1/crossnet/status", n.handleCrossNetStatus)
http.HandleFunc("/v1/crossnet/validators", n.handleCrossNetValidators)
http.HandleFunc("/v1/network/", n.handleNetworkSpecific)
http.HandleFunc("/ext/crossnet/status", n.handleCrossNetStatus)
http.HandleFunc("/ext/crossnet/validators", n.handleCrossNetValidators)
http.HandleFunc("/ext/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: /v1/network/{networkID}/...
// Parse network ID from path: /ext/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/v1/crossnet/status")
fmt.Println(" • Cross-network validators: http://localhost:9650/v1/crossnet/validators")
fmt.Println(" • Network-specific: http://localhost:9650/v1/network/{networkID}/...")
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}/...")
// This would be replaced with actual RPC server
node.StartRPCServer(9650)
Executable
BIN
View File
Binary file not shown.
+13 -23
View File
@@ -54,8 +54,7 @@ var (
QChainAliases = AliasesFor("Q")
AChainAliases = AliasesFor("A")
BChainAliases = AliasesFor("B")
MChainAliases = AliasesFor("M")
FChainAliases = AliasesFor("F")
TChainAliases = AliasesFor("T")
ZChainAliases = AliasesFor("Z")
GChainAliases = AliasesFor("G")
KChainAliases = AliasesFor("K")
@@ -605,8 +604,7 @@ 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.MChainGenesis), constants.MPCVMID, "M-Chain", nil},
{[]byte(config.FChainGenesis), constants.FHEVMID, "F-Chain", nil},
{[]byte(config.TChainGenesis), constants.ThresholdVMID, "T-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
@@ -728,10 +726,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)
}
@@ -749,7 +747,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
}
}
@@ -778,7 +776,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",
@@ -834,24 +832,16 @@ func Aliases(genesisBytes []byte) (map[string][]string, map[ids.ID][]string, err
path.Join(constants.ChainAliasPrefix, "bridge"),
}
chainAliases[chainID] = BChainAliases
case constants.MPCVMID:
case constants.ThresholdVMID:
apiAliases[endpoint] = []string{
"M",
"T",
"threshold",
"thresholdvm",
"mpc",
"mpcvm",
path.Join(constants.ChainAliasPrefix, "M"),
path.Join(constants.ChainAliasPrefix, "mpc"),
path.Join(constants.ChainAliasPrefix, "T"),
path.Join(constants.ChainAliasPrefix, "threshold"),
}
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
chainAliases[chainID] = TChainAliases
case constants.ZKVMID:
apiAliases[endpoint] = []string{
"Z",
+3 -2
View File
@@ -11,6 +11,7 @@ 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
@@ -62,7 +63,7 @@ func TestUTXOAssetIDFromGenesisBytes_POnly(t *testing.T) {
require := require.New(t)
pOnly := &genesis.Genesis{Chains: nil}
pOnlyBytes, err := pOnly.Bytes()
pOnlyBytes, err := genesis.Codec.Marshal(pchaintxs.CodecVersion, pOnly)
require.NoError(err)
id, ok, err := UTXOAssetIDFromGenesisBytes(pOnlyBytes)
@@ -92,7 +93,7 @@ func TestVMGenesisOptInChains(t *testing.T) {
pOnly := &genesis.Genesis{
Chains: nil,
}
pOnlyBytes, err := pOnly.Bytes()
pOnlyBytes, err := genesis.Codec.Marshal(pchaintxs.CodecVersion, pOnly)
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.Weight(),
require.Equal(stakers[i].Weight, ut.Wght,
"validator %d weight mismatch", i)
t.Logf("Validator %d: NodeID=%s Weight=%d StakeOuts=%d",
i, ut.Validator().NodeID, ut.Weight(), len(ut.StakeOuts()))
i, ut.Validator.NodeID, ut.Wght, len(ut.StakeOuts))
case *txs.AddPermissionlessValidatorTx:
require.Equal(stakers[i].Weight, ut.Weight(),
require.Equal(stakers[i].Weight, ut.Wght,
"validator %d weight mismatch", i)
t.Logf("Validator %d: NodeID=%s Weight=%d StakeOuts=%d",
i, ut.Validator().NodeID, ut.Weight(), len(ut.StakeOuts()))
i, ut.Validator.NodeID, ut.Wght, 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.Weight(), 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.Wght, 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.Weight(), len(ut.StakeOuts()))
i, ut.Validator.NodeID, ut.Wght, len(ut.StakeOuts))
case *txs.AddPermissionlessValidatorTx:
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)
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)
t.Logf("Validator %d: NodeID=%s Weight=%d StakeOuts=%d",
i, ut.Validator().NodeID, ut.Weight(), len(ut.StakeOuts()))
i, ut.Validator.NodeID, ut.Wght, len(ut.StakeOuts))
default:
t.Fatalf("unexpected validator tx type: %T", ut)
}
-173
View File
@@ -1,173 +0,0 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package builder
import (
"encoding/json"
"fmt"
"testing"
"github.com/stretchr/testify/require"
"github.com/luxfi/constants"
"github.com/luxfi/node/vms/platformvm/genesis"
pchaintxs "github.com/luxfi/node/vms/platformvm/txs"
)
// parsePolicy reads the operator form "3-of-5" into (K, N).
//
// The canonical parser is luxfi/threshold/pkg/quorum, but the node's genesis
// builder is a boot-critical path and does not take a dependency on a
// threshold-crypto library to read three integers. What this test pins is the
// FORM — that the genesis blob states a quorum in a spelling that cannot be
// confused with a polynomial degree — and the form is checkable here.
func parsePolicy(t *testing.T, s string) (k, n int) {
t.Helper()
_, err := fmt.Sscanf(s, "%d-of-%d", &k, &n)
require.NoErrorf(t, err, "policy %q is not in the operator form \"3-of-5\"", s)
require.Greaterf(t, k, 1, "policy %q: k=1 is not a threshold policy", s)
require.LessOrEqualf(t, k, n, "policy %q can never be satisfied", s)
return k, n
}
// canonicalMPCVMID is M-Chain's vmID in the encoding the node's plugin registry
// resolves against. Pinned literally: the plugin binary's FILENAME must equal
// it, and a change that leaves the two out of step produces a chain that is
// declared in genesis but that no node can start.
//
// The predecessor value, tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t, is
// the retired `thresholdvm` identifier. It was what the Dockerfile installed
// the plugin under while genesis declared MPCVMID, so the two never met.
const canonicalMPCVMID = "qCURact1n41FcoNBch8iMVBwc9AWie48D118ZNJ5tBdWrvryS"
// M-Chain must be a GENESIS chain — in the P-Chain's chain set at height 0,
// tracked by every validator from boot — not a chain created later by a
// CreateChainTx someone has to remember to submit.
//
// The difference matters for custody specifically. A post-genesis chain exists
// only from the height its tx landed, so a node syncing from genesis has a
// window with no custody state, and the chain's very existence depends on an
// operator action that can be forgotten or fumbled. Bridged funds should not
// depend on that.
func TestMChainIsAGenesisChain(t *testing.T) {
for _, networkID := range []uint32{
constants.MainnetID,
constants.TestnetID,
constants.LocalID,
} {
cfg := GetConfig(networkID)
require.NotNilf(t, cfg, "network %d has no genesis config", networkID)
require.NotEmptyf(t, cfg.MChainGenesis,
"network %d carries no mchain.json, so the builder skips M-Chain entirely", networkID)
raw, _, err := FromConfig(cfg)
require.NoErrorf(t, err, "network %d genesis build", networkID)
parsed, err := genesis.Parse(raw)
require.NoErrorf(t, err, "network %d genesis parse", networkID)
// Genesis chains are CreateChainTx entries executed at height 0 — the
// chain exists from the first block, without anyone submitting a tx.
var found *pchaintxs.CreateChainTx
for _, c := range parsed.Chains {
u, ok := c.Unsigned.(*pchaintxs.CreateChainTx)
require.True(t, ok, "a genesis chain entry must be a CreateChainTx")
if u.VMID() == constants.MPCVMID {
found = u
break
}
}
require.NotNilf(t, found,
"network %d: no genesis chain with vmID %s (M-Chain)", networkID, constants.MPCVMID)
require.Equal(t, "M-Chain", found.BlockchainName())
require.Equalf(t, []byte(cfg.MChainGenesis), found.GenesisData(),
"network %d: the VM must receive exactly the mchain.json bytes", networkID)
require.Equalf(t, canonicalMPCVMID, found.VMID().String(),
"network %d: genesis declares a vmID the plugin registry will look up by filename", networkID)
}
}
// The genesis blob decides how many custodians must cooperate to move bridged
// funds, so it must state that in a form nothing can misread as a polynomial
// degree. A bare `"threshold": 3` reads as the signer count to an operator and
// as the degree to every threshold library, and those differ by one.
func TestMChainGenesisPolicyIsUnambiguousAndDeployable(t *testing.T) {
for _, tc := range []struct {
networkID uint32
want string
}{
{constants.MainnetID, "3-of-5"},
{constants.TestnetID, "3-of-5"},
{constants.LocalID, "2-of-3"},
} {
cfg := GetConfig(tc.networkID)
require.NotNil(t, cfg)
var blob struct {
Policy string `json:"policy"`
VM string `json:"vm"`
}
require.NoErrorf(t, json.Unmarshal([]byte(cfg.MChainGenesis), &blob),
"network %d mchain.json must decode, policy included", tc.networkID)
require.Equalf(t, tc.want, blob.Policy, "network %d policy", tc.networkID)
k, n := parsePolicy(t, blob.Policy)
require.Greaterf(t, 2*k, n,
"network %d: %s admits two disjoint quorums, so two halves of the committee could authorise contradictory releases",
tc.networkID, blob.Policy)
require.Equalf(t, "mpcvm", blob.VM,
"network %d must name the current VM, not the retired ThresholdVM", tc.networkID)
}
}
// The ambiguous fields must be gone, not merely ignored. Leaving them in place
// means the next reader has two numbers to choose between, and the whole point
// of the policy field is that there is exactly one.
func TestMChainGenesisHasNoAmbiguousThresholdFields(t *testing.T) {
for _, networkID := range []uint32{constants.MainnetID, constants.TestnetID, constants.LocalID} {
cfg := GetConfig(networkID)
require.NotNil(t, cfg)
var blob map[string]any
require.NoError(t, json.Unmarshal([]byte(cfg.MChainGenesis), &blob))
for _, dead := range []string{"mpcThreshold", "mpcParties", "threshold", "totalParties"} {
require.NotContainsf(t, blob, dead,
"network %d mchain.json still carries %q; the policy field is the only quorum statement",
networkID, dead)
}
}
}
// A policy is only real if the network has enough validators to hold its
// shares. M-Chain draws its committee from the validator set, and RunKeygen
// refuses a policy needing more parties than the committee has — so a genesis
// declaring 7-of-10 on a five-validator network fails closed forever: no
// custody key can ever be generated, and the failure only shows up the first
// time someone tries to bridge.
//
// mainnet's mchain.json shipped exactly that mismatch for as long as the field
// existed, harmlessly, because nothing read it. This test is what keeps it
// harmless now that the policy is authoritative.
func TestMChainPolicyIsSatisfiableByTheGenesisValidatorSet(t *testing.T) {
for _, networkID := range []uint32{constants.MainnetID, constants.TestnetID, constants.LocalID} {
cfg := GetConfig(networkID)
require.NotNil(t, cfg)
raw, _, err := FromConfig(cfg)
require.NoError(t, err)
parsed, err := genesis.Parse(raw)
require.NoError(t, err)
var blob struct {
Policy string `json:"policy"`
}
require.NoError(t, json.Unmarshal([]byte(cfg.MChainGenesis), &blob))
_, n := parsePolicy(t, blob.Policy)
require.LessOrEqualf(t, n, len(parsed.Validators),
"network %d: M-Chain policy %s needs %d parties but genesis declares %d validators; "+
"keygen would fail closed and no custody key could ever exist",
networkID, blob.Policy, n, len(parsed.Validators))
}
}
+1 -2
View File
@@ -62,8 +62,7 @@ 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: "M", VMID: constants.MPCVMID, Aliases: []string{"mpc", "mpcvm"}, Name: "M-Chain"},
{Letter: "F", VMID: constants.FHEVMID, Aliases: []string{"fhe", "fhevm"}, Name: "F-Chain"},
{Letter: "T", VMID: constants.ThresholdVMID, Aliases: []string{"threshold", "thresholdvm", "mpc"}, Name: "T-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"},
+2 -4
View File
@@ -41,8 +41,7 @@ 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"}},
{"M", MChainAliases, []string{"M", "mpc", "mpcvm"}},
{"F", FChainAliases, []string{"F", "fhe", "fhevm"}},
{"T", TChainAliases, []string{"T", "threshold", "thresholdvm", "mpc"}},
{"Z", ZChainAliases, []string{"Z", "zk", "zkvm"}},
{"G", GChainAliases, []string{"G", "graph", "graphvm", "dgraph"}},
{"K", KChainAliases, []string{"K", "key", "keyvm"}},
@@ -73,8 +72,7 @@ func TestVMAliasesRegistryParity(t *testing.T) {
constants.QuantumVMID: {"quantumvm", "quantum", "pq"},
constants.AIVMID: {"aivm", "attest", "ai"},
constants.BridgeVMID: {"bridgevm", "bridge"},
constants.MPCVMID: {"mpc", "mpcvm"},
constants.FHEVMID: {"fhe", "fhevm"},
constants.ThresholdVMID: {"thresholdvm", "threshold", "mpc"},
constants.ZKVMID: {"zkvm", "zk"},
constants.GraphVMID: {"graphvm", "graph", "dgraph"},
constants.KeyVMID: {"keyvm", "key"},
-73
View File
@@ -1,73 +0,0 @@
// 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)
}
}
}
+80 -76
View File
@@ -26,21 +26,21 @@ require (
github.com/huin/goupnp v1.3.0
github.com/jackpal/gateway v1.1.1
github.com/jackpal/go-nat-pmp v1.0.2
github.com/luxfi/consensus v1.36.12
github.com/luxfi/crypto v1.20.2
github.com/luxfi/database v1.21.1
github.com/luxfi/ids v1.3.2
github.com/luxfi/keychain v1.1.1
github.com/luxfi/consensus v1.35.2
github.com/luxfi/crypto v1.19.26
github.com/luxfi/database v1.20.4
github.com/luxfi/ids v1.3.0
github.com/luxfi/keychain v1.0.2
github.com/luxfi/log v1.4.3
github.com/luxfi/math v1.5.1
github.com/luxfi/metric v1.8.1
github.com/luxfi/math v1.4.1
github.com/luxfi/metric v1.5.9
github.com/luxfi/mock v0.1.1
github.com/mr-tron/base58 v1.3.0
github.com/onsi/ginkgo/v2 v2.28.1
github.com/pires/go-proxyproto v0.11.0
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.68.0 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/rs/cors v1.11.1
github.com/shirou/gopsutil v3.21.11+incompatible
github.com/spf13/cast v1.10.0 // indirect
@@ -56,13 +56,13 @@ require (
go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.uber.org/goleak v1.3.0
go.uber.org/mock v0.6.0
golang.org/x/crypto v0.54.0
golang.org/x/crypto v0.52.0
golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9 // indirect
golang.org/x/mod v0.37.0
golang.org/x/net v0.57.0
golang.org/x/sync v0.22.0
golang.org/x/mod v0.36.0
golang.org/x/net v0.55.0
golang.org/x/sync v0.20.0
golang.org/x/time v0.15.0
golang.org/x/tools v0.47.0
golang.org/x/tools v0.45.0
gonum.org/v1/gonum v0.17.0
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
@@ -73,7 +73,7 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/cespare/xxhash/v2 v2.3.0
github.com/cockroachdb/errors v1.13.0 // indirect
github.com/cockroachdb/errors v1.12.0 // indirect
github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 // indirect
github.com/cockroachdb/redact v1.1.8 // indirect
github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb // indirect
@@ -81,7 +81,7 @@ require (
github.com/deckarep/golang-set/v2 v2.9.0 // indirect
github.com/fatih/structtag v1.2.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/getsentry/sentry-go v0.46.2 // indirect
github.com/getsentry/sentry-go v0.44.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
@@ -90,7 +90,7 @@ require (
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.19.1
github.com/klauspost/compress v1.18.6
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
@@ -98,7 +98,7 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/rogpeppe/go-internal v1.15.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/sanity-io/litter v1.5.5 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
@@ -107,8 +107,8 @@ require (
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.40.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
)
require (
@@ -118,69 +118,68 @@ 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.1.1
github.com/luxfi/api v1.0.15
github.com/luxfi/atomic v1.0.0
github.com/luxfi/chains v1.7.9
github.com/luxfi/codec v1.2.1
github.com/luxfi/compress v0.1.1
github.com/luxfi/constants v1.6.2
github.com/luxfi/container v0.2.1
github.com/luxfi/chains v1.4.8
github.com/luxfi/codec v1.1.5
github.com/luxfi/compress v0.0.5
github.com/luxfi/constants v1.5.8
github.com/luxfi/container v0.0.4
github.com/luxfi/filesystem v0.0.1
github.com/luxfi/genesis v1.16.4
github.com/luxfi/genesis v1.13.16
github.com/luxfi/genesis/pkg/genesis/security v1.13.8
github.com/luxfi/geth v1.20.1
github.com/luxfi/go-bip39 v1.2.0
github.com/luxfi/kms v1.12.10
github.com/luxfi/geth v1.17.12
github.com/luxfi/go-bip39 v1.1.2
github.com/luxfi/keys v1.2.0
github.com/luxfi/math/safe v0.0.1
github.com/luxfi/net v0.1.1
github.com/luxfi/p2p v1.22.1
github.com/luxfi/resource v0.1.1
github.com/luxfi/net v0.0.5
github.com/luxfi/p2p v1.21.1
github.com/luxfi/resource v0.0.1
github.com/luxfi/rpc v1.1.0
github.com/luxfi/runtime v1.3.1
github.com/luxfi/sdk v1.18.1
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.3
github.com/luxfi/timer v1.1.1
github.com/luxfi/timer v1.0.2
github.com/luxfi/units v1.0.0
github.com/luxfi/utils v1.3.1
github.com/luxfi/utxo v0.5.8
github.com/luxfi/validators v1.3.1
github.com/luxfi/vm v1.3.3
github.com/luxfi/warp v1.24.1
github.com/luxfi/zap v1.2.6
github.com/luxfi/zwing v0.6.1
github.com/luxfi/utils v1.2.0
github.com/luxfi/utxo v0.3.7
github.com/luxfi/validators v1.2.0
github.com/luxfi/vm v1.2.5
github.com/luxfi/warp v1.24.0
github.com/luxfi/zap v0.8.11
github.com/luxfi/zwing v0.5.2
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354
github.com/valyala/fasthttp v1.73.0
github.com/zap-proto/http v0.3.0
github.com/zap-proto/http v0.0.0-20260506200741-fd6047874433
go.uber.org/zap v1.27.1
)
require (
capnproto.org/go/capnp/v3 v3.0.1-alpha.2 // indirect
filippo.io/edwards25519 v1.2.0 // indirect
filippo.io/hpke v0.4.0 // indirect
github.com/andybalholm/brotli v1.2.2 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.13 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.13 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.14 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.18 // indirect
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.5.0 // indirect
github.com/btcsuite/btcd/chainhash/v2 v2.0.0 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.6 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/colega/zeropool v0.0.0-20230505084239-6fb4a4f75381 // indirect
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
@@ -190,24 +189,24 @@ 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.6.0 // indirect
github.com/luxfi/age v1.5.0 // indirect
github.com/luxfi/bft v0.1.5 // indirect
github.com/luxfi/corona v0.10.4 // 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
github.com/luxfi/keys v1.4.1 // indirect
github.com/luxfi/kms v1.11.7 // indirect
github.com/luxfi/lattice/v7 v7.1.4 // indirect
github.com/luxfi/lens v0.2.1 // indirect
github.com/luxfi/light v1.0.0 // indirect
github.com/luxfi/lens v0.1.4 // indirect
github.com/luxfi/magnetar v1.2.3 // indirect
github.com/luxfi/mdns v0.1.1 // indirect
github.com/luxfi/mlwe v0.3.0 // indirect
github.com/luxfi/pq v1.1.0 // indirect
github.com/luxfi/precompile v0.19.3 // indirect
github.com/luxfi/protocol v0.0.2 // indirect
github.com/luxfi/pulsar v1.9.2 // indirect
github.com/luxfi/staking v1.6.1 // indirect
github.com/luxfi/trace v1.2.1 // indirect
github.com/luxfi/mlwe v0.2.1 // indirect
github.com/luxfi/pq v1.0.3 // indirect
github.com/luxfi/precompile v0.16.0 // 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,16 +215,13 @@ require (
github.com/philhofer/fwd v1.2.0 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/tinylib/msgp v1.6.4 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/zap-proto/go v1.1.0 // indirect
go.mongodb.org/mongo-driver v1.17.9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
)
require (
github.com/luxfi/concurrent v0.1.1
github.com/luxfi/proto v1.4.2
github.com/luxfi/upgrade v1.0.3 // indirect
github.com/luxfi/concurrent v0.0.3
github.com/luxfi/proto v1.3.5
github.com/luxfi/upgrade v1.0.1 // indirect
github.com/luxfi/version v1.0.1
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
)
@@ -246,13 +242,13 @@ require (
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/luxfi/address v1.1.1
github.com/luxfi/cache v1.3.1 // indirect
github.com/luxfi/formatting v1.1.1
github.com/luxfi/go-bip32 v1.1.0
github.com/luxfi/address v1.0.1
github.com/luxfi/cache v1.2.1 // indirect
github.com/luxfi/formatting v1.0.1
github.com/luxfi/go-bip32 v1.0.2
github.com/luxfi/math/big v0.1.0 // indirect
github.com/luxfi/sampler v1.1.0 // indirect
github.com/luxfi/tls v1.1.1 // indirect
github.com/luxfi/tls v1.0.3 // indirect
github.com/mattn/go-colorable v0.1.15 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/montanaflynn/stats v0.9.0 // indirect
@@ -261,7 +257,15 @@ require (
github.com/x448/float16 v0.8.4 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
)
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.
+137 -162
View File
@@ -1,5 +1,7 @@
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd h1:ZLsPO6WdZ5zatV4UfVpr7oAwLGRZ+sebTUruuM4Ra3M=
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo=
capnproto.org/go/capnp/v3 v3.0.1-alpha.2 h1:W/cf+XEArUSwcBBE/9wS2NpWDkM5NLQOjmzEiHZpYi0=
capnproto.org/go/capnp/v3 v3.0.1-alpha.2/go.mod h1:2vT5D2dtG8sJGEoEKU17e+j7shdaYp1Myl8X03B3hmc=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A=
@@ -17,14 +19,10 @@ github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA
github.com/StephenButtolph/canoto v0.17.3 h1:lvsnYD4b96vD1knnmp1xCmZqfYpY/jSeRozGdOfdvGI=
github.com/StephenButtolph/canoto v0.17.3/go.mod h1:IcnAHC6nJUfQFVR9y60ko2ecUqqHHSB6UwI9NnBFZnE=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM=
github.com/andybalholm/brotli v1.2.2/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
github.com/aws/aws-sdk-go-v2/config v1.32.13 h1:5KgbxMaS2coSWRrx9TX/QtWbqzgQkOdEa3sZPhBhCSg=
github.com/aws/aws-sdk-go-v2/config v1.32.13/go.mod h1:8zz7wedqtCbw5e9Mi2doEwDyEgHcEE9YOJp6a8jdSMY=
github.com/aws/aws-sdk-go-v2/credentials v1.19.13 h1:mA59E3fokBvyEGHKFdnpNNrvaR351cqiHgRg+JzOSRI=
@@ -39,24 +37,16 @@ github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 h1:eZioDaZGJ0tMM4gzmkNIO2aAoQd+je7Ug7TkvAzlmkU=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18/go.mod h1:CCXwUKAJdoWr6/NcxZ+zsiPr6oH/Q5aTooRGYieAyj4=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 h1:fJvQ5mIBVfKtiyx0AHY6HeWcRX5LGANLpq8SVR+Uazs=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10/go.mod h1:Kzm5e6OmNH8VMkgK9t+ry5jEih4Y8whqs+1hrkxim1I=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 h1:/A/xDuZAVD2BpsS2fftFRo/NoEKQJ8YTnJDEHBy2Gtg=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18/go.mod h1:hWe9b4f+djUQGmyiGEeOnZv69dtMSgpDRIvNMvuvzvY=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 h1:M1A9AjcFwlxTLuf0Faj88L8Iqw0n/AJHjpZTQzMMsSc=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2/go.mod h1:KsdTV6Q9WKUZm2mNJnUFmIoXfZux91M3sr/a4REX8e0=
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 h1:HwxWTbTrIHm5qY+CAEur0s/figc3qwvLWsNkF4RPToo=
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 h1:QKZH0S178gCmFEgst8hN0mCX1KxLgHBKKY/CLqwP8lg=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9/go.mod h1:7yuQJoT+OoH8aqIxw9vwF+8KpvLZ8AWmvmUWHsGQZvI=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.14 h1:GcLE9ba5ehAQma6wlopUesYg/hbcOhFNWTjELkiWkh4=
@@ -77,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.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/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/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=
@@ -86,9 +76,8 @@ 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=
@@ -109,8 +98,8 @@ github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4=
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU=
github.com/cockroachdb/errors v1.13.0 h1:BoCcJeiP9hpBJDETkX19qi8Tb8So37srSsp3stTaDMQ=
github.com/cockroachdb/errors v1.13.0/go.mod h1:bjxt/4E5+OyuAnacpTIU9rn2mzPu1VlthvHP+xpROq0=
github.com/cockroachdb/errors v1.12.0 h1:d7oCs6vuIMUQRVbi6jWWWEJZahLCfJpnJSVobd1/sUo=
github.com/cockroachdb/errors v1.12.0/go.mod h1:SvzfYNNBshAVbZ8wzNc/UPK3w1vf0dKDUP41ucAIf7g=
github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0 h1:pU88SPhIFid6/k0egdR5V6eALQYq2qbSmukrkgIh/0A=
github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M=
github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 h1:ASDL+UJcILMqgNeV5jiqR4j+sTuvQNHdf2chuKj1M5k=
@@ -121,6 +110,8 @@ github.com/cockroachdb/redact v1.1.8 h1:8eVLLj6juKxiKrAEw2b8cJvNqWq++U8WOfQFuL7K
github.com/cockroachdb/redact v1.1.8/go.mod h1:GceHHpJ0rMDpYARL5In88Alq/xMBUtVlz7Qxix6ZVkw=
github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb h1:3bCgBvB8PbJVMX1ouCcSIxvsqKPYM7gs72o0zC76n9g=
github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
github.com/colega/zeropool v0.0.0-20230505084239-6fb4a4f75381 h1:d5EKgQfRQvO97jnISfR89AiCCCJMwMFoSxUiU0OGCRU=
github.com/colega/zeropool v0.0.0-20230505084239-6fb4a4f75381/go.mod h1:OU76gHeRo8xrzGJU3F3I1CqX1ekM8dfJw0+wPeMwnp0=
github.com/consensys/gnark-crypto v0.20.1 h1:PXDUBvk8AzhvWowHLWBEAfUQcV1/aZgWIqD6eMpXmDg=
github.com/consensys/gnark-crypto v0.20.1/go.mod h1:RBWrSgy+IDbGR69RRV313th3M/aZU1ubk2om+qHuTSc=
github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY=
@@ -175,8 +166,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=
github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/getsentry/sentry-go v0.46.2 h1:1jhYwrKGa3sIpo/y5iDNXS5wDoT7I1KNzMHrnK6ojns=
github.com/getsentry/sentry-go v0.46.2/go.mod h1:evVbw2qotNUdYG8KxXbAdjOQWWvWIwKxpjdZZIvcIPw=
github.com/getsentry/sentry-go v0.44.1 h1:/cPtrA5qB7uMRrhgSn9TYtcEF36auGP3Y6+ThvD/yaI=
github.com/getsentry/sentry-go v0.44.1/go.mod h1:XDotiNZbgf5U8bPDUAfvcFmOnMQQceESxyKaObSssW0=
github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs=
github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M=
@@ -288,8 +279,8 @@ github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlT
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
@@ -305,152 +296,146 @@ github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzW
github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
github.com/luxfi/accel v1.2.4 h1:5VbIHyEvvfobn2zBiTFODxDw1CeqxCepZOLlvkuf9yQ=
github.com/luxfi/accel v1.2.4/go.mod h1:ISIwAX+ZfsL/S5nsP2JvfldXN6Nc+QzoWf6Jtaq+xsQ=
github.com/luxfi/address v1.1.1 h1:4afWzyBWzTiZN7RenBtdMC9LIvP9L4CSBzSquwKEAgI=
github.com/luxfi/address v1.1.1/go.mod h1:KG0jUBcgoJYeieKP5jboCq9UewwDBIOus8ZCqUMVlw8=
github.com/luxfi/age v1.6.0 h1:KMD8gSOP4NVCb7NWSlRcgBZNV2xm2a+qQWPyPmiX6f4=
github.com/luxfi/age v1.6.0/go.mod h1:7cu9CIyikgyAvr5MlXFapEDQ15yBaHOSdKkK5lG04WE=
github.com/luxfi/api v1.1.1 h1:CXD7m0quPmUm+Qw35TrF+E7b0Fq4qz9gHfrZ5gyrjHU=
github.com/luxfi/api v1.1.1/go.mod h1:g6J0iohVqaIj2aO1u/ZJPqjiX2tog0NM3/SBf7wJ4cA=
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/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.3.1 h1:grQhi/B5GKypG7avDMeY143QTgFbfEvQICKNIh1Cw6U=
github.com/luxfi/cache v1.3.1/go.mod h1:2MokdbeNUy/9O3mdREWkE6BiN7tRvePkXiKkcb+4M7g=
github.com/luxfi/chains v1.7.9 h1:sDF/WQn56o/4i1/uCS8i5ENDMuyWi8+iVcnYGAstMDM=
github.com/luxfi/chains v1.7.9/go.mod h1:Qtx9JNLOWYyhnvq434YSjPINT6we9nfViUIIuGD/2bc=
github.com/luxfi/codec v1.2.1 h1:NA/O3dWm9QejQPdjLEIVx42ddVqAXsy6Y6igL/V1+aU=
github.com/luxfi/codec v1.2.1/go.mod h1:xjWOTEbw9gxY/N8nZwQPvRCfPnK/ugJHBWsb3BZ0HHs=
github.com/luxfi/compress v0.1.1 h1:cQjRYQFRrw8HinjW1T8FmKgqdRwrFYDCdecc1t0i+uQ=
github.com/luxfi/compress v0.1.1/go.mod h1:d4CkRRmwPzafIp57Jxra3RmAmNwNwU8Oc0QBBRlTqGo=
github.com/luxfi/concurrent v0.1.1 h1:LkAmNXybOsAm97nFILqMDBp/YLgleuLmyGomFT7YNxU=
github.com/luxfi/concurrent v0.1.1/go.mod h1:GlkfiJtPBpatNxvROf7hkPi4gsnmdHGmqhDnVWFrkZE=
github.com/luxfi/consensus v1.36.10 h1:LJPvHc2zL7VFhbqfYu11rA50yvxzTK17Z4mRN04HQhU=
github.com/luxfi/consensus v1.36.10/go.mod h1:iwlx11D7BRdEaUqyUvueAe2Fit1K1k26avBK2eLYdnc=
github.com/luxfi/consensus v1.36.12 h1:MJKq6j9+04Y2NC+NArGQgv5QNcjprYCk+gppCz1PdSg=
github.com/luxfi/consensus v1.36.12/go.mod h1:mCtA6k31FVW/H6rsYQ7V8VvS198WsFS8oFo/z3IMI+A=
github.com/luxfi/constants v1.6.2 h1:pXHdKIFbfE9qX4xOjq2LxYvagNhhNvspUVEbPcIEKfA=
github.com/luxfi/constants v1.6.2/go.mod h1:r0oH8C/+r/XFYBq1AJxt6zWRKKRKgDzrEMop/CCs9rI=
github.com/luxfi/container v0.2.1 h1:MTnfKXzS5+oxV5jKZerdOxSA6iMPaQI9/FWGufizzaw=
github.com/luxfi/container v0.2.1/go.mod h1:B+uM0wP0lGvt/SSK7QOEn/qBcsHzILVHlKikdCyzSgM=
github.com/luxfi/corona v0.10.4 h1:/+Uy5iOWBMkr+XACnRRiyrbb5ebsZLsUXjHfJW3sFyw=
github.com/luxfi/corona v0.10.4/go.mod h1:44Tjnm2uRG22kmmLfCzR8QEO8OXZ3jR/OnUKLsnjVJ8=
github.com/luxfi/crypto v1.20.2 h1:L81WEsU/hs2A76F5PWBusG0yU74QqkDdUqqgexWUxh4=
github.com/luxfi/crypto v1.20.2/go.mod h1:qYHOM0lO4PRh7LEaObxFQUIMjmT1/paVm/WgZkobT1k=
github.com/luxfi/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/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/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/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.21.1 h1:GNnoWVa82l+n2dK7x2aG8LR2NEToq6ZCRX0sQjmK0OM=
github.com/luxfi/database v1.21.1/go.mod h1:Gc7Z2OPrrcYLnAL8B1trOnguXauOlSDV5tkviLN6Xec=
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/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.1.1 h1:MJhVXIPh1dbysvYEjtaEA/Z0FUTiI7n0DwOF54FS08c=
github.com/luxfi/formatting v1.1.1/go.mod h1:zhBWp6fLZduhpiAdPgVDdPVOyhw4FvwRUksF6+xKQCE=
github.com/luxfi/genesis v1.16.4 h1:8ZtqvgPnICgGpjBzAjYAPrI3qwr1g1DPbum+hgjc4bg=
github.com/luxfi/genesis v1.16.4/go.mod h1:0F6hV6GfwDSmIWsueB7/vqPZFxyS7A4Jo3p1QU87fFc=
github.com/luxfi/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/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.20.1 h1:QUGQr4AKvADjwMi7t8a0OfoyxShgEcI9pwie1jFYfm0=
github.com/luxfi/geth v1.20.1/go.mod h1:GV5bIMEgWviRN+jPXERyVpI16H3iHqPcdIokDoZdrvU=
github.com/luxfi/go-bip32 v1.1.0 h1:zjy19WKa1KJnGamRNyOZM3l/MPtV/sax7M4NMPwLHZQ=
github.com/luxfi/go-bip32 v1.1.0/go.mod h1:QyDXlzWL3xRiCbMUi4Z3J52Q/SMoCvdRLXXlYvSZor8=
github.com/luxfi/go-bip39 v1.2.0 h1:fx3pFuSGawCG4In6pA4OLLStqbgIqD1j8EygFskoHzY=
github.com/luxfi/go-bip39 v1.2.0/go.mod h1:if+2OVbG4k4jKIuBt/Rse1KV1kgWQM5j5xFbUtwbNtc=
github.com/luxfi/ids v1.3.2 h1:c6Rft5kZB4XqiCtWaGH47bfhaNFm3FGRfhEzI01GVeI=
github.com/luxfi/ids v1.3.2/go.mod h1:+5l8cYMbKpORJbQ2r98CYJo9TQATgUdnmzpYFZWMwwc=
github.com/luxfi/keychain v1.1.1 h1:dTYEPy6CGVC1sogMci4iJogUvW6VdTmemplQdzRqnAs=
github.com/luxfi/keychain v1.1.1/go.mod h1:hAzBcwxGumtoYrM5hfhwdt8wE0p7r2JCd5AxswqfkoY=
github.com/luxfi/keys v1.4.1 h1:2Zcoovaz9OLPz7m7VGXfRrGnrlqt0GeUpJclsPBi4EU=
github.com/luxfi/keys v1.4.1/go.mod h1:P8EUP5DKrR1SUZBGZjDT3rWcp2P1miUlVh7IBRNBphU=
github.com/luxfi/kms v1.12.10 h1:fXgisnBkixFSrqhOFbZVRQkf8cX4q4vS3Gix1qjL3PQ=
github.com/luxfi/kms v1.12.10/go.mod h1:CCcWDXIlDT2TwfYAUxedLyzShFRJAlVlp15zQ2L3CrU=
github.com/luxfi/geth v1.17.12 h1:UP/fhpcfbGPTrkOCwX3d88Oc3jVm5gTOgfjgq+lek6s=
github.com/luxfi/geth v1.17.12/go.mod h1:3vQfQJd9JC+AVBjxNXa9PYQOqpbE2dKu8E3jqhPZ3LU=
github.com/luxfi/go-bip32 v1.0.2 h1:7vFbb+Wr4Z499q2tuCLdd7wWjtn8sH+HWBlx76mhH9Y=
github.com/luxfi/go-bip32 v1.0.2/go.mod h1:bc7/LXDKAJQZ/F0Xjf5yXaTZxY9/ssLb4FC+Hxn/cDk=
github.com/luxfi/go-bip39 v1.1.2 h1:p+wLMPGs6MLQh7q0YIsmy2EhHL7LHiELEGTJko6t/Jg=
github.com/luxfi/go-bip39 v1.1.2/go.mod h1:96de9VkR2kY/ASAnhMtvt3TSh+PZkAFAngNj0GjRGDo=
github.com/luxfi/ids v1.3.0 h1:11xnwRDm6zQzbqcRnkFujOYkvhK4Fs/+g+sKRlRUNsU=
github.com/luxfi/ids v1.3.0/go.mod h1:6vpdcdZW0qxeade+3xby8aLTutbcJ7O0r8+fNQrksGI=
github.com/luxfi/keychain v1.0.2 h1:uQgmjs37/VBIALEiYrrszTpxvtqr07/YvS9TnmxGafs=
github.com/luxfi/keychain v1.0.2/go.mod h1:q/4ULgZBlstKkwzOzG/0T6y73BDPgnkrcibbJyTvmbU=
github.com/luxfi/keys v1.2.0 h1:3TAcr4twyMpwQp7J29ZRtIa5vzAoDrnXnLcPKVHJWmw=
github.com/luxfi/keys v1.2.0/go.mod h1:SjsAaxo6sGmSp9OaHXUiVCqsknO8iPspN6jMOoEAMb8=
github.com/luxfi/kms v1.11.7 h1:E25z8SCNTGOVvzzg5tj6pwJQ2K3FrE/nuy0KAfF+0zs=
github.com/luxfi/kms v1.11.7/go.mod h1:XhLUVqN4RBv6j4Bj3MNgTZmHCnm74jH7RqqK0b9xbzw=
github.com/luxfi/lattice/v7 v7.1.4 h1:hQR02M6cHTAV5+joOPi9gb9Gm+z/hKJnhJF4IlciIJs=
github.com/luxfi/lattice/v7 v7.1.4/go.mod h1:DmIQFi3mJiehVsR235l1NKYEU0JhU649OX5p7gMEW2c=
github.com/luxfi/lens v0.2.1 h1:5Qd0GdjbM+XUVgwDbZ452tKkR7yeE8QnBTHHaH8fJNY=
github.com/luxfi/lens v0.2.1/go.mod h1:6FIhC8weEE5RbNMF3SaE+XPSB9cr6FmjypYBoHkz4JQ=
github.com/luxfi/light v1.0.0 h1:zTJgp5M0xX8rIwRjYDQgOGhLas3rgXyhkszrTX+ne3s=
github.com/luxfi/light v1.0.0/go.mod h1:1G0kgjEe/srlBMCIrFq4IvhnrMuHKFG18CRUWfw1z30=
github.com/luxfi/lens v0.1.4 h1:goGjGDXx2BNdjzXDunL5QT8elK2ZyCcc0z8TAbtWYrg=
github.com/luxfi/lens v0.1.4/go.mod h1:mL+G8IK+9L41d78/2FYRgfhEzAjcr5+VEXB8SGuHbus=
github.com/luxfi/log v1.4.3 h1:xkUKRWvQ4ZwvlUC2e0/RTtHYZOYSMvSQ9W9lbjwBmiI=
github.com/luxfi/log v1.4.3/go.mod h1:myIkufyiQomSQH34K981kbz6cG4WUoerRUh7F4XhlQI=
github.com/luxfi/magnetar v1.2.3 h1:n4UrJZLK+mhDDZr1HLl2H/KgA6o6v62r5oiC61R7awE=
github.com/luxfi/magnetar v1.2.3/go.mod h1:z9PLkqzzYiaFGT/qFBQSnNoHmZrg8y7JlYGiNnHAAdk=
github.com/luxfi/math v1.5.1 h1:FDOY75e4vn/Xra1ij99xOS/9XdxQGCPP6HONHRkCwfg=
github.com/luxfi/math v1.5.1/go.mod h1:3j9R24hVfPhrbvs45YSJP7jAyVNfwx/cj/+lAO8IGro=
github.com/luxfi/math v1.4.1 h1:1t9bCCsEqnl9yIKrShlbs80DBKyYTWdnzkVfBqEeO7Q=
github.com/luxfi/math v1.4.1/go.mod h1:QvbRxauQyE1w4lvbcLSe6c8yeJz2Zj1Bq1rayGgs2tA=
github.com/luxfi/math/big v0.1.0 h1:Vz4c0RsZVPdIKPsHPgAJChH/R3p15WHRUz7LkLf+NIQ=
github.com/luxfi/math/big v0.1.0/go.mod h1:BuxSu22RbO93xBLk5Eam5nldFponoJ73xDFz4uJ3Huk=
github.com/luxfi/math/safe v0.0.1 h1:GfSBINV9mOFgHzd32JbgfHSLhlNn0BwnP43rteYEosc=
github.com/luxfi/math/safe v0.0.1/go.mod h1:EejrmOJHh03YAD8+Zww8cPcMR1K3Q2I7w1dX4sMloeo=
github.com/luxfi/mdns v0.1.1 h1:g2eRr9AXcziPkkcd24M+Qu9ApEpoKKjfI79QSNqv0rQ=
github.com/luxfi/mdns v0.1.1/go.mod h1:dbp5f3h3aE7CGzwbaWzBM9cwdcekhmSrWhQevgYhhNA=
github.com/luxfi/metric v1.8.1 h1:v58GgPFAOLPVxSa/JiNLwqJQNEFHdWbXZV28piMXX4s=
github.com/luxfi/metric v1.8.1/go.mod h1:R1OPAIeW4UBW3osK7j2r3/XPmczfNRFTXg4bnlemTuE=
github.com/luxfi/mlwe v0.3.0 h1:5mtXLbO2RxaE45r75sj43c6UdpjDKQ5nTQcOGuoRQT8=
github.com/luxfi/mlwe v0.3.0/go.mod h1:DD9EHTeiyh/y0KGGeqL+q9S4n8raeGiGdaG/BQPAvT0=
github.com/luxfi/metric v1.5.9 h1:UAgXMNZf5oN/XJwwuKorf8iMaCj3nyP6thHPCwkUwY4=
github.com/luxfi/metric v1.5.9/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=
github.com/luxfi/mock v0.1.1/go.mod h1:jo35akl3Vtd8LbzDts8VJ0jmSVycrd1/eBi6g6t5hKU=
github.com/luxfi/net v0.1.1 h1:jIQCb8ulBGEvHIcorzDDNCeWzutFzLVtRWodutrQE24=
github.com/luxfi/net v0.1.1/go.mod h1:SwxbUQ538u4QAcgC/N61uahDk1TDHL6Ku89CX/WV2lk=
github.com/luxfi/p2p v1.22.1 h1:M59Iy+FIJta99aFfTpG6EE70jm9uqIX+iHrGBYPHDhg=
github.com/luxfi/p2p v1.22.1/go.mod h1:FHOSavVcq8JS1ZQtfddd9Jj1gaPstz1cjvNgczAQRyg=
github.com/luxfi/pq v1.1.0 h1:ADplfUSyirLymSxs3Ix0HeDTyl5oswCNUpXJt/5vLY8=
github.com/luxfi/pq v1.1.0/go.mod h1:KT5rG9ztpzIkT9QSnXK4WFqBBLzKCLjY7l1c/unBi8I=
github.com/luxfi/precompile v0.19.3 h1:ljJholS+9mlR8EjbfM96BLYvIni7xCt7KkX06YzAIJU=
github.com/luxfi/precompile v0.19.3/go.mod h1:c+dh+FWfpKr3FNfI4LhIdnn0naQmmS5Yz0KKTt4LknY=
github.com/luxfi/proto v1.4.2 h1:dEGTE7xOWVPTXRZNDBJfwh2Q19vE7MQBhyXXmWenw+8=
github.com/luxfi/proto v1.4.2/go.mod h1:vwVkrC9ghhuCL8bluA59+G5jaD6WuqiDyd1iKh7yzOE=
github.com/luxfi/protocol v0.0.2 h1:TAuXMm1K4VDpG3B3brq1EE9Lb7XlbhQmVBObskgNhmc=
github.com/luxfi/protocol v0.0.2/go.mod h1:Yjn2VRwn5PXim0+JTalAyrVG6YbmuSnYOfeUXSbAsqA=
github.com/luxfi/pulsar v1.9.2 h1:pFLoAfBlCwFZfchqHn78J6yMe29AhaoKGRa/KTUP8CE=
github.com/luxfi/pulsar v1.9.2/go.mod h1:uTOtribcUvTTwAOy0Ztg0S2AUiNAsVqFfopTrKW+zjM=
github.com/luxfi/resource v0.1.1 h1:k11s5xLGX85UWq/iLZyWLhnqeLTlp3FHEt8u/8AHdkY=
github.com/luxfi/resource v0.1.1/go.mod h1:s7SbZOSVbgj9bWFOcLCcXgnMlHxbqtqhAeJ3f0Xuy/Y=
github.com/luxfi/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/proto v1.3.5 h1:AW11rnu5xyvB7beyowoiY9uIffLOF3+eMR/a3EkK2c8=
github.com/luxfi/proto v1.3.5/go.mod h1:ixTofGpdW1rTYr+wgTuBhAsgBv8GnWYHMLWbPNEdm7M=
github.com/luxfi/pulsar v1.9.0 h1:c0JnatYF79aN87aof9VlYjIoCzmixxrgNPeUUuh8ScU=
github.com/luxfi/pulsar v1.9.0/go.mod h1:1+/atAiiiOm9RnXM3c66eHF3garjAa3C+sn4rAU7JUU=
github.com/luxfi/resource v0.0.1 h1:mTh+ICWSy548GTUSSyx7V/X5dV18oEwxZeQEYGJQhD4=
github.com/luxfi/resource v0.0.1/go.mod h1:wWpZktciYwIi6RNqA+fHwzmPrUJa7PRX7urfwT+spRE=
github.com/luxfi/rpc v1.1.0 h1:B/PJbK399th1mHRDSufhCpVbAciZqId3LsaWhIGNWH4=
github.com/luxfi/rpc v1.1.0/go.mod h1:s0bI7/Wg1ZdFdG/cQK+4pZNdEmUsXNBA3HeZRZ+XLeM=
github.com/luxfi/runtime v1.3.1 h1:vsQZ3sl6XMeyHuNGCMM86d0whrE/lhMre4CCJaClPdE=
github.com/luxfi/runtime v1.3.1/go.mod h1:Tct998uUcmCQbUC1WLEeGLDH2IaVgMb+SkKcHKZpHV0=
github.com/luxfi/runtime v1.1.3 h1:6Yp/PKwQCohjXmBR9GA+gamdSAp+xA2rdN6J/74Y4aw=
github.com/luxfi/runtime v1.1.3/go.mod h1:r1uonDnxRCnPz6N6WYwaC72HW95KbFIAyChnJyxePGs=
github.com/luxfi/sampler v1.1.0 h1:u3iRDl7V06ARh0e85h3HT+aZ1saCFo2yMMsh+dCJbqk=
github.com/luxfi/sampler v1.1.0/go.mod h1:kJa53S3tC9+VSbuV3RFu68MmbCCBlr2UM39LOClQ/Hs=
github.com/luxfi/sdk v1.18.1 h1:KnqkL6bNATtQXHbnTMsue5pLz9tn74YFAoux2ek+k90=
github.com/luxfi/sdk v1.18.1/go.mod h1:8BE1iF2ewkDbsV31TPE0/HT7RGJILR1ZALbKE1GgHMc=
github.com/luxfi/staking v1.6.1 h1:be023mY88AnFgF9P+MMvILX21I2CaqVAVkGcc+a20so=
github.com/luxfi/staking v1.6.1/go.mod h1:X8i/uCLc009mlIUnFMErrQMCwfGaOVCAVf+GbDN5nn4=
github.com/luxfi/sdk v1.17.9 h1:MfExzWNym7IicO2egiHg6N0WnImLtAUpjCpiD/zc2ZE=
github.com/luxfi/sdk v1.17.9/go.mod h1:XvZuopyltjR4SvHvA1c6wtNcnO+FzLyjfm0v+FyN9sI=
github.com/luxfi/staking v1.5.1 h1:f9MaGnRm0xc02crDm5Qs1T2r88d3KzNkHZypAvsmAlU=
github.com/luxfi/staking v1.5.1/go.mod h1:lT7KLaiTpdq3lg78H0gp2qSEfX9LaK1vs7w73XV/9nw=
github.com/luxfi/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.3 h1:CYqcPfzVUs0M/+e/ja1q7aLBZZUi6Rv/XIXR5lrZ7aE=
github.com/luxfi/threshold v1.12.3/go.mod h1:xQT8xzmh9pQ1CdBgpl7P3ezXQZcqTPv3tqpIytvuiTA=
github.com/luxfi/timer v1.1.1 h1:54GiNBKydQ0VF5/EwVc/mCsbqe0yJNfZV7Ae8qJhCwk=
github.com/luxfi/timer v1.1.1/go.mod h1:OXY/8ZFKCdEsimpfnUG1MQWvzjjFbsmBOiW/m6KSrC0=
github.com/luxfi/tls v1.1.1 h1:BSZ0gHSp7U8vzlmzx7WSSCz+b7Ky4JtD9HDDhn7vrDg=
github.com/luxfi/tls v1.1.1/go.mod h1:+5TDy8UtLL+tz124brZzpUDBRj+sKrq0JFqdmpMUHgw=
github.com/luxfi/trace v1.2.1 h1:MPV079P2eTijB7F06AyJU1HJwpQVxRx1qYXhva9YsvM=
github.com/luxfi/trace v1.2.1/go.mod h1:/bX8g0RRHPHUq7kX8of/Aaq6C7rD0JuNH57vVbw7ZG8=
github.com/luxfi/threshold v1.12.0 h1:JJ369xC/YyDvrqXj+xFoK98nP2rUM099qFs03hBvq/M=
github.com/luxfi/threshold v1.12.0/go.mod h1:iuRQGDAy8ZKjQhZjkSKg7NtbP75/8Up9zj52y7IuyZo=
github.com/luxfi/timer v1.0.2 h1:g/odi0VQJIsrzdklJUG1thHZ/sGNnbIiVGcU6LctJm0=
github.com/luxfi/timer v1.0.2/go.mod h1:SoaZwntYigUE3H6z1GV32YwP8QaSiAT0UiEv7iPugXg=
github.com/luxfi/tls v1.0.3 h1:rK3nxSAxrUOOSHOZnKChwV4f6UJ+cfOl8KWJXAQx/SI=
github.com/luxfi/tls v1.0.3/go.mod h1:dQqSiGE7YxXUxOwICoReUuIitBms9DYOaCeteBwmIWw=
github.com/luxfi/trace v1.1.0 h1:eQoObwStrdQN879zfJWJeN1l0FJnfOKQHQHyEJOYjCI=
github.com/luxfi/trace v1.1.0/go.mod h1:Sgtpj8ZE5GBSi4ZyQOL3rL9enl59sSWswWWKw3BUdpk=
github.com/luxfi/units v1.0.0 h1:2aNVB+WsP1XeDob71IsO0w3jJqP3FtZdYnFsmORkJZg=
github.com/luxfi/units v1.0.0/go.mod h1:tma28v4ed1tupdS0kpSeyO+u1wWK/g1NqODPbN1YzmA=
github.com/luxfi/upgrade v1.0.3 h1:mFMfIb88HzoL4fp7I6w1g+VnxlAWj6UQdZOdf4AkCLk=
github.com/luxfi/upgrade v1.0.3/go.mod h1:3c/u4T/n7EsODofkWg5VigSt9BATQM8EmaNttfHMCrU=
github.com/luxfi/utils v1.3.1 h1:Us02ag60kGu94B41XIelExa7c+K6zPKwDJyq+eB+hc0=
github.com/luxfi/utils v1.3.1/go.mod h1:ROZrzpt6Kx8ttS1mo12oqsOzRB088GQ1h9jXEoDDpNA=
github.com/luxfi/utxo v0.5.8 h1:HydTOKERb8vY0Z39Dvg/V3qg7My3N/eXOY4nLv3iezg=
github.com/luxfi/utxo v0.5.8/go.mod h1:kqkwMm99NbWwGZrOzuRzF0vck+lCJwrlejmmCwj5pZc=
github.com/luxfi/validators v1.3.1 h1:+/7j0CTXlMKyaSLFM+gd6Fq64/edORJfrD/xGnf7Xcg=
github.com/luxfi/validators v1.3.1/go.mod h1:sIQyUZOvXoJ/9/RCOhHSzjumZIoxiqacNOn5mQWVozU=
github.com/luxfi/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/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.3.1 h1:U2Bv7IDRFv8JtjUARfY53ZnOPRB2iPrPHSMDCQ+T0Js=
github.com/luxfi/vm v1.3.1/go.mod h1:6YR/uFV2FoofmogQShv+HM7V8alz5rbrQYjp5w0bNVA=
github.com/luxfi/vm v1.3.3 h1:BEBamD9VUcPG8mtL2Ga0Us1lTQ+ubaZqng/PZjNqzPc=
github.com/luxfi/vm v1.3.3/go.mod h1:v2XjW0yymxHwy1xPsSz338s0J8nRqOIb6RiDf0wUEgk=
github.com/luxfi/warp v1.24.1 h1:9F+z6fy4sxmXp+3LlR9m3TiZ8Dfthh+s5KOeewSJL30=
github.com/luxfi/warp v1.24.1/go.mod h1:kRGQDt6EB3oRLYo71aXqnmJuCBVfND3oQ5/2miaLoyg=
github.com/luxfi/zap v1.2.6 h1:NBpbm9Gib41Oi/XAkAZKQ3hb+xCafo7JsrUjw+bKiAc=
github.com/luxfi/zap v1.2.6/go.mod h1:sTAe/AMMamoE85cVoe81+NbqHJkgvqS0LhY9ByHEmr0=
github.com/luxfi/zapcodec v1.1.1 h1:SdYexj7oWdks/wfF9s+8m/9PAYt3S8QjORxURutvIs0=
github.com/luxfi/zapcodec v1.1.1/go.mod h1:fmmgd8C/JrQdh3b1OzBkrvPN4TYs5uRfVV3sVtbiLbQ=
github.com/luxfi/vm v1.2.5 h1:L1etY/gh68f9tns1BtyDUpZcBVqc3Ng1mqU3n38GyLo=
github.com/luxfi/vm v1.2.5/go.mod h1:TCCg4lDcQFCjxaxfXnxPIrpRSVAyyf2ucT4A4w654Hg=
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/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=
github.com/luxfi/zapdb v1.10.1/go.mod h1:3Y0hH2A9kvjR+Bp9N2yEbtHnhXGHhqCQOLvBRkHrrM0=
github.com/luxfi/zwing v0.6.1 h1:Ve7RvYVBXx4JWe8TZhLfpez1N1G685hl/PSMDsR4RI4=
github.com/luxfi/zwing v0.6.1/go.mod h1:Eal4hnjmdFXc6rxciA6cxeCfV1BKs8le03v83W5oiKg=
github.com/luxfi/zwing v0.5.2 h1:2+nDKHVIdT8GvaKO79GuO3x/3DgJvX0gI32h4P+P6RU=
github.com/luxfi/zwing v0.5.2/go.mod h1:8nixkEL3bhO2LrqVqhJ8WgT+QGUtnCPIQIhFQh9gQio=
github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
@@ -533,15 +518,15 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.68.0 h1:8rQJvQmYltsR2L7h8Zw0Iyj8WYNNmpwikoQTZXwfVeA=
github.com/prometheus/common v0.68.0/go.mod h1:4soH+U8yJSROk7OJ//hmTiWKsxapv6zRGgTt3keN8gQ=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc=
github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
@@ -599,33 +584,27 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk=
github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk=
github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU=
github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI=
github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4=
github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg=
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.73.0 h1:ocTOORnBWtJ+P8t/6wAjdkchMzdfHmWx2VD/DPbgZ7s=
github.com/valyala/fasthttp v1.73.0/go.mod h1:EtXQDHaR+5P18p8wqDRFpUhxr108Ga9mXvVJXHRrN2k=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zap-proto/go v1.1.0 h1:DDGhuTBNqkmNax7QV/VvkInJR7hiOcybwUjN3YBt8fg=
github.com/zap-proto/go v1.1.0/go.mod h1:914SNGTH6Rv3Yu1MweWJBPEN8FZlo5C39QyhaB0C7Q0=
github.com/zap-proto/http v0.3.0 h1:l7DvlngiYqmzNY6fzyRYw2ZIAhF35FqwOe6mvAOqpMg=
github.com/zap-proto/http v0.3.0/go.mod h1:UYfGhDDCetgxs65XSev8Lpf65COg5vKQK+cWwZGh4zQ=
github.com/zap-proto/http v0.0.0-20260506200741-fd6047874433 h1:7WsCr/pZvWozimdYNffL3B9K6gLr8w0Z7WAi1+eZWtc=
github.com/zap-proto/http v0.0.0-20260506200741-fd6047874433/go.mod h1:xySyVTIwjknmVE+p+6rukkX86rFZtXeAu/QY7KXMYqw=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
@@ -664,16 +643,16 @@ golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9 h1:4d4PbuBNwaxMXkXI8yiIYjydtMU+04RHeuSxJdgKftM=
golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -688,15 +667,15 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -719,8 +698,8 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -728,8 +707,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -739,8 +718,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -748,10 +727,6 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
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/v1/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/ext/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/v1/index/C/block
// - http://1.2.3.4:9650/v1/index/X/tx
// - http://1.2.3.4:9650/ext/index/C/block
// - http://1.2.3.4:9650/ext/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 + "/v1/index/P/block"
uri = primary.LocalAPIURI + "/ext/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(platformvmBlockBytes)
platformvmBlock, err := platformvmblock.Parse(platformvmblock.Codec, 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 + "/v1/index/X/block"
uri = primary.LocalAPIURI + "/ext/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
```
/v1/index/C/block
/ext/index/C/block
```
### P-Chain Blocks
```
/v1/index/P/block
/ext/index/P/block
```
### X-Chain Transactions
```
/v1/index/X/tx
/ext/index/X/tx
```
### X-Chain Blocks
```
/v1/index/X/block
/ext/index/X/block
```
<Callout type="warn">
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.
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.
</Callout>
## Methods
@@ -87,7 +87,7 @@ index.getContainerByID({
**Example Call**:
```sh
curl --location --request POST 'localhost:9630/v1/index/X/tx' \
curl --location --request POST 'localhost:9630/ext/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/v1/index/X/tx' \
curl --location --request POST 'localhost:9630/ext/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/v1/index/X/tx' \
curl --location --request POST 'localhost:9630/ext/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/v1/index/X/tx' \
curl --location --request POST 'localhost:9630/ext/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/v1/index/X/tx' \
curl --location --request POST 'localhost:9630/ext/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/v1/index/X/tx' \
curl --location --request POST 'localhost:9630/ext/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/v1/index/X/tx' \
curl --location --request POST 'https://indexer-demo.lux.network/ext/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/v1/bc/X
}' -H 'content-type:application/json;' https://api.lux.network/ext/bc/X
```
**Response**:
+3 -3
View File
@@ -79,7 +79,7 @@ spec:
cpu: "4"
livenessProbe:
httpGet:
path: /v1/health
path: /ext/health
port: 9630
initialDelaySeconds: 120
periodSeconds: 30
@@ -87,7 +87,7 @@ spec:
timeoutSeconds: 10
readinessProbe:
httpGet:
path: /v1/health
path: /ext/health
port: 9630
initialDelaySeconds: 30
periodSeconds: 10
@@ -95,7 +95,7 @@ spec:
timeoutSeconds: 5
startupProbe:
httpGet:
path: /v1/health
path: /ext/health
port: 9630
initialDelaySeconds: 10
periodSeconds: 10
-36
View File
@@ -34,20 +34,6 @@ type Net interface {
// IsBootstrapped returns true if the chains in this chain are done bootstrapping
IsBootstrapped() bool
// Bootstrapping returns the IDs of the chains in this net that have NOT yet
// finished initial sync. The net owns the bootstrapping set, so it is the
// net — not its caller — that can name those chains.
Bootstrapping() []ids.ID
// IsChainBootstrapped reports whether a SPECIFIC chain in this net has finished
// initial sync — i.e. Bootstrapped(chainID) was called for it (the chain reached
// the network frontier and its VM went to normal operation). This is the per-chain
// 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)
@@ -80,28 +66,6 @@ func (s *chain) IsBootstrapped() bool {
return s.bootstrapping.Len() == 0
}
func (s *chain) Bootstrapping() []ids.ID {
s.lock.RLock()
defer s.lock.RUnlock()
return s.bootstrapping.List()
}
// IsChainBootstrapped assumes MONOTONIC per-process bootstrapped state: a chain
// only ever moves bootstrapping→bootstrapped (Bootstrapped is forward-only and
// AddChain refuses to re-add a chain already in either set), so this signal — and
// 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()
-23
View File
@@ -68,26 +68,3 @@ func TestIsAllowed(t *testing.T) {
require.False(s.IsAllowed(ids.GenerateTestNodeID(), false), "Non-validator should not be allowed with validator only rules and allowed nodes")
require.True(s.IsAllowed(allowedNodeID, true), "Non-validator allowed node should be allowed with validator only rules and allowed nodes")
}
// Bootstrapping must name the CHAINS still syncing. The net-level aggregate
// IsBootstrapped() only says "something here is unconverged"; only the chain
// IDs say what.
func TestNetBootstrappingNamesTheChains(t *testing.T) {
require := require.New(t)
chainID0 := ids.GenerateTestID()
chainID1 := ids.GenerateTestID()
s := New(ids.GenerateTestNodeID(), Config{})
require.Empty(s.Bootstrapping())
s.AddChain(chainID0)
s.AddChain(chainID1)
require.ElementsMatch([]ids.ID{chainID0, chainID1}, s.Bootstrapping())
s.Bootstrapped(chainID0)
require.Equal([]ids.ID{chainID1}, s.Bootstrapping())
s.Bootstrapped(chainID1)
require.Empty(s.Bootstrapping())
}
+6 -8
View File
@@ -340,16 +340,15 @@ func NewNetwork(
// AddPermissionlessValidatorTx (modern). Handle both.
switch tx := validatorTx.Unsigned.(type) {
case *txs.AddPermissionlessValidatorTx:
validator := tx.Validator()
nodeID := validator.NodeID
weight := validator.Wght
nodeID := tx.Validator.NodeID
weight := tx.Validator.Wght
if weight == 0 {
weight = 1
}
var blsKey []byte
if s := tx.Signer(); s != nil {
if pubKey := s.Key(); pubKey != nil {
if tx.Signer != nil {
if pubKey := tx.Signer.Key(); pubKey != nil {
blsKey = bls.PublicKeyToCompressedBytes(pubKey)
}
}
@@ -369,9 +368,8 @@ func NewNetwork(
zap.Int("blsKeyLen", len(blsKey)),
)
case *txs.AddValidatorTx:
validator := tx.Validator()
nodeID := validator.NodeID
weight := validator.Wght
nodeID := tx.Validator.NodeID
weight := tx.Validator.Wght
if weight == 0 {
weight = 1
}
+5 -74
View File
@@ -14,10 +14,9 @@ import (
"github.com/luxfi/math/set"
"github.com/luxfi/node/message"
"github.com/luxfi/node/proto/p2p"
"github.com/luxfi/node/trace"
"github.com/luxfi/node/version"
"github.com/luxfi/timer"
luxversion "github.com/luxfi/version"
"github.com/luxfi/node/trace"
)
// router implements Router interface for routing messages to chain handlers
@@ -194,94 +193,26 @@ func (r *chainRouter) AddChain(ctx context.Context, chainID ids.ID, h handler.Ha
)
}
// versionedConnector is the capability a chain handler advertises when it can
// forward a peer's REAL application version to its VM. The consensus
// handler.Handler.Connected signature carries only the nodeID (the version was
// dropped at that boundary); a handler that implements this receives the real
// version instead. blockHandler implements it. The version must survive to the
// inner VM: the C-Chain (coreth) state-sync peer tracker compares peer versions
// and dereferences a nil version, panicking a state-syncing node.
type versionedConnector interface {
ConnectedWithVersion(ctx context.Context, nodeID ids.NodeID, nodeVersion *luxversion.Application) error
}
// toAppVersion converts the node's peer version (github.com/luxfi/node/version)
// to the github.com/luxfi/version.Application the VM Connected boundary
// (chain.VersionInfo) expects. nil-safe: a nil peer version maps to nil.
func toAppVersion(v *version.Application) *luxversion.Application {
if v == nil {
return nil
}
return &luxversion.Application{
Name: v.Name,
Major: v.Major,
Minor: v.Minor,
Patch: v.Patch,
}
}
func (r *chainRouter) Connected(nodeID ids.NodeID, nodeVersion *version.Application, netID ids.ID) {
r.lock.Lock()
r.connectedPeers.Add(nodeID)
handlers := make([]handler.Handler, 0, len(r.chains))
for _, h := range r.chains {
handlers = append(handlers, h)
}
r.lock.Unlock()
defer r.lock.Unlock()
r.connectedPeers.Add(nodeID)
r.log.Debug("peer connected",
log.Stringer("nodeID", nodeID),
log.Any("version", nodeVersion),
log.Stringer("netID", netID),
)
// Deliver the connection to every chain handler so VMs observe peer
// connectivity — the P-chain uptime tracker in particular. Handlers dedup, so
// repeated dispatch of the same connection (once per tracked network) is
// safe. Dispatch OUTSIDE the router lock: a handler must never re-enter the
// router while we hold it.
//
// Deliver the REAL peer version through the versionedConnector capability so
// it reaches the inner VM (proposervm → coreth state-sync). Only handlers
// that cannot carry a version fall back to the plain nodeID-only Connected.
appVersion := toAppVersion(nodeVersion)
for _, h := range handlers {
var err error
if vc, ok := h.(versionedConnector); ok {
err = vc.ConnectedWithVersion(context.Background(), nodeID, appVersion)
} else {
err = h.Connected(context.Background(), nodeID)
}
if err != nil {
r.log.Debug("chain handler Connected failed",
log.Stringer("nodeID", nodeID),
log.Err(err),
)
}
}
}
func (r *chainRouter) Disconnected(nodeID ids.NodeID) {
r.lock.Lock()
r.connectedPeers.Remove(nodeID)
handlers := make([]handler.Handler, 0, len(r.chains))
for _, h := range r.chains {
handlers = append(handlers, h)
}
r.lock.Unlock()
defer r.lock.Unlock()
r.connectedPeers.Remove(nodeID)
r.log.Debug("peer disconnected",
log.Stringer("nodeID", nodeID),
)
for _, h := range handlers {
if err := h.Disconnected(context.Background(), nodeID); err != nil {
r.log.Debug("chain handler Disconnected failed",
log.Stringer("nodeID", nodeID),
log.Err(err),
)
}
}
}
func (r *chainRouter) Benched(chainID ids.ID, nodeID ids.NodeID) {
@@ -1,90 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package node
import (
"context"
"sync"
"testing"
"github.com/stretchr/testify/require"
"github.com/luxfi/consensus/networking/handler"
"github.com/luxfi/constants"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/math/set"
"github.com/luxfi/node/version"
luxversion "github.com/luxfi/version"
)
// fakeVersionedHandler implements handler.Handler AND the versionedConnector
// capability, recording which path the router used and the version delivered.
type fakeVersionedHandler struct {
mu sync.Mutex
versionedHit bool
plainHit bool
gotAppVersion *luxversion.Application
}
func (h *fakeVersionedHandler) HandleInbound(context.Context, handler.Message) error { return nil }
func (h *fakeVersionedHandler) HandleOutbound(context.Context, handler.Message) error { return nil }
func (h *fakeVersionedHandler) Connected(context.Context, ids.NodeID) error {
h.mu.Lock()
defer h.mu.Unlock()
h.plainHit = true
return nil
}
func (h *fakeVersionedHandler) Disconnected(context.Context, ids.NodeID) error { return nil }
func (h *fakeVersionedHandler) ConnectedWithVersion(_ context.Context, _ ids.NodeID, v *luxversion.Application) error {
h.mu.Lock()
defer h.mu.Unlock()
h.versionedHit = true
h.gotAppVersion = v
return nil
}
// TestChainRouterConnectedDeliversConvertedVersion is the router half of the
// RED CRITICAL #1 fix: chainRouter.Connected must deliver the REAL peer version
// to a version-capable handler, converting it from the node's peer version type
// (github.com/luxfi/node/version) to the VM boundary type
// (github.com/luxfi/version, aka chain.VersionInfo). The old code dropped the
// version at dispatch (h.Connected(ctx, nodeID)); the fix routes through the
// versionedConnector capability so the real, converted version survives.
func TestChainRouterConnectedDeliversConvertedVersion(t *testing.T) {
require := require.New(t)
h := &fakeVersionedHandler{}
chainID := ids.GenerateTestID()
r := &chainRouter{
log: log.Noop(),
chains: map[ids.ID]handler.Handler{chainID: h},
connectedPeers: set.NewSet[ids.NodeID](1),
}
nodeID := ids.GenerateTestNodeID()
peerVersion := &version.Application{Name: "lux", Major: 1, Minor: 36, Patch: 27}
r.Connected(nodeID, peerVersion, constants.PrimaryNetworkID)
h.mu.Lock()
defer h.mu.Unlock()
require.True(h.versionedHit, "router must use the versioned capability path for a version-capable handler")
require.False(h.plainHit, "router must NOT fall back to the nil-version plain Connected")
require.NotNil(h.gotAppVersion, "handler must receive a non-nil converted version")
require.Equal("lux", h.gotAppVersion.Name)
require.Equal(1, h.gotAppVersion.Major)
require.Equal(36, h.gotAppVersion.Minor)
require.Equal(27, h.gotAppVersion.Patch)
}
// TestToAppVersionNilSafe documents that a nil peer version converts to nil
// (never a panic) — the conversion is defensive at the boundary.
func TestToAppVersionNilSafe(t *testing.T) {
require.Nil(t, toAppVersion(nil))
}
+265
View File
@@ -0,0 +1,265 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package node
import (
"crypto/tls"
"net/netip"
"time"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/genesis/pkg/genesis"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/math/set"
"github.com/luxfi/node/server/http"
"github.com/luxfi/node/benchlist"
"github.com/luxfi/node/chains"
"github.com/luxfi/node/nets"
"github.com/luxfi/node/network"
"github.com/luxfi/node/trace"
"github.com/luxfi/node/upgrade"
"github.com/luxfi/node/vms/platformvm/txs/fee"
"github.com/luxfi/timer"
"github.com/luxfi/node/utils/profiler"
)
type APIIndexerConfig struct {
IndexAPIEnabled bool `json:"indexAPIEnabled"`
IndexAllowIncomplete bool `json:"indexAllowIncomplete"`
}
type TargeterConfig struct {
// VdrAlloc is the percentage of resource usage that is attributed to validators
// The range is [0, 1], defaults to 1 (100%)
VdrAlloc float64 `json:"vdrAlloc"`
// MaxNonVdrUsage is the maximum amount of resources that non-validators can use
// The range is [0, 1], defaults to 0
MaxNonVdrUsage float64 `json:"maxNonVdrUsage"`
// MaxNonVdrNodeUsage is the maximum amount of resources that a non-validator node can use
// The range is [0, 1], defaults to 0
MaxNonVdrNodeUsage float64 `json:"maxNonVdrNodeUsage"`
}
type HTTPConfig struct {
server.HTTPConfig
APIConfig `json:"apiConfig"`
HTTPHost string `json:"httpHost"`
HTTPPort uint16 `json:"httpPort"`
HTTPSEnabled bool `json:"httpsEnabled"`
HTTPSKey []byte `json:"-"`
HTTPSCert []byte `json:"-"`
HTTPAllowedOrigins []string `json:"httpAllowedOrigins"`
HTTPAllowedHosts []string `json:"httpAllowedHosts"`
ShutdownTimeout time.Duration `json:"shutdownTimeout"`
ShutdownWait time.Duration `json:"shutdownWait"`
}
type APIConfig struct {
APIIndexerConfig `json:"indexerConfig"`
// Enable/Disable APIs
AdminAPIEnabled bool `json:"adminAPIEnabled"`
InfoAPIEnabled bool `json:"infoAPIEnabled"`
KeystoreAPIEnabled bool `json:"keystoreAPIEnabled"`
MetricsAPIEnabled bool `json:"metricsAPIEnabled"`
HealthAPIEnabled bool `json:"healthAPIEnabled"`
}
type IPConfig struct {
PublicIP string `json:"publicIP"`
PublicIPResolutionService string `json:"publicIPResolutionService"`
PublicIPResolutionFreq time.Duration `json:"publicIPResolutionFreq"`
// The host portion of the address to listen on. The port to
// listen on will be sourced from IPPort.
//
// - If empty, listen on all interfaces (both ipv4 and ipv6).
// - If populated, listen only on the specified address.
ListenHost string `json:"listenHost"`
ListenPort uint16 `json:"listenPort"`
}
type StakingConfig struct {
genesis.StakingConfig
SybilProtectionEnabled bool `json:"sybilProtectionEnabled"`
PartialSyncPrimaryNetwork bool `json:"partialSyncPrimaryNetwork"`
StakingTLSCert tls.Certificate `json:"-"`
StakingSigningKey *bls.SecretKey `json:"-"`
SybilProtectionDisabledWeight uint64 `json:"sybilProtectionDisabledWeight"`
StakingKeyPath string `json:"stakingKeyPath"`
StakingCertPath string `json:"stakingCertPath"`
StakingSignerPath string `json:"stakingSignerPath"`
}
type StateSyncConfig struct {
StateSyncIDs []ids.NodeID `json:"stateSyncIDs"`
StateSyncIPs []netip.AddrPort `json:"stateSyncIPs"`
}
type BootstrapConfig struct {
// Timeout before emitting a warn log when connecting to bootstrapping beacons
BootstrapBeaconConnectionTimeout time.Duration `json:"bootstrapBeaconConnectionTimeout"`
// Max number of containers in an ancestors message sent by this node.
BootstrapAncestorsMaxContainersSent int `json:"bootstrapAncestorsMaxContainersSent"`
// This node will only consider the first [AncestorsMaxContainersReceived]
// containers in an ancestors message it receives.
BootstrapAncestorsMaxContainersReceived int `json:"bootstrapAncestorsMaxContainersReceived"`
// Max time to spend fetching a container and its
// ancestors while responding to a GetAncestors message
BootstrapMaxTimeGetAncestors time.Duration `json:"bootstrapMaxTimeGetAncestors"`
Bootstrappers []genesis.Bootstrapper `json:"bootstrappers"`
// Skip bootstrapping and start processing immediately
SkipBootstrap bool `json:"skipBootstrap"`
// Enable automining in POA mode
EnableAutomining bool `json:"enableAutomining"`
}
type DatabaseConfig struct {
// If true, all writes are to memory and are discarded at node shutdown.
ReadOnly bool `json:"readOnly"`
// Path to database
Path string `json:"path"`
// Name of the database type to use
Name string `json:"name"`
// Path to config file
Config []byte `json:"-"`
}
// Config contains all of the configurations of a Lux node.
type Config struct {
HTTPConfig `json:"httpConfig"`
IPConfig `json:"ipConfig"`
StakingConfig `json:"stakingConfig"`
fee.StaticConfig `json:"txFeeConfig"`
StateSyncConfig `json:"stateSyncConfig"`
BootstrapConfig `json:"bootstrapConfig"`
DatabaseConfig `json:"databaseConfig"`
UpgradeConfig upgrade.Config `json:"upgradeConfig"`
// Genesis information
GenesisBytes []byte `json:"-"`
UTXOAssetID ids.ID `json:"utxoAssetID"`
// ID of the network this node should connect to
NetworkID uint32 `json:"networkID"`
// Health
HealthCheckFreq time.Duration `json:"healthCheckFreq"`
// Network configuration
NetworkConfig network.Config `json:"networkConfig"`
AdaptiveTimeoutConfig timer.AdaptiveTimeoutConfig `json:"adaptiveTimeoutConfig"`
BenchlistConfig benchlist.Config `json:"benchlistConfig"`
ProfilerConfig profiler.Config `json:"profilerConfig"`
// LoggingConfig log.Config `json:"loggingConfig"` // log.Config doesn't exist
PluginDir string `json:"pluginDir"`
// DevMode enables local PoA + auto-mine mode (network-id=local, sybil-protection disabled)
DevMode bool `json:"devMode"`
// File Descriptor Limit
FdLimit uint64 `json:"fdLimit"`
// Metrics
MeterVMEnabled bool `json:"meterVMEnabled"`
// Delay between automatic block proposals when DevMode is set
DevBlockDelay time.Duration `json:"devBlockDelay"`
RouterHealthConfig HealthConfig `json:"routerHealthConfig"`
ConsensusShutdownTimeout time.Duration `json:"consensusShutdownTimeout"`
// Poll for new frontiers every [FrontierPollFrequency]
FrontierPollFrequency time.Duration `json:"consensusGossipFreq"`
// ConsensusAppConcurrency defines the maximum number of goroutines to
// handle App messages per chain.
ConsensusAppConcurrency int `json:"consensusAppConcurrency"`
TrackedChains set.Set[ids.ID] `json:"trackedChains"`
TrackAllChains bool `json:"trackAllChains"`
NetConfigs map[ids.ID]nets.Config `json:"chainConfigs"`
ChainConfigs map[string]chains.ChainConfig `json:"-"`
ChainAliases map[ids.ID][]string `json:"chainAliases"`
VMAliases map[ids.ID][]string `json:"vmAliases"`
// Halflife to use for the processing requests tracker.
// Larger halflife --> usage metrics change more slowly.
SystemTrackerProcessingHalflife time.Duration `json:"systemTrackerProcessingHalflife"`
// Frequency to check the real resource usage of tracked processes.
// More frequent checks --> usage metrics are more accurate, but more
// expensive to track
SystemTrackerFrequency time.Duration `json:"systemTrackerFrequency"`
// Halflife to use for the cpu tracker.
// Larger halflife --> cpu usage metrics change more slowly.
SystemTrackerCPUHalflife time.Duration `json:"systemTrackerCPUHalflife"`
// Halflife to use for the disk tracker.
// Larger halflife --> disk usage metrics change more slowly.
SystemTrackerDiskHalflife time.Duration `json:"systemTrackerDiskHalflife"`
CPUTargeterConfig TargeterConfig `json:"cpuTargeterConfig"`
DiskTargeterConfig TargeterConfig `json:"diskTargeterConfig"`
RequiredAvailableDiskSpace uint64 `json:"requiredAvailableDiskSpace"`
WarningThresholdAvailableDiskSpace uint64 `json:"warningThresholdAvailableDiskSpace"`
TraceConfig trace.Config `json:"traceConfig"`
// See comment on [UseCurrentHeight] in platformvm.Config
UseCurrentHeight bool `json:"useCurrentHeight"`
// ProvidedFlags contains all the flags set by the user
ProvidedFlags map[string]interface{} `json:"-"`
// ChainDataDir is the root path for per-chain directories where VMs can
// write arbitrary data.
ChainDataDir string `json:"chainDataDir"`
// ImportChainData is the path to import blockchain data from another chain
ImportChainData string `json:"importChainData"`
// Path to write process context to (including PID, API URI, and
// staking address).
ProcessContextFilePath string `json:"processContextFilePath"`
// POA Mode Configuration
POAModeEnabled bool `json:"poaModeEnabled"`
POASingleNodeMode bool `json:"poaSingleNodeMode"`
POAMinBlockTime time.Duration `json:"poaMinBlockTime"`
POAAuthorizedNodes []string `json:"poaAuthorizedNodes"`
// Low Memory Configuration
LowMemoryEnabled bool `json:"lowMemoryEnabled"`
DBCacheSize uint64 `json:"dbCacheSize"`
DBMemtableSize uint64 `json:"dbMemtableSize"`
StateCacheSize uint64 `json:"stateCacheSize"`
BlockCacheSize uint64 `json:"blockCacheSize"`
DisableBloomFilters bool `json:"disableBloomFilters"`
LazyChainLoading bool `json:"lazyChainLoading"`
SingleValidatorMode bool `json:"singleValidatorMode"`
// Logging
Log log.Logger `json:"-"`
}
+5 -9
View File
@@ -1354,10 +1354,6 @@ func (n *Node) initChainManager(utxoAssetID ids.ID) error {
SybilProtectionEnabled: n.Config.SybilProtectionEnabled,
StakingTLSSigner: n.StakingTLSSigner,
StakingTLSCert: n.StakingTLSCert,
StakingMLDSASigner: n.Config.StakingConfig.StakingMLDSA,
StakingMLDSAPub: n.Config.StakingConfig.StakingMLDSAPub,
ProposerWindowDuration: n.Config.ProposerWindowDuration,
ProposerMinBlockDelay: n.Config.ProposerMinBlockDelay,
StakingBLSKey: n.Config.StakingSigningKey,
Log: n.Log,
LogFactory: n.LogFactory,
@@ -1829,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 /v1/security with methods securityProfile and
// - JSON-RPC: POST /ext/security with methods securityProfile and
// blockSecurity (dispatched on the wire as security_securityProfile
// / security_blockSecurity per gorilla/rpc namespace convention)
// - REST: GET /v1/security/profile
// - REST: GET /v1/security/block/{n}
// - REST: GET /ext/security/profile
// - REST: GET /ext/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 /v1/metrics carries the profile
// node-wide metrics gatherer here so /ext/metrics carries the profile
// posture immediately after boot.
//
// Closes F102 follow-ups (securityProfile RPC + profile metrics).
@@ -1848,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 /v1/metrics carries them alongside the
// node-wide gatherer so /ext/metrics carries them alongside the
// existing process / api / chain metric families.
securityMetricsReg, err := metric.MakeAndRegister(
n.MetricsGatherer,
+11 -24
View File
@@ -114,29 +114,17 @@ var CoreVMs = map[ids.ID]CoreVM{
// today's opt-in-flag behavior) while the gate itself remains fail-closed.
//
// C (evm) and the remaining app VMs are plugin-loaded but ungated today.
//
// Declaring a VM here is a statement of intent, not a guarantee that a binary
// exists: the registry scan simply finds nothing and the chain cannot start.
// fhevm (F-Chain) is exactly that case today — see its entry below.
var OptionalVMs = map[ids.ID]PluginSpec{
constants.DexVMID: {Name: "dexvm", RequiredNFT: &NFTRequirement{Collection: "dex-operator", GroupID: 0}},
constants.BridgeVMID: {Name: "bridgevm", RequiredNFT: &NFTRequirement{Collection: "bridge-operator", GroupID: 0}},
constants.EVMID: {Name: "evm"},
constants.AIVMID: {Name: "aivm"},
constants.GraphVMID: {Name: "graphvm"},
constants.IdentityVMID: {Name: "identityvm"},
constants.KeyVMID: {Name: "keyvm"},
constants.OracleVMID: {Name: "oraclevm"},
constants.RelayVMID: {Name: "relayvm"},
constants.MPCVMID: {Name: "mpcvm"},
// F-Chain. Reserved ID, no shipping VM: luxfi/chains has no fhevm/
// directory, so `make` produces no fhevm binary and this scan always comes
// up empty. The FHE runtime library lives at luxfi/chains/mpcvm/fhe as a
// package inside mpcvm, not as a standalone chain. F-Chain is a spec
// (LP-8200, LP-167). Kept declared so the intent stays visible and the ID
// stays reserved — remove it only when F-Chain is cancelled, not merely
// because it is unbuilt.
constants.FHEVMID: {Name: "fhevm"},
constants.DexVMID: {Name: "dexvm", RequiredNFT: &NFTRequirement{Collection: "dex-operator", GroupID: 0}},
constants.BridgeVMID: {Name: "bridgevm", RequiredNFT: &NFTRequirement{Collection: "bridge-operator", GroupID: 0}},
constants.EVMID: {Name: "evm"},
constants.AIVMID: {Name: "aivm"},
constants.GraphVMID: {Name: "graphvm"},
constants.IdentityVMID: {Name: "identityvm"},
constants.KeyVMID: {Name: "keyvm"},
constants.OracleVMID: {Name: "oraclevm"},
constants.RelayVMID: {Name: "relayvm"},
constants.ThresholdVMID: {Name: "thresholdvm"},
}
func init() {
@@ -166,8 +154,7 @@ func assertRegistriesDisjoint() error {
// registerCoreVMs registers the in-process core VMs that carry a concrete
// factory (Q, Z). P and X are registered separately in node.go because their
// factories require live node dependencies (RegisteredInNodeGo=true in
// CoreVMs). The optional chain VMs (A/B/C/D/F/G/I/K/M/O/R — the keys of
// OptionalVMs; there is no T, LP-134 dissolved it) are NEVER registered
// CoreVMs). The optional chain VMs (A/B/C/D/G/I/K/O/R/T) are NEVER registered
// here — they load from PluginDir via the VMRegistry scan, exactly like any
// other plugin. Registering an optional VM in-process would shadow its plugin
// (the registry skips any VMID the manager already has — vms/registry/
+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/mpcvm",
"github.com/luxfi/chains/thresholdvm",
}
for _, pkg := range []string{"./node/", "./main"} {
deps := goListDeps(t, pkg)
+22
View File
@@ -0,0 +1,22 @@
FROM bufbuild/buf:1.26.1 AS builder
FROM ubuntu:20.04
RUN apt-get update && apt -y install bash curl unzip git
WORKDIR /opt
RUN \
curl -L https://golang.org/dl/go1.24.5.linux-amd64.tar.gz > golang.tar.gz && \
mkdir golang && \
tar -zxvf golang.tar.gz -C golang/
ENV PATH="${PATH}:/opt/golang/go/bin"
COPY --from=builder /usr/local/bin/buf /usr/local/bin/
# any version changes here should also be bumped in scripts/protobuf_codegen.sh
RUN \
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.30.0 && \
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.3.0
ENV PATH="${PATH}:/root/go/bin/"
+36
View File
@@ -0,0 +1,36 @@
# Lux gRPC
Now Serving: **Protocol Version 42**
Protobuf files are hosted at
[https://buf.build/luxfi/lux](https://buf.build/luxfi/lux) and
can be used as dependencies in other projects.
Protobuf linting and generation for this project is managed by
[buf](https://github.com/bufbuild/buf).
Please find installation instructions on
[https://docs.buf.build/installation/](https://docs.buf.build/installation/).
Any changes made to proto definition can be updated by running
`protobuf_codegen.sh` located in the `scripts/` directory of Lux Node.
`buf` Quickstart
[https://buf.build/docs/cli/quickstart](https://buf.build/docs/cli/quickstart)
## Protocol Version Compatibility
The protobuf definitions and generated code are versioned based on the
[RPCChainVMProtocol](../version/version.go#L13) defined for the RPCChainVM.
Many versions of a Lux client can use the same
[RPCChainVMProtocol](../version/version.go#L13). But each Lux client and
chain VM must use the same protocol version to be compatible.
## Publishing to Buf Schema Registry
- Checkout appropriate tag in Lux Node `git checkout v1.10.1`
- Change to proto/ directory `cd proto`.
- Publish new tag to buf registry. `buf push -t v26`
Note: Publishing requires auth to the luxfi org in buf
https://buf.build/luxfi/repositories
+8
View File
@@ -0,0 +1,8 @@
version: v1
plugins:
- name: go
out: pb
opt: paths=source_relative
- name: go-grpc
out: pb
opt: paths=source_relative
+36
View File
@@ -0,0 +1,36 @@
# Lux gRPC
Now Serving: **Protocol Version 42**
Protobuf files are hosted at
[https://buf.build/luxfi/lux](https://buf.build/luxfi/lux) and
can be used as dependencies in other projects.
Protobuf linting and generation for this project is managed by
[buf](https://github.com/bufbuild/buf).
Please find installation instructions on
[https://docs.buf.build/installation/](https://docs.buf.build/installation/).
Any changes made to proto definition can be updated by running
`protobuf_codegen.sh` located in the `scripts/` directory of Lux Node.
`buf` Quickstart
[https://buf.build/docs/cli/quickstart](https://buf.build/docs/cli/quickstart)
## Protocol Version Compatibility
The protobuf definitions and generated code are versioned based on the
[RPCChainVMProtocol](../version/version.go#L13) defined for the RPCChainVM.
Many versions of a Lux client can use the same
[RPCChainVMProtocol](../version/version.go#L13). But each Lux client and
chain VM must use the same protocol version to be compatible.
## Publishing to Buf Schema Registry
- Checkout appropriate tag in Lux Node `git checkout v1.10.1`
- Change to proto/ directory `cd proto`.
- Publish new tag to buf registry. `buf push -t v26`
Note: Publishing requires auth to the luxfi org in buf
https://buf.build/luxfi/repositories
+30
View File
@@ -0,0 +1,30 @@
version: v1
name: buf.build/luxfi/lux
build:
excludes: []
breaking:
use:
- FILE
# deps removed - now using local io/metric/client/metrics.proto
lint:
use:
- STANDARD
except:
- SERVICE_SUFFIX # service requirement of <name>+Service
- RPC_REQUEST_STANDARD_NAME # explicit <rpc>+Request naming
- RPC_RESPONSE_STANDARD_NAME # explicit <rpc>+Response naming
- PACKAGE_VERSION_SUFFIX # versioned naming <service>.v1beta
ignore:
# TODO: how will fixing this affect functionality. Multiple fields are used as the request
# or response type for multiple RPCs
- aliasreader/aliasreader.proto
- net/conn/conn.proto
# Third-party proto from prometheus/client_model - don't lint
- io/metric/client/metrics.proto
# allows RPC requests or responses to be google.protobuf.Empty messages. This can be set if you
# want to allow messages to be void forever, that is they will never take any parameters.
rpc_allow_google_protobuf_empty_requests: true
rpc_allow_google_protobuf_empty_responses: true
# allows the same message type to be used for a single RPC's request and response type.
# TODO: this should not be tolerated and if it is only perscriptivly.
rpc_allow_same_request_response: true
+25
View File
@@ -0,0 +1,25 @@
#!/bin/bash
# Rename types, methods, and functions to remove "App" prefix
# Targeted at p2p message types and builder functions
# 1. Rename Message Types and Fields (Getters)
find . -name "*.go" -type f -print0 | xargs -0 sed -i '' \
-e 's/AppRequest/Request/g' \
-e 's/AppResponse/Response/g' \
-e 's/AppGossip/Gossip/g' \
-e 's/AppError/Error/g' \
-e 's/GetAppRequest/GetRequest/g' \
-e 's/GetAppResponse/GetResponse/g' \
-e 's/GetAppGossip/GetGossip/g' \
-e 's/GetAppError/GetError/g'
# 2. Rename Builder Functions (Inbound/Outbound)
# Note: s/AppRequest/Request/g above already handled the suffix.
# Now we need to handle prefixes if they still exist.
# e.g. InboundAppRequest -> InboundRequest (if AppRequest matched first, it became InboundRequest)
# Check: InboundAppRequest -> InboundRequest.
# So IsInboundAppRequest -> IsInboundRequest?
# We should just ensure "InboundApp" -> "Inbound" (for any other patterns)
find . -name "*.go" -type f -print0 | xargs -0 sed -i '' \
-e 's/InboundApp/Inbound/g' \
-e 's/OutboundApp/Outbound/g'
+30
View File
@@ -0,0 +1,30 @@
#!/bin/bash
# Replace package declarations in moved files
# Ensure we are fixing package names for moved files
# vm/manager was 'package manager', so it fits node/vms/manager
# vm/rpc was 'package rpc' (likely), needs to be 'package rpcchainvm'
if [ -d "vms/manager" ]; then
sed -i '' 's/^package.*/package manager/' vms/manager/*.go
fi
if [ -d "vms/rpcchainvm" ]; then
sed -i '' 's/^package.*/package rpcchainvm/' vms/rpcchainvm/*.go
fi
# Global replacements
DIRS=". ../precompile ../coreth ../evm ../rpc ../wallet ../staking"
for dir in $DIRS; do
if [ -d "$dir" ]; then
echo "Processing $dir..."
find "$dir" -name "*.go" -type f -print0 | xargs -0 sed -i '' \
-e 's|github.com/luxfi/consensus/runtime|github.com/luxfi/runtime|g' \
-e 's|github.com/luxfi/consensus/validator|github.com/luxfi/validators|g' \
-e 's|github.com/luxfi/consensus/version|github.com/luxfi/version|g' \
-e 's|github.com/luxfi/vm/manager|github.com/luxfi/node/vms/manager|g' \
-e 's|github.com/luxfi/vm/rpc|github.com/luxfi/node/vms/rpcchainvm|g' \
-e 's|github.com/luxfi/vm/chains|github.com/luxfi/node/chains|g' \
-e 's|version\.Current()|version.CurrentApp|g' \
-e 's|consensusversion\.Current()|version.CurrentApp|g'
fi
done
+5 -13
View File
@@ -54,16 +54,8 @@ source "${REPO_ROOT}"/scripts/constants.sh
# Determine the git commit hash to use for the build
source "${REPO_ROOT}"/scripts/git_commit.sh
# Configure build based on profile.
#
# `metrics` is a base tag, not a profile choice: without it luxfi/metric's
# NewRegistry() resolves to the no-op registry (registry_noop.go, //go:build
# !metrics), every metric registers into a black hole, and /v1/metrics answers
# 200 with a zero-byte body — while --api-metrics-enabled=true still reports
# metrics as on. Whether metrics are served is the runtime flag's decision
# alone; the build must not silently overrule it.
base_tags="metrics"
tags="${base_tags}"
# Configure build based on profile
tags=""
ldflags="-X github.com/luxfi/node/version.GitCommit=$git_commit \
-X github.com/luxfi/node/version.VersionMajor=$version_major \
-X github.com/luxfi/node/version.VersionMinor=$version_minor \
@@ -75,7 +67,7 @@ upx_compress=false
case "${profile}" in
minimal)
echo "Profile: minimal (ZAP, all VMs, NAT, stripped)"
tags="${base_tags},nattraversal"
tags="nattraversal"
strip_flags="-s -w"
;;
core)
@@ -85,7 +77,7 @@ case "${profile}" in
;;
full)
echo "Profile: full (gRPC+ZAP, all VMs, all features, stripped)"
tags="${base_tags},grpc,nattraversal,zxcvbn"
tags="grpc,nattraversal,zxcvbn,metrics"
strip_flags="-s -w"
;;
dev)
@@ -94,7 +86,7 @@ case "${profile}" in
;;
tiny)
echo "Profile: tiny (all VMs, NAT + UPX compressed)"
tags="${base_tags},nattraversal"
tags="nattraversal"
strip_flags="-s -w"
upx_compress=true
;;
+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/v1/info" >/dev/null 2>&1; then
if curl -s "http://127.0.0.1:$HTTP_PORT/ext/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/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 " 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 ""
echo " Logs: tail -f $LOG_FILE"
echo " Stop: kill $(cat "$PID_FILE")"
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
set -euo pipefail
if ! [[ "$0" =~ scripts/protobuf_codegen.sh ]]; then
echo "must be run from repository root"
exit 255
fi
# the versions here should match those of the binaries installed in the nix dev shell
## ensure the correct version of "buf" is installed
BUF_VERSION='1.47.2'
if [[ $(buf --version | cut -f2 -d' ') != "${BUF_VERSION}" ]]; then
echo "could not find buf ${BUF_VERSION}, is it installed + in PATH?"
exit 255
fi
## ensure the correct version of "protoc-gen-go" is installed
PROTOC_GEN_GO_VERSION='v1.35.1'
if [[ $(protoc-gen-go --version | cut -f2 -d' ') != "${PROTOC_GEN_GO_VERSION}" ]]; then
echo "could not find protoc-gen-go ${PROTOC_GEN_GO_VERSION}, is it installed + in PATH?"
exit 255
fi
## ensure the correct version of "protoc-gen-go-grpc" is installed
PROTOC_GEN_GO_GRPC_VERSION='1.3.0'
if [[ $(protoc-gen-go-grpc --version | cut -f2 -d' ') != "${PROTOC_GEN_GO_GRPC_VERSION}" ]]; then
echo "could not find protoc-gen-go-grpc ${PROTOC_GEN_GO_GRPC_VERSION}, is it installed + in PATH?"
exit 255
fi
BUF_MODULES=("proto" "connectproto")
REPO_ROOT=$PWD
for BUF_MODULE in "${BUF_MODULES[@]}"; do
TARGET=$REPO_ROOT/$BUF_MODULE
if [ -n "${1:-}" ]; then
TARGET="$1"
fi
# move to buf module directory
cd "$TARGET"
echo "Generating for buf module $BUF_MODULE"
echo "Running protobuf fmt for..."
buf format -w
echo "Running protobuf lint check..."
if ! buf lint; then
echo "ERROR: protobuf linter failed"
exit 1
fi
echo "Re-generating protobuf..."
if ! buf generate; then
echo "ERROR: protobuf generation failed"
exit 1
fi
done
+1 -1
View File
@@ -60,7 +60,7 @@ PLUGINS=(
r5m1ujrmXxVcQetG3CQfuDLHp2RHKh6vCDaFgBRQfUcTZh7eS # oraclevm
ry9Sg8rZdT26iEKvJDmC2wkESs4SDKgZEhk5BgLSwg1EpcNug # quantumvm
sP6dLqrrBR9w3soP18fbJ3YzZecZdD7DDdfH2cFhhLq7Hy9bz # relayvm
qCURact1n41FcoNBch8iMVBwc9AWie48D118ZNJ5tBdWrvryS # mpcvm
tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t # thresholdvm
vv3qPfyTVXZ5ArRZA9Jh4hbYDTBe43f7sgQg4CHfNg1rnnvX9 # zkvm
)
+1 -1
View File
@@ -101,7 +101,7 @@ global:
scrape_configs:
- job_name: "node"
metrics_path: "/v1/metrics"
metrics_path: "/ext/metrics"
file_sd_configs:
- files:
- '${FILE_SD_PATH}/*.json'

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