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
461 changed files with 21624 additions and 16201 deletions
+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
-10
View File
@@ -5,16 +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.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
+62 -42
View File
@@ -257,15 +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.
ARG EVM_VERSION=v1.104.9
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
@@ -289,7 +281,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
# byte-identity; DEX big.Rat + determinism). Pin chains v1.4.8 (warp
# consolidated to one luxfi/warp helper; graphvm genesis-last-accepted fix)
# to match the chain-VM plugin stage (CHAINS_REF) below.
go mod edit -require=github.com/luxfi/chains@v1.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=0 GOFLAGS=-mod=mod \
@@ -310,16 +302,16 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
# oraclevm -> r5m1ujrmXxVcQetG3CQfuDLHp2RHKh6vCDaFgBRQfUcTZh7eS
# quantumvm -> ry9Sg8rZdT26iEKvJDmC2wkESs4SDKgZEhk5BgLSwg1EpcNug
# relayvm -> sP6dLqrrBR9w3soP18fbJ3YzZecZdD7DDdfH2cFhhLq7Hy9bz
# mpcvm -> tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t
# 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' {} +
@@ -353,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/tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t ./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 \
tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t \
vv3qPfyTVXZ5ArRZA9Jh4hbYDTBe43f7sgQg4CHfNg1rnnvX9 ; do \
test -s /luxd/build/plugins/$p \
|| { echo "FATAL: required chain-VM plugin $p missing/empty — its build failed above (see the matching WARN line); the runtime-stage hard COPY would otherwise fail cryptically. Surface & fix the real Go build error, or remove the plugin from BOTH the build list and the runtime COPY."; exit 1; } ; \
done && \
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 ================
@@ -383,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);
@@ -399,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 && \
@@ -448,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.
+1 -1
View File
@@ -52,7 +52,7 @@ Target: validate all consensus, EVM, and staking behavior with K=11 validators.
- [ ] Verify all precompile activation timestamps are after 2025-12-25
- [ ] Deploy via PaaS (platform.hanzo.ai), not manual kubectl
- [ ] Verify all 11 pods reach Running state
- [ ] Verify all 11 nodes report healthy via `/v1/health/liveness`
- [ ] Verify all 11 nodes report healthy via `/ext/health/liveness`
### 1.2 Bootstrap and Connectivity
-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 -133
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,101 +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.
## Essential Commands
### Release & build (canonical) — via platform.hanzo.ai, NOT GitHub Actions
@@ -348,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)
@@ -438,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
@@ -757,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
@@ -777,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"}
```
@@ -786,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`
@@ -853,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"
```
@@ -887,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
```
+1 -1
View File
@@ -212,7 +212,7 @@ run-testnet: build-fips init-chains
node-status:
@echo "$(GREEN)Checking node status...$(NC)"
@curl -s -X POST --data '{"jsonrpc":"2.0","id":1,"method":"info.isBootstrapped","params":{}}' \
-H 'content-type:application/json;' http://localhost:9630/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}}
+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
-24
View File
@@ -46,30 +46,6 @@ func (s *Nets) GetOrCreate(chainID ids.ID) (nets.Net, bool) {
return chain, true
}
// IsChainBootstrapped reports whether the given chain has finished initial sync
// (reached the network frontier and transitioned its VM to normal operation) on
// this node — Bootstrapped(chainID) was called for it in its validation net. A
// chain that is merely tracked (its sync goroutine launched) but has NOT converged
// reads false. This is the per-chain truth manager.IsBootstrapped / info.isBootstrapped
// key on, replacing the mere-existence test that returned true the instant a chain
// was added to the manager (the premature-true masking bug: a C-Chain stalled at
// genesis reported bootstrapped=true). A chainID is added to exactly one net's
// tracking, so the first net that reports it bootstrapped is authoritative.
func (s *Nets) IsChainBootstrapped(chainID ids.ID) bool {
if s == nil {
return false // no net tracking wired ⇒ nothing has been marked bootstrapped
}
s.lock.RLock()
defer s.lock.RUnlock()
for _, chain := range s.chains {
if chain.IsChainBootstrapped(chainID) {
return true
}
}
return false
}
// Bootstrapping returns the chainIDs of any chains that are still
// bootstrapping.
func (s *Nets) Bootstrapping() []ids.ID {
-139
View File
@@ -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))
}
}
+56 -295
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"
@@ -102,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"
@@ -171,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
@@ -244,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
}
@@ -364,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
@@ -779,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.
@@ -864,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 {
@@ -885,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)
@@ -931,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)
}
@@ -964,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))
@@ -1131,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")
}
@@ -1207,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
@@ -1244,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
})
@@ -1292,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
@@ -1384,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",
@@ -1417,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",
@@ -1561,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 {
@@ -1572,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
@@ -1636,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 {
@@ -1905,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
@@ -2073,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 {
@@ -2184,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 }
@@ -2356,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]
@@ -2374,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 {
@@ -2384,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
@@ -2692,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{}
@@ -2875,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).
@@ -2986,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
@@ -3254,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()))
}
@@ -3382,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
@@ -3437,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
@@ -3480,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
@@ -3619,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
}
@@ -4340,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
+1 -1
View File
@@ -150,7 +150,7 @@ func (r *ChainHandlerRegistrar) ValidateEndpoint(
}
// Build the full URL
fullURL := fmt.Sprintf("/v1/%s%s", info.Base, endpoint)
fullURL := fmt.Sprintf("/ext/%s%s", info.Base, endpoint)
r.log.Info("Validating endpoint",
log.Stringer("chainID", chainID),
+7 -7
View File
@@ -68,20 +68,20 @@ func (d *DebugTool) DiagnoseEndpoint(chainID ids.ID, alias string) *DiagnosticRe
// getURLPatterns returns all possible URL patterns to test.
func (d *DebugTool) getURLPatterns(chainID ids.ID, alias string) []string {
patterns := []string{
fmt.Sprintf("%s/v1/bc/%s/rpc", d.baseURL, chainID.String()),
fmt.Sprintf("%s/v1/bc/%s/ws", d.baseURL, chainID.String()),
fmt.Sprintf("%s/v1/bc/%s", d.baseURL, chainID.String()),
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/v1/bc/%s/rpc", d.baseURL, alias),
fmt.Sprintf("%s/v1/bc/%s/ws", d.baseURL, alias),
fmt.Sprintf("%s/v1/bc/%s", d.baseURL, alias),
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 /v1 prefix (some setups might differ)
// Also test without /ext prefix (some setups might differ)
patterns = append(patterns,
fmt.Sprintf("%s/bc/%s/rpc", d.baseURL, chainID.String()),
)
+2 -2
View File
@@ -111,7 +111,7 @@ func (m *HandlerManager) RegisterChainHandlers(
log.Err(err))
} else {
m.log.Info("Handler registered successfully",
log.String("route", fmt.Sprintf("/v1/%s%s", base, endpoint)),
log.String("route", fmt.Sprintf("/ext/%s%s", base, endpoint)),
log.Stringer("chainID", chainID))
}
}
@@ -187,7 +187,7 @@ func (m *HandlerManager) getFullRoutes(bases []string, endpoints []string) []str
routes := []string{}
for _, base := range bases {
for _, endpoint := range endpoints {
routes = append(routes, fmt.Sprintf("/v1/%s%s", base, endpoint))
routes = append(routes, fmt.Sprintf("/ext/%s%s", base, endpoint))
}
}
return routes
+2 -2
View File
@@ -80,9 +80,9 @@ func IntegrationExample(
fmt.Printf("\n✅ Chain %s RPC endpoints ready:\n", chainID)
for _, endpoint := range info.Endpoints {
if info.ChainAlias != "" {
fmt.Printf(" %s/v1/bc/%s%s\n", baseURL, info.ChainAlias, endpoint)
fmt.Printf(" %s/ext/bc/%s%s\n", baseURL, info.ChainAlias, endpoint)
}
fmt.Printf(" %s/v1/bc/%s%s\n", baseURL, chainID, endpoint)
fmt.Printf(" %s/ext/bc/%s%s\n", baseURL, chainID, endpoint)
}
fmt.Println()
}
+1 -1
View File
@@ -19,7 +19,7 @@ services:
LUXD_CONSENSUS_QUORUM_SIZE: "1"
LUXD_LOG_LEVEL: "info"
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:9630/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)
+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)
}
+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)
}
}
}
+60 -52
View File
@@ -26,14 +26,14 @@ require (
github.com/huin/goupnp v1.3.0
github.com/jackpal/gateway v1.1.1
github.com/jackpal/go-nat-pmp v1.0.2
github.com/luxfi/consensus v1.36.7
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
@@ -118,37 +118,36 @@ 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.7
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.2
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/keys v1.4.1
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.1
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/zap-proto/http v0.0.0-20260506200741-fd6047874433
go.uber.org/zap v1.27.1
@@ -177,8 +176,8 @@ require (
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.18 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect
github.com/aws/smithy-go v1.24.2 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.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
@@ -190,21 +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/corona v0.10.4 // indirect
github.com/luxfi/age v1.5.0 // indirect
github.com/luxfi/bft v0.1.5 // 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/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/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
@@ -214,13 +216,12 @@ require (
github.com/rs/xid v1.6.0 // indirect
github.com/tinylib/msgp v1.6.4 // indirect
go.mongodb.org/mongo-driver v1.17.9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
)
require (
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
)
@@ -241,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,3 +262,10 @@ require (
)
exclude github.com/ethereum/go-ethereum v1.10.26
// TEMPORARY local-dev build aid for the bootstrap frozen-cache convergence fix.
// FINAL CASCADE (publish step, NOT done here): tag consensus v1.25.36 (the uncommitted
// engine/chain/integration.go FinalizedLedger + FinalizedBlockAtHeight accessors and the
// engine/chain/bootstrap HasAccepted change), bump the require above v1.25.35 v1.25.36,
// then DELETE this replace. The zap client (option b) is node-only and does NOT widen the
// consensus bump.
+107 -110
View File
@@ -67,8 +67,8 @@ github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6
github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg=
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
github.com/btcsuite/btcd/btcec/v2 v2.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=
@@ -76,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=
@@ -297,144 +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/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.7 h1:LOOF3hxI4/hGCHMOMyKkThphU67GNCEXe3ouPGkNX2Q=
github.com/luxfi/chains v1.7.7/go.mod h1:wsUtdcLtnbnS7AJcCpTpa0TcZ2vylvvDko+QI98mHKU=
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.7 h1:ijJh/4sl1Y65ziUFsQJa2DLwnAhb1h/c72FHybEXKps=
github.com/luxfi/consensus v1.36.7/go.mod h1:m4aMFbKYxwM2X4oPYRjq4LmDSShfCgAHnhmxIGKDyFU=
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/bft v0.1.5 h1:5xVLPkog4e5LTgaVlb9pgxA0EWE6tkrKwHPZVRz+RZw=
github.com/luxfi/bft v0.1.5/go.mod h1:5I8Ft8yA69xZlDe3RB0i4MgbqFKLZe65o/sha8JuKvU=
github.com/luxfi/cache v1.2.1 h1:kAzOS55/hmYeNKR+0HAKv4ma48Y6JjkI8UQeqdZ8bfI=
github.com/luxfi/cache v1.2.1/go.mod h1:co7JTxZZHpKT31Yh01LFp5aZOxmoUg157FhBLQdQHVU=
github.com/luxfi/chains v1.4.8 h1:i5QxDfGR922oPGYrBbUo2Qn3tFMpJD9BilNTLFaQscE=
github.com/luxfi/chains v1.4.8/go.mod h1:F/jT9YbC8/yD4WxJu4AvRFbi4NyKY+FHBWrE8M08dgE=
github.com/luxfi/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/dex v1.14.0 h1:4PtorVbRmbD73S6YWPvgp5GRCb9jeuaedl7mo1CbzCI=
github.com/luxfi/dex v1.14.0/go.mod h1:wYWQmwospvdKBWQ6OJMXb8I+kfgEtE46R1rD8H7vHCQ=
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.2 h1:OLQg5+ln8qgORBYnJuQsZxar8AfZlsXg5g/f95AraPI=
github.com/luxfi/genesis v1.16.2/go.mod h1:piaSqJY80eVpgov8DUWQXll9IdlCroWgvhnwC7/3lTA=
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/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/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:rLCbygaajehVkUoJ5czwhpUAJaC5J5okq3p+j2QSPSo=
github.com/luxfi/vm v1.3.1/go.mod h1:uViH3COP8hhCbj42v1MTohkPCDwYQCZanNIOb/StWqY=
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=
@@ -726,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
-24
View File
@@ -34,15 +34,6 @@ type Net interface {
// IsBootstrapped returns true if the chains in this chain are done bootstrapping
IsBootstrapped() bool
// IsChainBootstrapped reports whether a SPECIFIC chain in this net has finished
// initial sync — i.e. Bootstrapped(chainID) was called for it (the chain reached
// the network frontier and its VM went to normal operation). This is the per-chain
// truth that info.isBootstrapped keys on, distinct from IsBootstrapped() which is
// the net-wide "no chain still bootstrapping" aggregate. A chain that is merely
// tracked (added, sync goroutine launched) but has NOT converged is still in the
// bootstrapping set and reads false here — closing the premature-true masking bug.
IsChainBootstrapped(chainID ids.ID) bool
// Bootstrapped marks the chain as done bootstrapping
Bootstrapped(chainID ids.ID)
@@ -75,21 +66,6 @@ func (s *chain) IsBootstrapped() bool {
return s.bootstrapping.Len() == 0
}
// IsChainBootstrapped assumes MONOTONIC per-process bootstrapped state: a chain
// only ever moves bootstrapping→bootstrapped (Bootstrapped is forward-only and
// AddChain refuses to re-add a chain already in either set), so this signal — and
// the Bootstrapping() health check that shares the set — never report stale-true.
// INVARIANT: if a future path ever moves a live chain BACK to bootstrapping (e.g.
// SetState(Bootstrapping) on a running chain) it MUST remove the chainID from
// bootstrapped, or both this signal and the readiness health check will report a
// re-syncing chain as still bootstrapped.
func (s *chain) IsChainBootstrapped(chainID ids.ID) bool {
s.lock.RLock()
defer s.lock.RUnlock()
return s.bootstrapped.Contains(chainID)
}
func (s *chain) Bootstrapped(chainID ids.ID) {
s.lock.Lock()
defer s.lock.Unlock()
+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 -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,
+1 -2
View File
@@ -124,8 +124,7 @@ var OptionalVMs = map[ids.ID]PluginSpec{
constants.KeyVMID: {Name: "keyvm"},
constants.OracleVMID: {Name: "oraclevm"},
constants.RelayVMID: {Name: "relayvm"},
constants.MPCVMID: {Name: "mpcvm"},
constants.FHEVMID: {Name: "fhevm"},
constants.ThresholdVMID: {Name: "thresholdvm"},
}
func init() {
+1 -1
View File
@@ -58,7 +58,7 @@ func TestOptionalVMsNotLinkedInProcess(t *testing.T) {
"github.com/luxfi/chains/keyvm",
"github.com/luxfi/chains/oraclevm",
"github.com/luxfi/chains/relayvm",
"github.com/luxfi/chains/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
+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
tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t # 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'
+11 -11
View File
@@ -147,9 +147,9 @@ func (r *router) handleRootGET(w http.ResponseWriter, _ *http.Request) {
P string `json:"p"`
X string `json:"x"`
}{
C: baseURL + "/bc/C/rpc",
P: baseURL + "/bc/P",
X: baseURL + "/bc/X",
C: "/ext/bc/C/rpc",
P: "/ext/bc/P",
X: "/ext/bc/X",
},
Endpoints: struct {
RPC string `json:"rpc"`
@@ -157,10 +157,10 @@ func (r *router) handleRootGET(w http.ResponseWriter, _ *http.Request) {
Info string `json:"info"`
Health string `json:"health"`
}{
RPC: baseURL + "/bc/C/rpc",
Websocket: baseURL + "/bc/C/ws",
Info: baseURL + "/info",
Health: baseURL + "/health",
RPC: "/ext/bc/C/rpc",
Websocket: "/ext/bc/C/ws",
Info: "/ext/info",
Health: "/ext/health",
},
}
}
@@ -173,10 +173,10 @@ func (r *router) handleRootGET(w http.ResponseWriter, _ *http.Request) {
// handleRootPOST proxies JSON-RPC requests to the C-chain
func (r *router) handleRootPOST(w http.ResponseWriter, req *http.Request) {
// Look up the C-chain RPC handler
handler, err := r.GetHandler(baseURL+"/bc/C", "/rpc")
handler, err := r.GetHandler("/ext/bc/C", "/rpc")
if err != nil {
// Try alternate path formats
handler, err = r.GetHandler(baseURL+"/bc/C/rpc", "")
handler, err = r.GetHandler("/ext/bc/C/rpc", "")
if err != nil {
// Return proper JSON-RPC error
w.Header().Set("Content-Type", "application/json")
@@ -198,10 +198,10 @@ func (r *router) SetRootInfoProvider(provider RootInfoProvider) {
}
// handleHealthz returns a minimal health response for K8s probes.
// This delegates to the full /v1/health handler when available,
// This delegates to the full /ext/health handler when available,
// falling back to a static 200 response during early startup.
func (r *router) handleHealthz(w http.ResponseWriter, req *http.Request) {
if handler, err := r.GetHandler(baseURL+"/health", "/health"); err == nil {
if handler, err := r.GetHandler("/ext/health", "/health"); err == nil {
handler.ServeHTTP(w, req)
return
}
+2 -7
View File
@@ -25,12 +25,7 @@ import (
)
const (
// baseURL is the canonical — and only — prefix for every luxd HTTP route
// (/v1/bc/C/rpc, /v1/info, /v1/health, ...). Single source of truth:
// AddRoute/AddAliases and the root/health helpers in router.go all derive
// their paths from it. The legacy Avalanche-heritage /ext prefix is gone;
// one way, no backward compatibility (activation Dec 25 2025).
baseURL = "/v1"
baseURL = "/ext"
maxConcurrentStreams = 64
)
@@ -100,7 +95,7 @@ type server struct {
listener net.Listener
// handler is the fully-wrapped API handler chain (CORS + host-filter +
// /v1/* router). Held here so the optional ZAP-RPC listener serves the
// /ext/* router). Held here so the optional ZAP-RPC listener serves the
// exact same handler as the HTTP listener.
handler http.Handler
+1 -1
View File
@@ -2,7 +2,7 @@
// See the file LICENSE for licensing terms.
// ZAP-RPC listener — serves the EXACT same fully-wrapped API handler chain
// (CORS + host-filter + /v1/* router) as the HTTP listener, but over the
// (CORS + host-filter + /ext/* router) as the HTTP listener, but over the
// github.com/zap-proto/http binary protocol. This is what makes luxd a
// first-class citizen of the ZAP service mesh: the api.<brand> gateway can
// proxy to luxd over native ZAP instead of HTTP/1.1.
+2 -2
View File
@@ -60,7 +60,7 @@ func TestStartZapRPCListener_RoundTrip(t *testing.T) {
const body = `{"jsonrpc":"2.0","result":"0x2a","id":1}`
mux := http.NewServeMux()
mux.HandleFunc("/v1/bc/C/rpc", func(w http.ResponseWriter, r *http.Request) {
mux.HandleFunc("/ext/bc/C/rpc", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, body)
})
@@ -75,7 +75,7 @@ func TestStartZapRPCListener_RoundTrip(t *testing.T) {
time.Sleep(150 * time.Millisecond)
client := &http.Client{Transport: zaphttp.NewTransport(addr)}
resp, err := client.Post("http://"+addr+"/v1/bc/C/rpc", "application/json", nil)
resp, err := client.Post("http://"+addr+"/ext/bc/C/rpc", "application/json", nil)
if err != nil {
t.Fatalf("ZAP round-trip POST failed: %v", err)
}
+12 -12
View File
@@ -143,7 +143,7 @@ func TestRevokeToken(t *testing.T) {
auth := NewFromHash(log.NewNoOpLogger(), "auth", hashedPassword).(*auth)
// Make a token
endpoints := []string{"/v1/info", "/v1/bc/X", "/v1/metrics"}
endpoints := []string{"/ext/info", "/ext/bc/X", "/ext/metrics"}
tokenStr, err := auth.NewToken(testPassword, defaultTokenLifespan, endpoints)
require.NoError(err)
@@ -157,7 +157,7 @@ func TestWrapHandlerHappyPath(t *testing.T) {
auth := NewFromHash(log.NewNoOpLogger(), "auth", hashedPassword)
// Make a token
endpoints := []string{"/v1/info", "/v1/bc/X", "/v1/metrics"}
endpoints := []string{"/ext/info", "/ext/bc/X", "/ext/metrics"}
tokenStr, err := auth.NewToken(testPassword, defaultTokenLifespan, endpoints)
require.NoError(err)
@@ -178,7 +178,7 @@ func TestWrapHandlerRevokedToken(t *testing.T) {
auth := NewFromHash(log.NewNoOpLogger(), "auth", hashedPassword)
// Make a token
endpoints := []string{"/v1/info", "/v1/bc/X", "/v1/metrics"}
endpoints := []string{"/ext/info", "/ext/bc/X", "/ext/metrics"}
tokenStr, err := auth.NewToken(testPassword, defaultTokenLifespan, endpoints)
require.NoError(err)
@@ -205,7 +205,7 @@ func TestWrapHandlerExpiredToken(t *testing.T) {
auth.clock.Set(time.Now().Add(-2 * defaultTokenLifespan))
// Make a token that expired well in the past
endpoints := []string{"/v1/info", "/v1/bc/X", "/v1/metrics"}
endpoints := []string{"/ext/info", "/ext/bc/X", "/ext/metrics"}
tokenStr, err := auth.NewToken(testPassword, defaultTokenLifespan, endpoints)
require.NoError(err)
@@ -227,7 +227,7 @@ func TestWrapHandlerNoAuthToken(t *testing.T) {
auth := NewFromHash(log.NewNoOpLogger(), "auth", hashedPassword)
endpoints := []string{"/v1/info", "/v1/bc/X", "/v1/metrics"}
endpoints := []string{"/ext/info", "/ext/bc/X", "/ext/metrics"}
wrappedHandler := auth.WrapHandler(dummyHandler)
for _, endpoint := range endpoints {
req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://127.0.0.1:9630%s", endpoint), strings.NewReader(""))
@@ -245,11 +245,11 @@ func TestWrapHandlerUnauthorizedEndpoint(t *testing.T) {
auth := NewFromHash(log.NewNoOpLogger(), "auth", hashedPassword)
// Make a token
endpoints := []string{"/v1/info"}
endpoints := []string{"/ext/info"}
tokenStr, err := auth.NewToken(testPassword, defaultTokenLifespan, endpoints)
require.NoError(err)
unauthorizedEndpoints := []string{"/v1/bc/X", "/v1/metrics", "", "/foo", "/v1/info/foo"}
unauthorizedEndpoints := []string{"/ext/bc/X", "/ext/metrics", "", "/foo", "/ext/info/foo"}
wrappedHandler := auth.WrapHandler(dummyHandler)
for _, endpoint := range unauthorizedEndpoints {
@@ -269,12 +269,12 @@ func TestWrapHandlerAuthEndpoint(t *testing.T) {
auth := NewFromHash(log.NewNoOpLogger(), "auth", hashedPassword)
// Make a token
endpoints := []string{"/v1/info", "/v1/bc/X", "/v1/metrics", "", "/foo", "/v1/info/foo"}
endpoints := []string{"/ext/info", "/ext/bc/X", "/ext/metrics", "", "/foo", "/ext/info/foo"}
tokenStr, err := auth.NewToken(testPassword, defaultTokenLifespan, endpoints)
require.NoError(err)
wrappedHandler := auth.WrapHandler(dummyHandler)
req := httptest.NewRequest(http.MethodPost, "http://127.0.0.1:9630/v1/auth", strings.NewReader(""))
req := httptest.NewRequest(http.MethodPost, "http://127.0.0.1:9630/ext/auth", strings.NewReader(""))
req.Header.Add("Authorization", headerValStart+tokenStr)
rr := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rr, req)
@@ -287,7 +287,7 @@ func TestWrapHandlerAccessAll(t *testing.T) {
auth := NewFromHash(log.NewNoOpLogger(), "auth", hashedPassword)
// Make a token that allows access to all endpoints
endpoints := []string{"/v1/info", "/v1/bc/X", "/v1/metrics", "", "/foo", "/v1/foo/info"}
endpoints := []string{"/ext/info", "/ext/bc/X", "/ext/metrics", "", "/foo", "/ext/foo/info"}
tokenStr, err := auth.NewToken(testPassword, defaultTokenLifespan, []string{"*"})
require.NoError(err)
@@ -316,7 +316,7 @@ func TestWrapHandlerMutatedRevokedToken(t *testing.T) {
auth := NewFromHash(log.NewNoOpLogger(), "auth", hashedPassword)
// Make a token
endpoints := []string{"/v1/info", "/v1/bc/X", "/v1/metrics"}
endpoints := []string{"/ext/info", "/ext/bc/X", "/ext/metrics"}
tokenStr, err := auth.NewToken(testPassword, defaultTokenLifespan, endpoints)
require.NoError(err)
@@ -339,7 +339,7 @@ func TestWrapHandlerInvalidSigningMethod(t *testing.T) {
auth := NewFromHash(log.NewNoOpLogger(), "auth", hashedPassword).(*auth)
// Make a token
endpoints := []string{"/v1/info", "/v1/bc/X", "/v1/metrics"}
endpoints := []string{"/ext/info", "/ext/bc/X", "/ext/metrics"}
idBytes := [tokenIDByteLen]byte{}
_, err := rand.Read(idBytes[:])
require.NoError(err)
+1 -1
View File
@@ -22,7 +22,7 @@ type Password struct {
type NewTokenArgs struct {
Password
// Endpoints that may be accessed with this token e.g. if endpoints is
// ["/v1/bc/X", "/v1/admin"] then the token holder can hit the X-Chain API
// ["/ext/bc/X", "/ext/admin"] then the token holder can hit the X-Chain API
// and the admin API. If [Endpoints] contains an element "*" then the token
// allows access to all API endpoints. [Endpoints] must have between 1 and
// [maxEndpoints] elements
+9 -9
View File
@@ -23,7 +23,7 @@ To get an HTTP status code response that indicates the node's health, make a `GE
To filter GET health checks, add a `tag` query parameter to the request. The `tag` parameter is a string. For example, to filter health results by netID `29uVeLPJB1eQJkzRemU8g8wZDw5uJRqpab5U2mX9euieVwiEbL`, use the following query:
```sh
curl 'http://localhost:9630/v1/health?tag=29uVeLPJB1eQJkzRemU8g8wZDw5uJRqpab5U2mX9euieVwiEbL'
curl 'http://localhost:9630/ext/health?tag=29uVeLPJB1eQJkzRemU8g8wZDw5uJRqpab5U2mX9euieVwiEbL'
```
In this example returned results will contain global health checks and health checks that are related to netID `29uVeLPJB1eQJkzRemU8g8wZDw5uJRqpab5U2mX9euieVwiEbL`.
@@ -33,7 +33,7 @@ In this example returned results will contain global health checks and health ch
In order to filter results by multiple tags, use multiple `tag` query parameters. For example, to filter health results by netID `29uVeLPJB1eQJkzRemU8g8wZDw5uJRqpab5U2mX9euieVwiEbL` and `28nrH5T2BMvNrWecFcV3mfccjs6axM1TVyqe79MCv2Mhs8kxiY` use the following query:
```sh
curl 'http://localhost:9630/v1/health?tag=29uVeLPJB1eQJkzRemU8g8wZDw5uJRqpab5U2mX9euieVwiEbL&tag=28nrH5T2BMvNrWecFcV3mfccjs6axM1TVyqe79MCv2Mhs8kxiY'
curl 'http://localhost:9630/ext/health?tag=29uVeLPJB1eQJkzRemU8g8wZDw5uJRqpab5U2mX9euieVwiEbL&tag=28nrH5T2BMvNrWecFcV3mfccjs6axM1TVyqe79MCv2Mhs8kxiY'
```
The returned results will include health checks for both netIDs as well as global health checks.
@@ -42,10 +42,10 @@ The returned results will include health checks for both netIDs as well as globa
The available endpoints for GET requests are:
- `/v1/health` returns a holistic report of the status of the node. **Most operators should monitor this status.**
- `/v1/health/health` is the same as `/v1/health`.
- `/v1/health/readiness` returns healthy once the node has finished initializing.
- `/v1/health/liveness` returns healthy once the endpoint is available.
- `/ext/health` returns a holistic report of the status of the node. **Most operators should monitor this status.**
- `/ext/health/health` is the same as `/ext/health`.
- `/ext/health/readiness` returns healthy once the node has finished initializing.
- `/ext/health/liveness` returns healthy once the endpoint is available.
## JSON RPC Request
@@ -71,7 +71,7 @@ curl -H 'Content-Type: application/json' --data '{
"params": {
"tags": ["11111111111111111111111111111111LpoYY", "29uVeLPJB1eQJkzRemU8g8wZDw5uJRqpab5U2mX9euieVwiEbL"]
}
}' 'http://localhost:9630/v1/health'
}' 'http://localhost:9630/ext/health'
```
**Example Response**:
@@ -203,7 +203,7 @@ curl -H 'Content-Type: application/json' --data '{
"params": {
"tags": ["11111111111111111111111111111111LpoYY", "29uVeLPJB1eQJkzRemU8g8wZDw5uJRqpab5U2mX9euieVwiEbL"]
}
}' 'http://localhost:9630/v1/health'
}' 'http://localhost:9630/ext/health'
```
**Example Response**:
@@ -249,7 +249,7 @@ curl -H 'Content-Type: application/json' --data '{
"jsonrpc":"2.0",
"id" :1,
"method" :"health.liveness"
}' 'http://localhost:9630/v1/health'
}' 'http://localhost:9630/ext/health'
```
**Example Response**:
+15 -15
View File
@@ -7,7 +7,7 @@ This API uses the `json 2.0` RPC format. For more information on making JSON RPC
## Endpoint
```
/v1/info
/ext/info
```
## Methods
@@ -38,7 +38,7 @@ curl -sX POST --data '{
"id" :1,
"method" :"info.lps",
"params" :{}
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/info
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/info
```
**Example Response**:
@@ -125,7 +125,7 @@ curl -X POST --data '{
"params": {
"chain":"X"
}
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/info
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/info
```
**Example Response**:
@@ -160,7 +160,7 @@ curl -X POST --data '{
"params": {
"alias":"X"
}
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/info
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/info
```
**Example Response**:
@@ -192,7 +192,7 @@ curl -X POST --data '{
"jsonrpc":"2.0",
"id" :1,
"method" :"info.getNetworkID"
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/info
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/info
```
**Example Response**:
@@ -226,7 +226,7 @@ curl -X POST --data '{
"jsonrpc":"2.0",
"id" :1,
"method" :"info.getNetworkName"
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/info
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/info
```
**Example Response**:
@@ -273,7 +273,7 @@ curl -X POST --data '{
"jsonrpc":"2.0",
"id" :1,
"method" :"info.getNodeID"
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/info
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/info
```
**Example Response**:
@@ -313,7 +313,7 @@ curl -X POST --data '{
"jsonrpc":"2.0",
"id" :1,
"method" :"info.getNodeIP"
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/info
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/info
```
**Example Response**:
@@ -359,7 +359,7 @@ curl -X POST --data '{
"jsonrpc":"2.0",
"id" :1,
"method" :"info.getNodeVersion"
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/info
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/info
```
**Example Response**:
@@ -426,7 +426,7 @@ curl -X POST --data '{
"jsonrpc":"2.0",
"id" :1,
"method" :"info.getTxFee"
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/info
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/info
```
**Example Response**:
@@ -473,7 +473,7 @@ curl -X POST --data '{
"id" :1,
"method" :"info.getVMs",
"params" :{}
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/info
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/info
```
**Example Response**:
@@ -540,7 +540,7 @@ curl -X POST --data '{
"params": {
"nodeIDs": []
}
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/info
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/info
```
**Example Response**:
@@ -619,7 +619,7 @@ curl -X POST --data '{
"jsonrpc":"2.0",
"id" :1,
"method" :"info.uptime"
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/info
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/info
```
**Example Response**:
@@ -645,7 +645,7 @@ curl -X POST --data '{
"params" :{
"netID":"29uVeLPJB1eQJkzRemU8g8wZDw5uJRqpab5U2mX9euieVwiEbL"
}
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/info
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/info
```
#### Example Lux L1 Response
@@ -672,7 +672,7 @@ curl -X POST --data '{
"jsonrpc":"2.0",
"id" :1,
"method" :"info.upgrades"
}' -H 'content-type:application/json;' 127.0.0.1:9650/v1/info
}' -H 'content-type:application/json;' 127.0.0.1:9650/ext/info
```
**Example Response**:
+4 -4
View File
@@ -64,7 +64,7 @@ func TestGetNodeVersionConsensusRoundtrip(t *testing.T) {
log: log.NewNoOpLogger(),
}
req := httptest.NewRequest("POST", "/v1/info", nil)
req := httptest.NewRequest("POST", "/ext/info", nil)
reply := apiinfo.GetNodeVersionReply{}
require.NoError(info.GetNodeVersion(req, nil, &reply))
require.NotNil(reply.Consensus)
@@ -114,7 +114,7 @@ func TestGetVMsSuccess(t *testing.T) {
id2: []string{alias2},
}
req := httptest.NewRequest("POST", "/v1/info", nil)
req := httptest.NewRequest("POST", "/ext/info", nil)
resources.mockVMManager.EXPECT().ListFactories(req.Context()).Times(1).Return(vmIDs, nil)
resources.mockVMManager.EXPECT().PrimaryAlias(req.Context(), id1).Times(1).Return(alias1, nil)
resources.mockVMManager.EXPECT().PrimaryAlias(req.Context(), id2).Times(1).Return(alias2, nil)
@@ -128,7 +128,7 @@ func TestGetVMsSuccess(t *testing.T) {
func TestGetVMsVMsListFactoriesFails(t *testing.T) {
resources := initGetVMsTest(t)
req := httptest.NewRequest("POST", "/v1/info", nil)
req := httptest.NewRequest("POST", "/ext/info", nil)
resources.mockVMManager.EXPECT().ListFactories(req.Context()).Times(1).Return(nil, errTest)
reply := apiinfo.GetVMsReply{}
@@ -146,7 +146,7 @@ func TestGetVMsGetAliasesFails(t *testing.T) {
vmIDs := []ids.ID{id1, id2}
alias1 := "vm1-alias-1"
req := httptest.NewRequest("POST", "/v1/info", nil)
req := httptest.NewRequest("POST", "/ext/info", nil)
resources.mockVMManager.EXPECT().ListFactories(req.Context()).Times(1).Return(vmIDs, nil)
resources.mockVMManager.EXPECT().PrimaryAlias(req.Context(), id1).Times(1).Return(alias1, nil)
resources.mockVMManager.EXPECT().PrimaryAlias(req.Context(), id2).Times(1).Return("", errTest)
+1 -1
View File
@@ -38,7 +38,7 @@ type client struct {
// instead.
func NewClient(uri string) Client {
return &client{requester: rpc.NewEndpointRequester(
uri + "/v1/keystore",
uri + "/ext/keystore",
)}
}
+6 -6
View File
@@ -48,7 +48,7 @@ This API uses the `json 2.0` API format. For more information on making JSON RPC
## Endpoint
```text
/v1/keystore
/ext/keystore
```
## Methods
@@ -89,7 +89,7 @@ curl -X POST --data '{
"username":"myUsername",
"password":"myPassword"
}
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/keystore
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/keystore
```
**Example Response:**
@@ -129,7 +129,7 @@ curl -X POST --data '{
"username":"myUsername",
"password":"myPassword"
}
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/keystore
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/keystore
```
**Example Response:**
@@ -183,7 +183,7 @@ curl -X POST --data '{
"username":"myUsername",
"password":"myPassword"
}
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/keystore
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/keystore
```
**Example Response:**
@@ -238,7 +238,7 @@ curl -X POST --data '{
"password":"myPassword",
"user" :"0x7655a29df6fc2747b0874e1148b423b954a25fcdb1f170d0ec8eb196430f7001942ce55b02a83b1faf50a674b1e55bfc000000008cf2d869"
}
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/keystore
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/keystore
```
**Example Response:**
@@ -274,7 +274,7 @@ curl -X POST --data '{
"jsonrpc":"2.0",
"id" :1,
"method" :"keystore.listUsers"
}' -H 'content-type:application/json;' 127.0.0.1:9630/v1/keystore
}' -H 'content-type:application/json;' 127.0.0.1:9630/ext/keystore
```
**Example Response:**
+1 -1
View File
@@ -32,7 +32,7 @@ type Client struct {
// NewClient returns a new Metrics API Client
func NewClient(uri string) *Client {
return &Client{
uri: uri + "/v1/metrics",
uri: uri + "/ext/metrics",
}
}
+2 -2
View File
@@ -7,7 +7,7 @@ This API set is for a specific node, it is unavailable on the [public server](ht
## Endpoint
```
/v1/metrics
/ext/metrics
```
## Usage
@@ -15,7 +15,7 @@ This API set is for a specific node, it is unavailable on the [public server](ht
To get the node metrics:
```sh
curl -X POST 127.0.0.1:9630/v1/metrics
curl -X POST 127.0.0.1:9630/ext/metrics
```
## Format
+9 -9
View File
@@ -27,7 +27,7 @@ var ErrNoProfile = errors.New(
"(genesis carries no SecurityProfile{} block); RPC unavailable")
// Service is the JSON-RPC handler set registered under the security
// namespace at /v1/security. Methods are read-only; the underlying
// namespace at /ext/security. Methods are read-only; the underlying
// profile pointer is set once at construction and never mutated.
//
// Exposed methods:
@@ -37,7 +37,7 @@ var ErrNoProfile = errors.New(
//
// On the wire, gorilla/rpc dispatches these as security_securityProfile
// and security_blockSecurity (namespace_method). Callers using the REST
// sidecars hit /v1/security/profile and /v1/security/block/{n}
// sidecars hit /ext/security/profile and /ext/security/block/{n}
// directly. One namespace, two transports, one shape.
//
// The "block" method returns the chain-wide envelope; per-block
@@ -53,13 +53,13 @@ type Service struct {
// namespace. Profile may be nil — see ErrNoProfile.
//
// The returned handler is suitable for APIServer.AddRoute(handler,
// "security", "") so it lands at /v1/security on the node's HTTP
// "security", "") so it lands at /ext/security on the node's HTTP
// listener. REST sidecars are mounted at /profile and /block/{n} on
// the same handler so the full external surface is:
//
// POST /v1/security (JSON-RPC, methods above)
// GET /v1/security/profile (REST sidecar)
// GET /v1/security/block/{n} (REST sidecar)
// POST /ext/security (JSON-RPC, methods above)
// GET /ext/security/profile (REST sidecar)
// GET /ext/security/block/{n} (REST sidecar)
func NewHandler(logger log.Logger, profile *consensusconfig.ChainSecurityProfile) (http.Handler, error) {
server := rpc.NewServer()
codec := avajson.NewCodec()
@@ -71,7 +71,7 @@ func NewHandler(logger log.Logger, profile *consensusconfig.ChainSecurityProfile
}
// REST sidecars share the Service receiver so the two transports
// stay byte-identical in semantics (one and only one way to
// compute the shape). Paths relative to /v1/security:
// compute the shape). Paths relative to /ext/security:
// /profile → restProfile
// /block/{n} → restBlockSecurity
mux := http.NewServeMux()
@@ -133,7 +133,7 @@ func (s *Service) BlockSecurity(_ *http.Request, _ *BlockSecurityArgs, reply *Bl
return nil
}
// restProfile is the GET /profile sidecar (full path /v1/security/profile).
// restProfile is the GET /profile sidecar (full path /ext/security/profile).
// Same body as the securityProfile JSON-RPC method; useful for
// explorers that don't speak JSON-RPC. Refuses every non-GET method so
// callers can't smuggle state through the read-only endpoint.
@@ -151,7 +151,7 @@ func (s *Service) restProfile(w http.ResponseWriter, r *http.Request) {
}
// restBlockSecurity is the GET /block/{n} sidecar (full path
// /v1/security/block/{n}). The path suffix is taken as the block
// /ext/security/block/{n}). The path suffix is taken as the block
// number; chain alias defaults to the platform chain (callers can
// re-route per-chain at the chain-manager layer if needed).
func (s *Service) restBlockSecurity(w http.ResponseWriter, r *http.Request) {
+2 -2
View File
@@ -203,7 +203,7 @@ func TestRPC_blockSecurity_StrictPQ(t *testing.T) {
}
// TestREST_securityProfile_GET proves the /profile sidecar (full path
// /v1/security/profile when mounted on APIServer) returns the same
// /ext/security/profile when mounted on APIServer) returns the same
// JSON shape as the JSON-RPC handler. One shape, two transports — no
// per-transport drift.
func TestREST_securityProfile_GET(t *testing.T) {
@@ -262,7 +262,7 @@ func TestREST_securityProfile_MethodNotAllowed(t *testing.T) {
}
// TestREST_blockSecurity_GET proves the /block/{n} sidecar (full path
// /v1/security/block/{n} when mounted on APIServer) returns the same
// /ext/security/block/{n} when mounted on APIServer) returns the same
// JSON shape as the JSON-RPC handler. One shape, two transports — no
// per-transport drift.
func TestREST_blockSecurity_GET(t *testing.T) {

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