mirror of
https://github.com/luxfi/node.git
synced 2026-07-29 08:36:26 +00:00
Compare commits
176
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0165a92f3 | ||
|
|
a33eec4872 | ||
|
|
0a394ef5be | ||
|
|
99ae438be4 | ||
|
|
bd2edc135f | ||
|
|
bdd7179164 | ||
|
|
50fe597bf7 | ||
|
|
8873e231e8 | ||
|
|
2a724fb090 | ||
|
|
23d93bf60b | ||
|
|
8a797f4805 | ||
|
|
9f4819cf77 | ||
|
|
84a08c2b9c | ||
|
|
011e9bf99d | ||
|
|
c39a85522d | ||
|
|
1c92026c7e | ||
|
|
f7e501d021 | ||
|
|
a57251c318 | ||
|
|
2f6ef061ab | ||
|
|
7d2f01eb0c | ||
|
|
c76fcda71d | ||
|
|
e1e3917772 | ||
|
|
e7137ed28d | ||
|
|
3398024a19 | ||
|
|
e77b696662 | ||
|
|
eb82872612 | ||
|
|
4fce5875c5 | ||
|
|
304717c199 | ||
|
|
ad76f2e709 | ||
|
|
26d419c3ad | ||
|
|
6c39b7d391 | ||
|
|
dada5a3169 | ||
|
|
3a44f0dcd3 | ||
|
|
d5eff75934 | ||
|
|
66d5940f88 | ||
|
|
e22009db91 | ||
|
|
011c3df6bd | ||
|
|
52de39485d | ||
|
|
e70f687c10 | ||
|
|
88bb914cd6 | ||
|
|
4a3a131757 | ||
|
|
45a3dcfff1 | ||
|
|
c3d6105804 | ||
|
|
d83191e286 | ||
|
|
13b6e80a78 | ||
|
|
57792ee625 | ||
|
|
cf8e4c6f51 | ||
|
|
6ca0508608 | ||
|
|
35cbdfb0ee | ||
|
|
e266345e69 | ||
|
|
45bbe13637 | ||
|
|
fe23efd1f2 | ||
|
|
b34dcd1558 | ||
|
|
375dadde31 | ||
|
|
4d712c3798 | ||
|
|
3f65eb486d | ||
|
|
4002a59aa6 | ||
|
|
97e0a0b704 | ||
|
|
24b67abdd0 | ||
|
|
7ee56895d9 | ||
|
|
bbce8e6e13 | ||
|
|
9930e98d26 | ||
|
|
64a770b4cc | ||
|
|
6b25706366 | ||
|
|
ddb3fbca93 | ||
|
|
b113618e5e | ||
|
|
3f890bc98e | ||
|
|
44d7651d16 | ||
|
|
dcde2167a8 | ||
|
|
5a2035adb5 | ||
|
|
b4992860f6 | ||
|
|
0ba5d05bd8 | ||
|
|
926ddaafa4 | ||
|
|
eda299d30d | ||
|
|
6a766516b1 | ||
|
|
2d227dd3aa | ||
|
|
a78aa07e6f | ||
|
|
dd04027476 | ||
|
|
28e34ba003 | ||
|
|
f2dcc91cd3 | ||
|
|
7123b7399e | ||
|
|
43cbbac511 | ||
|
|
7ea0c0383f | ||
|
|
6b599d29e1 | ||
|
|
c5e999487c | ||
|
|
9626ca6c2f | ||
|
|
87a57925f3 | ||
|
|
3b4d16bbc5 | ||
|
|
511a804016 | ||
|
|
c44cc2289f | ||
|
|
c73eba739f | ||
|
|
f1a3e85b8e | ||
|
|
d008eba286 | ||
|
|
b6143b5d53 | ||
|
|
20d36b36aa | ||
|
|
4610978bcc | ||
|
|
e869b7bcd1 | ||
|
|
bbba24bad6 | ||
|
|
34cb5c1684 | ||
|
|
e288b67008 | ||
|
|
f0a00167e9 | ||
|
|
33e005c869 | ||
|
|
949210730d | ||
|
|
8d2fb750b6 | ||
|
|
1bc07cb1ca | ||
|
|
1dede2e3d2 | ||
|
|
636944a63a | ||
|
|
9c2468671e | ||
|
|
7750ca1829 | ||
|
|
c526c0aaa4 | ||
|
|
7354ce84b9 | ||
|
|
8e7253a3c5 | ||
|
|
67ec31a344 | ||
|
|
77389092c2 | ||
|
|
13cca30a71 | ||
|
|
a93e5cdd9d | ||
|
|
ae3e3a45f5 | ||
|
|
83c1a83765 | ||
|
|
be18fce176 | ||
|
|
74be89840c | ||
|
|
9a0800065f | ||
|
|
4e71814e58 | ||
|
|
c13681108f | ||
|
|
a02611413e | ||
|
|
7720d00baa | ||
|
|
439f2768e0 | ||
|
|
2dc15620e4 | ||
|
|
2bbe067f60 | ||
|
|
ae15850e1a | ||
|
|
092ed0459e | ||
|
|
aa884628a0 | ||
|
|
3d65b1c0c2 | ||
|
|
a8971630b5 | ||
|
|
09ab817ee5 | ||
|
|
08465c2f63 | ||
|
|
ba14e55f2f | ||
|
|
d84b5397ed | ||
|
|
98911842db | ||
|
|
0f185bd813 | ||
|
|
a2f5c6c37b | ||
|
|
b3e92ce979 | ||
|
|
0428445890 | ||
|
|
86a342ae7b | ||
|
|
52fd2cef35 | ||
|
|
89bb0e718c | ||
|
|
6c6b256418 | ||
|
|
c7e0eef6f6 | ||
|
|
b4720c9f57 | ||
|
|
f840336167 | ||
|
|
d4323da799 | ||
|
|
97d9745ea9 | ||
|
|
6e7f0b3d7e | ||
|
|
6a87c69010 | ||
|
|
3db595a637 | ||
|
|
15ff32fbdc | ||
|
|
ec5cf94145 | ||
|
|
5e44cd67dc | ||
|
|
1b6487e0b7 | ||
|
|
67272bc7c9 | ||
|
|
2e98019144 | ||
|
|
6424671eb7 | ||
|
|
29c623880c | ||
|
|
ab4201301e | ||
|
|
29a263eb14 | ||
|
|
c3f037c737 | ||
|
|
d1400a4c65 | ||
|
|
bd996c41b1 | ||
|
|
feef2bcb1e | ||
|
|
99c690f307 | ||
|
|
00fa804fc0 | ||
|
|
65efff0628 | ||
|
|
5921a24dd9 | ||
|
|
7397da43ae | ||
|
|
a63a18bf71 | ||
|
|
363a5a8591 | ||
|
|
51a304804c |
@@ -1 +0,0 @@
|
||||
# CI Status Check - 2025-09-23 23:30:58
|
||||
@@ -1 +0,0 @@
|
||||
# Triggering CI - Version 1.13.5 Ready
|
||||
@@ -17,6 +17,6 @@ runs:
|
||||
- name: Set GOPRIVATE for luxfi packages
|
||||
# Some luxfi packages have large zip files that exceed Go proxy limits
|
||||
run: |
|
||||
echo "GOPRIVATE=github.com/luxfi/*" >> $GITHUB_ENV
|
||||
echo "GONOSUMDB=github.com/luxfi/*" >> $GITHUB_ENV
|
||||
echo "GOPRIVATE=github.com/lux-private/*" >> $GITHUB_ENV
|
||||
echo "GONOSUMDB=github.com/lux-private/*" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
@@ -27,8 +27,8 @@ runs:
|
||||
- name: Set GOPRIVATE and GONOSUMDB for luxfi packages
|
||||
shell: bash
|
||||
run: |
|
||||
echo "GOPRIVATE=github.com/luxfi/*" >> $GITHUB_ENV
|
||||
echo "GONOSUMDB=github.com/luxfi/*" >> $GITHUB_ENV
|
||||
echo "GOPRIVATE=github.com/lux-private/*" >> $GITHUB_ENV
|
||||
echo "GONOSUMDB=github.com/lux-private/*" >> $GITHUB_ENV
|
||||
- name: Configure git for private repo access
|
||||
if: inputs.github-token != ''
|
||||
shell: bash
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
name: Lint proto files
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'proto/**/*.proto'
|
||||
- '.github/workflows/buf-lint.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
buf-lint:
|
||||
runs-on: 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"
|
||||
@@ -9,7 +9,7 @@ on:
|
||||
|
||||
jobs:
|
||||
push:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: lux-build-amd64
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: bufbuild/buf-setup-action@v1.31.0
|
||||
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
TIMEOUT: ${{ env.TIMEOUT }}
|
||||
CGO_ENABLED: '0'
|
||||
Fuzz:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: lux-build-amd64
|
||||
env:
|
||||
GOPRIVATE: github.com/luxfi/*
|
||||
GONOSUMDB: github.com/luxfi/*
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
# e2e_pre_etna, e2e_post_etna, e2e_existing_network, Upgrade
|
||||
# These will be re-enabled once tmpnet is properly configured
|
||||
Lint:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: lux-build-amd64
|
||||
env:
|
||||
GOPRIVATE: github.com/luxfi/*
|
||||
GONOSUMDB: github.com/luxfi/*
|
||||
@@ -94,56 +94,9 @@ 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: ubuntu-latest
|
||||
runs-on: lux-build-amd64
|
||||
continue-on-error: true
|
||||
env:
|
||||
GOPRIVATE: github.com/luxfi/*
|
||||
@@ -163,7 +116,7 @@ jobs:
|
||||
run: .github/workflows/check-clean-branch.sh
|
||||
go_mod_tidy:
|
||||
name: Up-to-date go.mod and go.sum
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: lux-build-amd64
|
||||
env:
|
||||
GOPRIVATE: github.com/luxfi/*
|
||||
GONOSUMDB: github.com/luxfi/*
|
||||
@@ -180,7 +133,7 @@ jobs:
|
||||
run: .github/workflows/check-clean-branch.sh
|
||||
test_build_image:
|
||||
name: Image build
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: lux-build-amd64
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Docker Buildx
|
||||
|
||||
@@ -24,7 +24,7 @@ on:
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: lux-build-amd64
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
@@ -9,7 +9,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
fuzz:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: lux-build-amd64
|
||||
env:
|
||||
GOPRIVATE: github.com/luxfi/*
|
||||
GONOSUMDB: github.com/luxfi/*
|
||||
|
||||
@@ -11,7 +11,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
MerkleDB:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: lux-build-amd64
|
||||
env:
|
||||
GOPRIVATE: github.com/luxfi/*
|
||||
GONOSUMDB: github.com/luxfi/*
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: lux-build-amd64
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: crazy-max/ghaction-github-labeler@548a7c3603594ec17c819e1239f281a3b801ab4d #v6.0.0
|
||||
|
||||
@@ -4,7 +4,7 @@ on:
|
||||
- cron: '0 0 * * 0' # Run every day at midnight UTC on Sunday
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: lux-build-amd64
|
||||
steps:
|
||||
- uses: actions/stale@v10
|
||||
with:
|
||||
|
||||
@@ -15,7 +15,7 @@ env:
|
||||
|
||||
jobs:
|
||||
test-zapdb-replay:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: lux-build-amd64
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
--api-admin-enabled=true 2>&1 | grep -E "(genesis-db|Genesis)" || true
|
||||
|
||||
test-database-factory:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: lux-build-amd64
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.36.31]
|
||||
|
||||
### Fixed
|
||||
- **`/v1/health` reported a chain the node denies exists.** The `bootstrapped` check publishes `chains.Nets.Bootstrapping()` verbatim as its message, but `Nets.chains` is keyed by **net** ID and the aggregate appended the map **key**, not the chain. Every primary-network chain that failed to converge therefore surfaced as the single ID `11111111111111111111111111111111LpoYY` — `constants.PrimaryNetworkID` (`ids.Empty`) — so an operator resolving it got "there is no chain with alias/ID", and N stuck chains collapsed into one indistinguishable entry (measured on devnet/testnet: `"message":["11111111111111111111111111111111LpoYY"],"contiguousFailures":3213`). The net owns the bootstrapping set, so the net now names its own chains: new `nets.Net.Bootstrapping() []ids.ID` (`nets/net.go`), and `chains/chains.go` aggregates those instead of the keys. `TestNetsBootstrappingReportsChainsNotNets` asserts `ids.Empty` never appears and that two stuck chains are individually named; `TestNetsBootstrapping` no longer asserts the bug (it demanded the net ID).
|
||||
- Ships the GET `/v1/health` encoder fix from `dada5a31` (`{"healthy":…,"error":"health reply encode failed"}` on every node — jsonv2 has no representation for `apihealth.Result.Duration`), which was on `main` but had never been tagged.
|
||||
|
||||
## [1.36.13]
|
||||
|
||||
### Fixed
|
||||
- **The durable rejoin fix (#66/#74) shipped INERT for the C-Chain — the exact chain whose 40h mainnet freeze motivated it.** `chains/manager.go` gated `expectsStakedBeacons` on `ids.IsNativeChain(chainParams.ID)` — the *blockchain* ID. But `ids.IsNativeChain` only matches the symbolic `111…C` alias form, and every deployed C/X/Q carries a **hash** blockchain ID (devnet C `21HieZng…`, mainnet C `2wRdZG…`); only the P-chain has a symbolic blockchain ID (`111…P`), and it is excluded as the platform chain. So `isNativeChain` was **always false** for the real C-Chain → `expectsStakedBeacons=false` → under the production `--skip-bootstrap=true` the beacon set was emptied → a behind C-Chain named its STALE local tip the frontier and never caught up (verified on devnet v1.36.12: C-Chain wedged at height 0 with "using empty beacons for single-node mode"). Fix: discriminate on the **validating Net** (`chainParams.ChainID == constants.PrimaryNetworkID`) via new `chainValidatesOnPrimaryNetwork`, which is `PrimaryNetworkID` for C/X/Q and each sovereign L1's own net ID for an L2 — so C/X/Q now keep their staked beacons under `--skip-bootstrap` (peer-sync a behind validator) while L2s keep the empty-beacon single-node path. Regression test `TestChainValidatesOnPrimaryNetwork_RealHashChainID` exercises the real discriminator with hash chain IDs (the prior hardcoded-bool tests could not catch this); `TestRED_EmptyStakedSetFailsSafe` still passes (forged-frontier gate intact).
|
||||
|
||||
## [1.36.12]
|
||||
|
||||
### Fixed
|
||||
- **EVM chains (C/D/L2) failed to initialize on v1.36.11 — bundled VM plugins were built against a stale `luxfi/api`.** The node pins `luxfi/api v1.0.16`, whose `InitializeResponse` carries the appended `Capabilities uint64` field (Quasar-export handshake, api commit `1f2dc5a`). The image baked the C-Chain EVM plugin from `luxfi/evm@v1.104.8` and the D-Chain dexvm plugin from `luxfi/dex@v1.5.15`, both of which resolve `luxfi/api v1.0.15` (no `Capabilities`). Their `InitializeResponse.Encode` writes a shorter payload than the node's strict `InitializeResponse.Decode` expects, so `vms/rpcchainvm/zap/client.go` fails every EVM VM handshake with `zap decode initialize response: unexpected EOF`. Native VMs (P/X/Q) were unaffected. Fix: bump `EVM_VERSION` `v1.104.8 → v1.104.9` (api v1.0.16) and force `luxfi/api@v1.0.16` in the dexvm build stage. The api bump is code-free for plugins (`luxfi/chains v1.7.4 → v1.7.5` adopted it with a go.mod-only diff; `CHAINS_REF=v1.7.6` already carries v1.0.16). No node source change beyond the version bump — this is v1.36.11's node binary (durable rejoin fix, commit `63f61429d1`) rebuilt with plugin↔node ZAP-wire alignment.
|
||||
|
||||
## [1.28.0]
|
||||
|
||||
### Added
|
||||
|
||||
+78
-28
@@ -42,11 +42,11 @@ WORKDIR /build
|
||||
# deps not registered in the public sum.golang.org / proxy (reading e.g.
|
||||
# hanzoai/vfs@v0.4.1's go.mod via the public sumdb 404s and fails the build).
|
||||
ENV GONOSUMCHECK=github.com/luxfi/*,github.com/hanzoai/*
|
||||
ENV GONOSUMDB=github.com/luxfi/*,github.com/hanzoai/*
|
||||
ENV GONOSUMDB=github.com/lux-private/*,github.com/hanzoai/*
|
||||
# Use Go proxy for most deps (gonum.org is flaky via direct), direct only for
|
||||
# the cross-org private modules.
|
||||
ENV GOPROXY=https://proxy.golang.org,direct
|
||||
ENV GONOPROXY=github.com/luxfi/*,github.com/hanzoai/*
|
||||
ENV GONOPROXY=github.com/lux-private/*,github.com/hanzoai/*
|
||||
ENV GOFLAGS="-mod=mod"
|
||||
|
||||
# Copy and download lux dependencies using go mod.
|
||||
@@ -124,22 +124,27 @@ RUN --mount=type=secret,id=ghtok,required=false \
|
||||
# linked into the C-Chain plugin via github.com/luxfi/chains/evm/cevm
|
||||
# and become the default execution backend (parallel + GPU EVM).
|
||||
#
|
||||
# CI/RELEASE GAP: the luxcpp/cevm release artifacts MUST publish per-arch
|
||||
# tarballs at the URL below for both linux-x86_64 and linux-arm64. Until
|
||||
# those tarballs exist, builds with CGO_ENABLED=1 will fail at this step and
|
||||
# operators must build with CGO_ENABLED=0 (pure-Go fallback).
|
||||
# The release assets live in the PRIVATE lux-private/cevm repo, so the fetch is
|
||||
# authenticated with the same `ghtok` secret as the private go modules and
|
||||
# lux-accel above. Best-effort, like lux-accel: only a build that asks for the
|
||||
# cevm backend (EVM_CGO=1 below) actually needs these.
|
||||
ARG CGO_ENABLED=1
|
||||
ARG CEVM_VERSION=v0.19.0
|
||||
RUN if [ "${CGO_ENABLED}" = "1" ]; then \
|
||||
ARG CEVM_VERSION=v0.51.10
|
||||
ARG CEVM_REPO=lux-private/cevm
|
||||
RUN --mount=type=secret,id=ghtok,required=false \
|
||||
if [ "${CGO_ENABLED}" = "1" ]; then \
|
||||
ARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) && \
|
||||
if [ "$ARCH" = "amd64" ]; then CEVM_ARCH="linux-x86_64"; else CEVM_ARCH="linux-arm64"; fi && \
|
||||
wget -q "https://github.com/luxcpp/cevm/releases/download/${CEVM_VERSION}/luxcpp-cevm-${CEVM_ARCH}.tar.gz" \
|
||||
-O /tmp/cevm.tar.gz && \
|
||||
tar -xzf /tmp/cevm.tar.gz -C /usr/local && \
|
||||
rm /tmp/cevm.tar.gz && \
|
||||
ldconfig 2>/dev/null || true ; \
|
||||
AUTH=""; [ -s /run/secrets/ghtok ] && AUTH="--header=Authorization: Bearer $(cat /run/secrets/ghtok)"; \
|
||||
( wget -q ${AUTH:+"$AUTH"} \
|
||||
"https://github.com/${CEVM_REPO}/releases/download/${CEVM_VERSION}/luxcpp-cevm-${CEVM_ARCH}.tar.gz" \
|
||||
-O /tmp/cevm.tar.gz \
|
||||
&& tar -xzf /tmp/cevm.tar.gz -C /usr/local \
|
||||
&& rm /tmp/cevm.tar.gz \
|
||||
&& ldconfig 2>/dev/null \
|
||||
) || echo "WARN: cevm ${CEVM_VERSION} fetch skipped (unreachable; needed only when EVM_CGO=1)" ; \
|
||||
else \
|
||||
echo "CGO_ENABLED=0: skipping luxcpp/cevm fetch (pure-Go fallback build)" ; \
|
||||
echo "CGO_ENABLED=0: skipping cevm fetch (pure-Go fallback build)" ; \
|
||||
fi
|
||||
|
||||
# Build node. CGO_ENABLED=1 (default) links luxcpp/cevm for parallel + GPU EVM.
|
||||
@@ -257,7 +262,35 @@ RUN . ./build_env.sh && \
|
||||
# failed ValidateState, and BRICKED the node. Proven on-node: real swap → kill -9 →
|
||||
# clean reboot, state intact. v1.99.40 = v1.99.39 + deps to latest. consensus v1.25.21 =
|
||||
# stake-weighted alpha-of-K quorum finality + per-height single-finalize + epoch-bound certs.
|
||||
ARG EVM_VERSION=v1.101.2
|
||||
# v1.104.9 bumps luxfi/api v1.0.15 -> v1.0.16 (indirect via luxfi/vm). v1.0.16
|
||||
# APPENDED InitializeResponse.Capabilities (uint64) for the Quasar-export
|
||||
# handshake (api commit 1f2dc5a). The node pins api v1.0.16 and DECODES that
|
||||
# field; an EVM plugin built at v1.104.8 (api v1.0.15) omits it, so the node's
|
||||
# strict struct decode hits "zap decode initialize response: unexpected EOF"
|
||||
# and EVERY EVM chain (C + L2s hanzo/zoo/pars/spc, all mgj786) fails to
|
||||
# initialize. The api bump is code-free for plugins (chains v1.7.4->v1.7.5
|
||||
# adopted it with a go.mod-only diff), so v1.104.9 = v1.104.8 + api alignment.
|
||||
# C-Chain execution backend. The default is the pure-Go EVM, which is what
|
||||
# every image has shipped so far. EVM_CGO=1 EVM_TAGS=cevm links luxcpp/cevm
|
||||
# instead — that pair is what makes AutoEVM resolve to CppEVM. The libraries
|
||||
# come from the cevm fetch in this same stage above.
|
||||
ARG EVM_CGO=0
|
||||
ARG EVM_TAGS=""
|
||||
|
||||
# v1.104.22 realigns the plugin with THIS node's own go.mod: api v1.0.16 ->
|
||||
# v1.1.1, vm v1.2.6 -> v1.3.1, geth v1.17.12 -> v1.20.1. Node main pins api
|
||||
# v1.1.1, so pinning an evm at api v1.0.16 reintroduces the exact
|
||||
# InitializeResponse decode mismatch described above, just in the opposite
|
||||
# direction — keep this ARG and node's go.mod on the same api/vm/geth line.
|
||||
#
|
||||
# v1.104.14 is also the FIRST evm tag carrying the C-Chain fee-split seam
|
||||
# (core/fee_split.go creditTxFee + extras.FeeSplitTimestamp/FeeRewardVault).
|
||||
# Below it, encoding/json silently DISCARDS the genesis "feeSplitTimestamp"
|
||||
# key: a chain configured for the 50/50 split instead routes 100% of every fee
|
||||
# to the block coinbase and the reward vault 0x0100..0002 stays 0 forever, with
|
||||
# nothing burned. The split stays dormant wherever feeSplitTimestamp is absent
|
||||
# (mainnet), so this bump is behaviour-preserving there.
|
||||
ARG EVM_VERSION=v1.104.23
|
||||
ARG EVM_VM_ID=mgj786NP7uDwBCcq6YwThhaN8FLyybkCa4zBWTQbNgmK6k9A6
|
||||
# the pinned evm go.mod may pin a dead luxfi/upgrade pseudo-version
|
||||
# (v1.0.1-0.20260603055252-f51810805436 — commit pruned from origin). Heal it to
|
||||
@@ -281,11 +314,11 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
# byte-identity; DEX big.Rat + determinism). Pin chains v1.4.8 (warp
|
||||
# consolidated to one luxfi/warp helper; graphvm genesis-last-accepted fix)
|
||||
# to match the chain-VM plugin stage (CHAINS_REF) below.
|
||||
go mod edit -require=github.com/luxfi/chains@v1.4.8 && \
|
||||
go mod edit -require=github.com/luxfi/chains@v1.7.0 && \
|
||||
find /tmp/evm -name go.sum -exec sed -i -E '/^github.com\/(luxfi|hanzoai)\//d' {} + && \
|
||||
GOARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) \
|
||||
CGO_ENABLED=0 GOFLAGS=-mod=mod \
|
||||
go build -ldflags="-s -w" -o /luxd/build/plugins/${EVM_VM_ID} ./plugin && \
|
||||
CGO_ENABLED=${EVM_CGO} GOFLAGS=-mod=mod \
|
||||
go build -ldflags="-s -w" -tags "${EVM_TAGS}" -o /luxd/build/plugins/${EVM_VM_ID} ./plugin && \
|
||||
chmod +x /luxd/build/plugins/${EVM_VM_ID} && \
|
||||
rm -rf /tmp/evm
|
||||
|
||||
@@ -302,16 +335,16 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
# oraclevm -> r5m1ujrmXxVcQetG3CQfuDLHp2RHKh6vCDaFgBRQfUcTZh7eS
|
||||
# quantumvm -> ry9Sg8rZdT26iEKvJDmC2wkESs4SDKgZEhk5BgLSwg1EpcNug
|
||||
# relayvm -> sP6dLqrrBR9w3soP18fbJ3YzZecZdD7DDdfH2cFhhLq7Hy9bz
|
||||
# thresholdvm -> tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t
|
||||
# mpcvm -> qCURact1n41FcoNBch8iMVBwc9AWie48D118ZNJ5tBdWrvryS
|
||||
# zkvm -> vv3qPfyTVXZ5ArRZA9Jh4hbYDTBe43f7sgQg4CHfNg1rnnvX9
|
||||
|
||||
# MUST track node's go.mod luxfi/chains (the D-Chain dexvm + 10 VM plugins).
|
||||
# Bump with every chains release or the bundled VM plugins go stale vs node's deps.
|
||||
# v1.4.7 == node go.mod's luxfi/chains pin: warp consolidated to ONE luxfi/warp
|
||||
# helper (bridgevm/zkvm/thresholdvm), graphvm genesis-last-accepted fix, built on
|
||||
# helper (bridgevm/zkvm/mpcvm), graphvm genesis-last-accepted fix, built on
|
||||
# evm v1.99.48 + precompile v0.16.0 (enable-everything builder surface). Keeps the
|
||||
# baked VM plugins in lockstep with the host node.
|
||||
ARG CHAINS_REF=v1.4.8
|
||||
ARG CHAINS_REF=v1.7.6
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
git clone --depth 1 --branch ${CHAINS_REF} https://github.com/luxfi/chains.git /tmp/chains && \
|
||||
find /tmp/chains -name go.sum -exec sed -i -E '/^github.com\/(luxfi|hanzoai)\//d' {} +
|
||||
@@ -345,13 +378,25 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
-o /luxd/build/plugins/ry9Sg8rZdT26iEKvJDmC2wkESs4SDKgZEhk5BgLSwg1EpcNug ./cmd/plugin ) || echo "WARN: quantumvm plugin build skipped" ; \
|
||||
( cd /tmp/chains/relayvm && CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
|
||||
-o /luxd/build/plugins/sP6dLqrrBR9w3soP18fbJ3YzZecZdD7DDdfH2cFhhLq7Hy9bz ./cmd/plugin ) || echo "WARN: relayvm plugin build skipped" ; \
|
||||
( cd /tmp/chains/thresholdvm && CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
|
||||
-o /luxd/build/plugins/tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t ./cmd/plugin ) || echo "WARN: thresholdvm plugin build skipped" ; \
|
||||
( cd /tmp/chains/mpcvm && CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
|
||||
-o /luxd/build/plugins/qCURact1n41FcoNBch8iMVBwc9AWie48D118ZNJ5tBdWrvryS ./cmd/plugin ) || echo "WARN: mpcvm plugin build skipped" ; \
|
||||
( cd /tmp/chains/zkvm && CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
|
||||
-o /luxd/build/plugins/vv3qPfyTVXZ5ArRZA9Jh4hbYDTBe43f7sgQg4CHfNg1rnnvX9 ./cmd/plugin ) || echo "WARN: zkvm plugin build skipped" ; \
|
||||
( chmod +x /luxd/build/plugins/* 2>/dev/null || true ) && \
|
||||
test -s /luxd/build/plugins/kMhHABHM8j4bH94MCc4rsTNdo5E9En37MMyiujk4WdNxgXFsY \
|
||||
|| { echo "FATAL: bridgevm (B-Chain) plugin missing — the v1.30.16 regression would recur"; exit 1; } && \
|
||||
for p in \
|
||||
juFxSrbCM4wszxddKepj1GWwmrn9YgN1g4n3VUWPpRo9JjERA \
|
||||
kMhHABHM8j4bH94MCc4rsTNdo5E9En37MMyiujk4WdNxgXFsY \
|
||||
nZQm4Dmg1rjX18rb8maL9gamYyXPf1xCvF7ymWzxp6a1nSQTt \
|
||||
oR6tnZHezwogyf9fRnomNXC9ojwCEBAU6jdUzpgy2PB1tD7fM \
|
||||
pJJCSV7hHYVY6TUZwR8qUPAfuhX8JLb2C1AzNSezrYNbgau8M \
|
||||
r5m1ujrmXxVcQetG3CQfuDLHp2RHKh6vCDaFgBRQfUcTZh7eS \
|
||||
ry9Sg8rZdT26iEKvJDmC2wkESs4SDKgZEhk5BgLSwg1EpcNug \
|
||||
sP6dLqrrBR9w3soP18fbJ3YzZecZdD7DDdfH2cFhhLq7Hy9bz \
|
||||
qCURact1n41FcoNBch8iMVBwc9AWie48D118ZNJ5tBdWrvryS \
|
||||
vv3qPfyTVXZ5ArRZA9Jh4hbYDTBe43f7sgQg4CHfNg1rnnvX9 ; do \
|
||||
test -s /luxd/build/plugins/$p \
|
||||
|| { echo "FATAL: required chain-VM plugin $p missing/empty — its build failed above (see the matching WARN line); the runtime-stage hard COPY would otherwise fail cryptically. Surface & fix the real Go build error, or remove the plugin from BOTH the build list and the runtime COPY."; exit 1; } ; \
|
||||
done && \
|
||||
rm -rf /tmp/chains
|
||||
|
||||
# ============= Native D-Chain DEX VM Plugin Stage ================
|
||||
@@ -367,7 +412,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
# accelerator in pkg/lx is a separate concern gated by its own cuda/metal tags and
|
||||
# is NOT linked here. v1.5.10 is the first tag whose cmd/dchain builds CGO=0
|
||||
# (drops the phantom dchain+cgo gate); v1.5.11 wires CLOB order ingestion over the
|
||||
# node HTTP router (VM.CreateHandlers -> /ext/bc/D/dex/<method>, pkg/dchain/ingest.go)
|
||||
# node HTTP router (VM.CreateHandlers -> /v1/bc/D/dex/<method>, pkg/dchain/ingest.go)
|
||||
# so an order POSTed to the node flows submitTx -> mempool -> consensus -> Verify
|
||||
# (match) -> Accept; v1.5.12 persists the head block so the VM survives a restart
|
||||
# once advanced past genesis (GetBlock(lastAccepted) no longer ErrNotFound);
|
||||
@@ -379,7 +424,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
# DB (every plugin VM via vm/rpc) — that stranded the native D-Chain order book
|
||||
# (rebuildBookFromDB folded empty -> 0 fills despite committed asks). v1.5.15 adds
|
||||
# the committed-state READ surface (clob_get_trades/orders/markets/book over
|
||||
# /ext/bc/D/dex/<method>, pkg/dchain/read.go): read-only JSON of the durable trade
|
||||
# /v1/bc/D/dex/<method>, pkg/dchain/read.go): read-only JSON of the durable trade
|
||||
# log / resting book / markets, served beside the writes with ZERO consensus
|
||||
# impact. Needed to VERIFY a fill replicated identically across validators (query
|
||||
# every node, diff the trade rows + head root) and to feed markets-display (native
|
||||
@@ -390,6 +435,11 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
git clone --depth 1 --branch ${DEX_REF} https://github.com/luxfi/dex.git /tmp/dex && \
|
||||
find /tmp/dex -name go.sum -exec sed -i -E '/^github.com\/(luxfi|hanzoai)\//d' {} + && \
|
||||
cd /tmp/dex && \
|
||||
# Align the dexvm plugin's ZAP wire (luxfi/api) with the node. No dex release
|
||||
# pins api v1.0.16 yet (all <=v1.5.20 carry v1.0.15), so force it here; the
|
||||
# bump is code-free (adds InitializeResponse.Capabilities, capability-gated),
|
||||
# so dexvm emits the field the node decodes -> no "unexpected EOF" on D-Chain.
|
||||
go mod edit -require=github.com/luxfi/api@v1.0.16 && \
|
||||
. /build/build_env.sh && \
|
||||
GOARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) \
|
||||
CGO_ENABLED=0 GOFLAGS=-mod=mod \
|
||||
@@ -452,7 +502,7 @@ COPY --from=builder \
|
||||
/luxd/build/plugins/r5m1ujrmXxVcQetG3CQfuDLHp2RHKh6vCDaFgBRQfUcTZh7eS \
|
||||
/luxd/build/plugins/ry9Sg8rZdT26iEKvJDmC2wkESs4SDKgZEhk5BgLSwg1EpcNug \
|
||||
/luxd/build/plugins/sP6dLqrrBR9w3soP18fbJ3YzZecZdD7DDdfH2cFhhLq7Hy9bz \
|
||||
/luxd/build/plugins/tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t \
|
||||
/luxd/build/plugins/qCURact1n41FcoNBch8iMVBwc9AWie48D118ZNJ5tBdWrvryS \
|
||||
/luxd/build/plugins/vv3qPfyTVXZ5ArRZA9Jh4hbYDTBe43f7sgQg4CHfNg1rnnvX9 \
|
||||
/luxd/build/plugins/
|
||||
WORKDIR /luxd/build
|
||||
|
||||
@@ -1,324 +0,0 @@
|
||||
# Lux Mainnet Launch Checklist
|
||||
|
||||
Node: luxfi/node v1.24.11
|
||||
Consensus: Quasar (BLS+Corona, slashing, stake-weighted sampling)
|
||||
EVM: GPU ecrecover, 18 precompiles
|
||||
Genesis: networkID=1, startTime=2025-12-12T21:06:51Z (mainnet), networkID=2, startTime=2026-02-10T16:00:00Z (testnet)
|
||||
Precompile constraint: all activations MUST be after 2025-12-25
|
||||
|
||||
---
|
||||
|
||||
## Infrastructure
|
||||
|
||||
| Environment | Cluster | DOKS ID | K8s Version | Namespace | Validators |
|
||||
|-------------|---------|---------|-------------|-----------|------------|
|
||||
| Testnet | do-sfo3-lux-test-k8s | `005ec3c4` | 1.35.1-do.0 | lux-testnet | 11 (target) |
|
||||
| Rehearsal | do-sfo3-lux-dev-k8s | `0ff340e1` | 1.35.1-do.0 | lux-devnet | 21 (target) |
|
||||
| Mainnet | do-sfo3-lux-k8s | `04c46df5` | 1.34.1-do.4 | lux-mainnet | 21 (target) |
|
||||
|
||||
Current state: lux-k8s runs 5 validators (v1.23.31) via LuxNetwork CRD. Testnet cluster (lux-test-k8s) has a 3-replica StatefulSet (v1.23.40).
|
||||
|
||||
Image: `ghcr.io/luxfi/node:v1.24.11` (built via CI/CD, linux/amd64+arm64)
|
||||
Staking keys: KMS at `kms.lux.network`, project `lux-infra`, synced via KMSSecret CRD
|
||||
Secrets: never in manifests, never in env files, never committed
|
||||
Genesis configs: `/Users/z/work/lux/genesis/configs/{testnet,mainnet,devnet}/`
|
||||
K8s manifests: `/Users/z/work/lux/universe/k8s/`
|
||||
Profiles: `standard.json` (~100MB/node), `max.json` (~512MB/node)
|
||||
|
||||
Bootstrappers:
|
||||
- Mainnet: 5 seeds (ports 9631) -- `209.38.118.46`, `209.38.174.69`, `24.144.69.101`, `134.199.187.56`, `143.198.246.173`
|
||||
- Testnet: 2 seeds (ports 9641) -- `134.199.187.16`, `209.38.174.84`
|
||||
|
||||
Consensus parameters (mainnet): K=20, AlphaPreference=15, AlphaConfidence=15, Beta=20, ConcurrentPolls=4
|
||||
|
||||
Tokenomics: 10B total supply (9 decimals), min validator stake 1M LUX, min delegator stake 25K LUX, combined staking allowed (NFT+delegation), 80% uptime threshold
|
||||
|
||||
C-Chain: chainId=96369 (mainnet), 96368 (testnet), gasLimit=12M, targetBlockRate=2s, minBaseFee=25gwei
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Testnet (lux-test-k8s, networkID=2)
|
||||
|
||||
Target: validate all consensus, EVM, and staking behavior with K=11 validators.
|
||||
|
||||
### 1.1 Deployment
|
||||
|
||||
- [ ] Update LuxNetwork CRD in `universe/k8s/lux-k8s/validators/statefulset.yaml` (testnet section): `validators: 11`, `image.tag: v1.24.11`
|
||||
- [ ] Update lux-test-k8s StatefulSet in `universe/k8s/lux-test-k8s/testnet/statefulset.yaml`: replicas=11, image=v1.24.11
|
||||
- [ ] Generate 11 staking key pairs via `lux cli` and store in KMS (`lux-infra/testnet/staking/`)
|
||||
- [ ] Add 9 new bootstrapper entries to `genesis/configs/testnet/bootstrappers.json` (currently 2)
|
||||
- [ ] Apply `max.json` profile for testnet validators (512MB/node for stress testing headroom)
|
||||
- [ ] Regenerate testnet genesis with 11 initial validators via `genesis` tool
|
||||
- [ ] Verify all precompile activation timestamps are after 2025-12-25
|
||||
- [ ] Deploy via PaaS (platform.hanzo.ai), not manual kubectl
|
||||
- [ ] Verify all 11 pods reach Running state
|
||||
- [ ] Verify all 11 nodes report healthy via `/ext/health/liveness`
|
||||
|
||||
### 1.2 Bootstrap and Connectivity
|
||||
|
||||
- [ ] Verify all 11 validators discover each other via P2P (check `info.peers` RPC, expect 10 peers per node)
|
||||
- [ ] Verify staking port 9641 reachable between all pods (`luxd-{0..10}.luxd-headless.testnet.svc.cluster.local:9641`)
|
||||
- [ ] Verify P-chain bootstraps and all validators appear in `platform.getCurrentValidators`
|
||||
- [ ] Verify C-chain bootstraps and produces blocks
|
||||
- [ ] Verify X-chain bootstraps and processes UTXO transactions
|
||||
|
||||
### 1.3 Quasar Consensus Verification
|
||||
|
||||
- [ ] Submit transactions, verify Quasar finalization with K=11
|
||||
- [ ] Verify BLS aggregate signatures in block headers
|
||||
- [ ] Verify Corona optimistic fast path activates when all 11 validators are online
|
||||
- [ ] Measure finality latency (target: sub-second with Corona)
|
||||
- [ ] Verify stake-weighted sampling: validators with more stake get polled proportionally
|
||||
|
||||
### 1.4 EVM Execution
|
||||
|
||||
- [ ] Deploy a test contract, call all standard opcodes
|
||||
- [ ] Submit 100 sequential transactions, verify correct nonce ordering
|
||||
- [ ] Verify `eth_call` and `eth_estimateGas` return correct results
|
||||
- [ ] Verify block gas limit is 12M (from cchain.json config)
|
||||
- [ ] Verify minBaseFee=25gwei is enforced
|
||||
|
||||
### 1.5 GPU ecrecover
|
||||
|
||||
- [ ] Verify GPU backend auto-detection: CUDA on Linux DOKS nodes, Metal on macOS
|
||||
- [ ] Run ecrecover-heavy workload (1000 signature verifications per block)
|
||||
- [ ] Compare ecrecover throughput: GPU vs CPU fallback
|
||||
- [ ] Verify graceful fallback to CPU when GPU unavailable (set `--gpu-backend=cpu`)
|
||||
|
||||
### 1.6 Precompiles (all 18)
|
||||
|
||||
- [ ] Test each precompile individually via contract calls
|
||||
- [ ] Verify DEX precompile (LP-9010 PoolManager): pool creation, swaps, flash loans
|
||||
- [ ] Verify DEX router precompile (LP-9012): multi-hop routing
|
||||
- [ ] Verify all precompile addresses are deterministic and match spec
|
||||
- [ ] Verify precompile gas metering is correct (no underpriced or overpriced ops)
|
||||
- [ ] Verify precompiles revert correctly on invalid input
|
||||
|
||||
### 1.7 Slashing
|
||||
|
||||
- [ ] Craft equivocation evidence: have a validator sign two different blocks at same height
|
||||
- [ ] Submit equivocation proof to P-chain slashing precompile
|
||||
- [ ] Verify slashed validator's stake is burned
|
||||
- [ ] Verify slashed validator is removed from active set
|
||||
- [ ] Verify honest validators are unaffected
|
||||
|
||||
### 1.8 Uptime and Rewards
|
||||
|
||||
- [ ] Stop 1 validator (scale pod to 0)
|
||||
- [ ] Wait for reward period to elapse
|
||||
- [ ] Verify stopped validator's uptime drops below 80%
|
||||
- [ ] Verify rewards are withheld for the stopped validator
|
||||
- [ ] Restart the validator, verify it re-bootstraps and resumes
|
||||
- [ ] Verify validators with >80% uptime receive expected rewards
|
||||
|
||||
### 1.9 Stress Test
|
||||
|
||||
- [ ] Run stress test: maximum TPS with 1B gas blocks (increase gas limit temporarily)
|
||||
- [ ] Measure sustained TPS over 1 hour (target: verify consensus is the bottleneck, not EVM)
|
||||
- [ ] Monitor memory usage per node (should stay within `max.json` profile ~512MB)
|
||||
- [ ] Monitor disk I/O and database growth rate
|
||||
- [ ] Verify no consensus stalls under load
|
||||
- [ ] Verify block production rate stays at targetBlockRate=2s
|
||||
|
||||
### 1.10 Validator Join/Leave
|
||||
|
||||
- [ ] Add a 12th validator via `platform.addPermissionlessValidator` (permissionless staking)
|
||||
- [ ] Verify new validator bootstraps from existing state
|
||||
- [ ] Verify new validator begins participating in consensus
|
||||
- [ ] Remove a validator via unstaking (wait for stake period to end or use testnet short periods)
|
||||
- [ ] Verify removed validator exits gracefully
|
||||
- [ ] Verify remaining validators continue producing blocks
|
||||
|
||||
### 1.11 Formal Verification
|
||||
|
||||
- [ ] Run Lean proofs for Quasar consensus safety and liveness
|
||||
- [ ] Run TLA+ model checker for consensus state machine
|
||||
- [ ] Run Tamarin prover for BLS+Corona security properties
|
||||
- [ ] Run Halmos for EVM precompile correctness (symbolic execution)
|
||||
- [ ] All proofs pass with zero counterexamples
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Mainnet Rehearsal (lux-dev-k8s, networkID=3)
|
||||
|
||||
Target: full mainnet simulation with real parameters for 72 hours.
|
||||
|
||||
### 2.1 Deployment
|
||||
|
||||
- [ ] Update LuxNetwork CRD (devnet section): `validators: 21`, `image.tag: v1.24.11`
|
||||
- [ ] Generate 21 staking key pairs, store in KMS (`lux-infra/devnet/staking/`)
|
||||
- [ ] Use mainnet genesis parameters (networkID=3, but same tokenomics, same stake amounts)
|
||||
- [ ] Apply `max.json` profile
|
||||
- [ ] Deploy via PaaS
|
||||
- [ ] Verify all 21 pods healthy
|
||||
|
||||
### 2.2 Real Staking Parameters
|
||||
|
||||
- [ ] Configure minimum validator stake: 1M LUX
|
||||
- [ ] Configure minimum delegator stake: 25K LUX
|
||||
- [ ] Configure max delegation ratio: 10x
|
||||
- [ ] Configure NFT staking tiers (Genesis 500K/2x, Pioneer 750K/1.5x, Standard 1M/1x)
|
||||
- [ ] Verify combined staking logic: NFT value + delegation + staked >= 1M
|
||||
- [ ] Verify B-chain validators require 100M LUX + KYC
|
||||
|
||||
### 2.3 72-Hour Soak Test
|
||||
|
||||
- [ ] Start clock. Record block height and timestamp.
|
||||
- [ ] Continuous transaction load: 50 TPS sustained
|
||||
- [ ] Monitor: CPU, memory, disk, network per node (Prometheus + Grafana via PaaS)
|
||||
- [ ] Monitor: consensus latency p50/p95/p99
|
||||
- [ ] Monitor: block production rate (target: 1 block per 2s)
|
||||
- [ ] Monitor: peer count stability (all 21 connected)
|
||||
- [ ] Monitor: no OOMKills, no pod restarts, no crashloops
|
||||
- [ ] At hour 24: rolling restart of 5 validators (verify zero downtime)
|
||||
- [ ] At hour 48: simulate network partition (isolate 7 nodes), verify chain halts (< 2/3 online)
|
||||
- [ ] Restore partition, verify chain resumes within 30s
|
||||
- [ ] At hour 72: record final block height, calculate actual vs expected blocks
|
||||
- [ ] Pass criteria: zero consensus faults, zero data loss, <1% block time variance
|
||||
|
||||
### 2.4 Security Audit
|
||||
|
||||
- [ ] External security audit firm engaged (Red team)
|
||||
- [ ] Audit scope: consensus, EVM, precompiles, staking, slashing, P2P networking
|
||||
- [ ] Audit result: 0 critical findings, 0 high findings
|
||||
- [ ] All medium findings remediated or accepted with documented risk
|
||||
- [ ] Audit report signed and archived
|
||||
|
||||
### 2.5 Bridge / Teleport (B-Chain + T-Chain)
|
||||
|
||||
- [ ] Deploy MPC threshold signing (5 nodes, threshold 3) in `lux-mpc` namespace
|
||||
- [ ] Deploy bridge UI and API in `lux-bridge` namespace
|
||||
- [ ] Verify CGGMP21 keygen: 5 parties generate shared key
|
||||
- [ ] Verify threshold signing: 3-of-5 produces valid signature
|
||||
- [ ] Test cross-chain transfer: lock on source chain, mint on Lux
|
||||
- [ ] Test reverse: burn on Lux, unlock on source chain
|
||||
- [ ] Verify MPC API at `mpc-api.lux.network` responds
|
||||
- [ ] Verify bridge handles partial MPC node failure (2 down, 3 still sign)
|
||||
|
||||
### 2.6 DEX (D-Chain + Precompiles)
|
||||
|
||||
- [ ] Deploy DEX precompile PoolManager (LP-9010) -- already active from genesis
|
||||
- [ ] Deploy DEX Router precompile (LP-9012) -- already active from genesis
|
||||
- [ ] Deploy off-chain CLOB matching engine
|
||||
- [ ] Create liquidity pool via precompile
|
||||
- [ ] Execute swap via router precompile
|
||||
- [ ] Verify AMM pricing matches expected curve
|
||||
- [ ] Verify flash loan execution and repayment
|
||||
- [ ] Test CLOB: place limit order, verify fill
|
||||
- [ ] Verify DEX on lux.exchange frontend connects to devnet
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Mainnet Launch (lux-k8s, networkID=1)
|
||||
|
||||
Target: production network with real value.
|
||||
|
||||
### 3.1 Pre-launch
|
||||
|
||||
- [ ] All Phase 1 items passed
|
||||
- [ ] All Phase 2 items passed
|
||||
- [ ] Security audit sign-off received
|
||||
- [ ] Formal verification suite green
|
||||
- [ ] Legal review complete (terms of service, validator agreements)
|
||||
- [ ] Incident response runbook written and tested
|
||||
|
||||
### 3.2 Genesis Ceremony
|
||||
|
||||
- [ ] Final genesis config reviewed: `genesis/configs/mainnet/genesis.json` (networkID=1)
|
||||
- [ ] Genesis startTime confirmed: 2025-12-12T21:06:51Z
|
||||
- [ ] Initial allocations verified (500M initial + unlock schedule)
|
||||
- [ ] All 5 bootstrapper IPs confirmed reachable on port 9631
|
||||
- [ ] Genesis hash computed and published to lux.network
|
||||
- [ ] Genesis block signed by founding validators
|
||||
|
||||
### 3.3 Validator Onboarding
|
||||
|
||||
- [ ] Update LuxNetwork CRD (mainnet section): `validators: 21`, `image.tag: v1.24.11`
|
||||
- [ ] Scale from 5 current validators to 21
|
||||
- [ ] Generate 16 new staking key pairs in KMS (`lux-infra/mainnet/staking/`)
|
||||
- [ ] Update bootstrappers.json with all 21 validator endpoints
|
||||
- [ ] Deploy via PaaS with rolling update strategy
|
||||
- [ ] Verify all 21 validators healthy and in consensus
|
||||
- [ ] Publish validator onboarding guide for external operators
|
||||
- [ ] Open permissionless staking after initial stabilization period
|
||||
|
||||
### 3.4 Public RPC Endpoints
|
||||
|
||||
- [ ] Deploy KrakenD API gateway in `lux-gateway` namespace
|
||||
- [ ] Configure rate limiting per IP and per API key
|
||||
- [ ] Configure Cloudflare DNS (proxied, full SSL):
|
||||
- `api.lux.network` -> gateway (C-chain + P-chain + X-chain RPC)
|
||||
- `ws.lux.network` -> gateway (WebSocket subscriptions)
|
||||
- [ ] Verify `eth_chainId` returns `0x17871` (96369)
|
||||
- [ ] Verify `net_version` returns `96369`
|
||||
- [ ] Verify RPC endpoints handle 10K req/s without degradation
|
||||
- [ ] Verify WebSocket subscriptions for `newHeads`, `logs`, `pendingTransactions`
|
||||
|
||||
### 3.5 Explorer Deployment
|
||||
|
||||
- [ ] Deploy explorer (luxfi/explorer) in `lux-explorer` namespace (already has manifests for 5 chains)
|
||||
- [ ] Configure for C-chain (chainId 96369)
|
||||
- [ ] Configure indexers for all active chains
|
||||
- [ ] Configure Cloudflare DNS: `explore.lux.network`
|
||||
- [ ] Verify block display, transaction search, contract verification
|
||||
- [ ] Deploy exchange frontend: `lux.exchange`
|
||||
|
||||
### 3.6 Bridge Activation
|
||||
|
||||
- [ ] Deploy MPC production cluster (5 nodes, threshold 3)
|
||||
- [ ] Generate production MPC keys (CGGMP21 keygen ceremony)
|
||||
- [ ] Store MPC key shares in KMS (`lux-infra/mainnet/mpc/`)
|
||||
- [ ] Deploy bridge contracts on supported chains (ETH, BNB, Polygon, Arbitrum, Base, Optimism)
|
||||
- [ ] Deploy bridge UI at bridge domain
|
||||
- [ ] Configure Cloudflare DNS
|
||||
- [ ] Enable deposits (one chain at a time, small limits first)
|
||||
- [ ] Monitor for 24h, then raise limits
|
||||
|
||||
### 3.7 Post-Launch Monitoring
|
||||
|
||||
- [ ] Prometheus + Grafana dashboards live (via PaaS o11y stack)
|
||||
- [ ] Alerts configured:
|
||||
- Validator down (any pod not Ready for >5min)
|
||||
- Consensus stall (no new block for >30s)
|
||||
- Peer count drop (any node <15 peers)
|
||||
- Memory usage >80% of limit
|
||||
- Disk usage >70%
|
||||
- Error rate >1% on RPC endpoints
|
||||
- [ ] On-call rotation established
|
||||
- [ ] Runbook covers: validator restart, chain halt recovery, emergency upgrade, key rotation
|
||||
|
||||
---
|
||||
|
||||
## Port Reference
|
||||
|
||||
| Network | HTTP | Staking | Metrics |
|
||||
|---------|------|---------|---------|
|
||||
| Mainnet | 9630 | 9631 | 9090 |
|
||||
| Testnet | 9640 | 9641 | 9090 |
|
||||
| Devnet | 9650 | 9651 | 9090 |
|
||||
|
||||
## Chain IDs
|
||||
|
||||
| Chain | Mainnet | Testnet | Devnet |
|
||||
|-------|---------|---------|--------|
|
||||
| C-Chain | 96369 | 96368 | 96370 |
|
||||
| Zoo EVM | 200200 | 200201 | 200202 |
|
||||
| Hanzo EVM | 36963 | 36964 | 36964 |
|
||||
| SPC EVM | 36911 | 36910 | 36912 |
|
||||
| Pars EVM | 494949 | 7071 | 494951 |
|
||||
|
||||
## File References
|
||||
|
||||
| What | Path |
|
||||
|------|------|
|
||||
| Node source | `~/work/lux/node/` |
|
||||
| Genesis configs | `~/work/lux/genesis/configs/{mainnet,testnet,devnet}/` |
|
||||
| Chain configs | `~/work/lux/genesis/configs/chain-configs/` |
|
||||
| K8s manifests | `~/work/lux/universe/k8s/` |
|
||||
| Validator CRD | `~/work/lux/universe/k8s/lux-k8s/validators/statefulset.yaml` |
|
||||
| Testnet StatefulSet | `~/work/lux/universe/k8s/lux-test-k8s/testnet/statefulset.yaml` |
|
||||
| Node profiles | `~/work/lux/node/config/profiles/{standard,max}.json` |
|
||||
| Tokenomics config | `~/work/lux/node/config/tokenomics.go` |
|
||||
| GPU config | `~/work/lux/node/config/gpu.go` |
|
||||
| Health/consensus params | `~/work/lux/node/config/health.go` |
|
||||
| Network registry | `~/work/lux/universe/NETWORKS.yaml` |
|
||||
@@ -10,3 +10,24 @@ For the canonical Lux IP and licensing strategy, see:
|
||||
|
||||
For commercial inquiries that go beyond BSD-3 (e.g. private moat
|
||||
acceleration kernels), contact `licensing@lux.network`.
|
||||
|
||||
## Upstream attribution
|
||||
|
||||
See [NOTICE](NOTICE) for the full attribution. In summary:
|
||||
|
||||
- **avalanchego** (Ava Labs, Inc.) — this repository is derived from
|
||||
[ava-labs/avalanchego](https://github.com/ava-labs/avalanchego), licensed
|
||||
under the **BSD 3-Clause License** (© 2019 Ava Labs, Inc.). BSD-3 is
|
||||
permissive; the Lux additions here are likewise BSD-3-Clause.
|
||||
- **go-ethereum** (The go-ethereum Authors) — EVM support derives from
|
||||
[go-ethereum](https://github.com/ethereum/go-ethereum). It is **not**
|
||||
vendored in-tree; it is consumed as the external Go module
|
||||
`github.com/luxfi/geth`, which retains go-ethereum's original licenses:
|
||||
the library is **LGPL-3.0-or-later** and the command-line tools are
|
||||
**GPL-3.0**.
|
||||
|
||||
**Copyleft flag:** the LGPL-3.0/GPL-3.0 terms of the go-ethereum-derived code
|
||||
(via `github.com/luxfi/geth`) are **not** superseded by this repository's
|
||||
BSD-3-Clause license. Distributing compiled node binaries must honor LGPL-3.0
|
||||
for the linked geth library (published source of that code and its
|
||||
modifications at <https://github.com/luxfi/geth>, and user ability to relink).
|
||||
|
||||
@@ -65,10 +65,10 @@ selection, and EVM contract auth.
|
||||
### Where to look for X
|
||||
- Profile resolve at boot: `node/node.go:initSecurityProfile`
|
||||
- Profile RPC + REST + metrics: `service/security/`
|
||||
- JSON-RPC namespace: `security` at `POST /ext/security`
|
||||
- JSON-RPC namespace: `security` at `POST /v1/security`
|
||||
(methods `securityProfile`, `blockSecurity`)
|
||||
- REST sidecars: `GET /ext/security/profile`, `GET /ext/security/block/{n}`
|
||||
- Prometheus gauges: `/ext/metrics` under the `security_*` family
|
||||
- REST sidecars: `GET /v1/security/profile`, `GET /v1/security/block/{n}`
|
||||
- Prometheus gauges: `/v1/metrics` under the `security_*` family
|
||||
- Peer scheme gate: `network/peer/scheme_gate.go`
|
||||
- Classical-compat registry: `vms/txs/auth/policy.go`
|
||||
- Mempool gate (P-Chain): `vms/platformvm/mempool/*.go`
|
||||
@@ -85,6 +85,29 @@ selection, and EVM contract auth.
|
||||
|
||||
## FeePolicy — canonical user-tx fee gate
|
||||
|
||||
> **Topology + UTXO ownership + cross-chain fee model** are normatively
|
||||
> specified by [**LP-0130** (Chain Topology, UTXO Ownership, and Fee
|
||||
> Model)](https://github.com/luxfi/lps/blob/main/LPs/lp-0130-chain-topology-utxo-ownership-and-fee-model.md).
|
||||
> Read that LP before touching any VM's fee/settlement path or any
|
||||
> cross-chain import/export flow. In particular:
|
||||
>
|
||||
> - Only **P** and **X** are canonical UTXO state machines (LP-0130 §2).
|
||||
> - **X** is the money rail; **P** is the staking/reward rail; **LUX**
|
||||
> is the fee currency everywhere (LP-0130 §3, §5).
|
||||
> - **Q-Chain has no user-payable blockspace** — finality is a
|
||||
> validator obligation paid via P (LP-0130 §6). `quantumvm` MUST
|
||||
> use `NoUserTxPolicy{}` — enforced in chains/quantumvm/feegate.go as of 2026-07-03.
|
||||
> - **M-Chain fees are service fees** deducted from the originating
|
||||
> chain's fee pool, not a user M-balance (LP-0130 §7). `mpcvm`
|
||||
> already runs `NoUserTxPolicy{}` — correct.
|
||||
> - **B-Chain fees** are deducted from the bridged amount (LP-0130 §8).
|
||||
> - Every non-P/X chain settles worker rewards to X (asset payouts) or
|
||||
> P (staker rewards) via epoch fee roots reconciled at Q finality
|
||||
> (LP-0130 §4, §11).
|
||||
> - **Σ-escrow invariant** (LP-0130 I-8): `Σ non-P/X fee balances ==
|
||||
> Σ X-side fee escrow` at every Q checkpoint. Drift is a
|
||||
> finality-blocking fault.
|
||||
|
||||
Every Lux VM that accepts user-submitted txs declares a `fee.Policy`
|
||||
(package `vms/types/fee`). There is one interface and one validator —
|
||||
no per-VM bespoke fee structs.
|
||||
@@ -97,14 +120,14 @@ no per-VM bespoke fee structs.
|
||||
| zkvm | Z-Chain | user-tx | `FlatPolicy{Fee: MinTxFeeFloor, ...}` |
|
||||
| aivm | A-Chain | user-tx | `FlatPolicy{Fee: MinTxFeeFloor, ...}` |
|
||||
| keyvm | K-Chain | user-tx | `FlatPolicy{Fee: MinTxFeeFloor, ...}` |
|
||||
| bridgevm | B-Chain | user-tx | `FlatPolicy{Fee: MinTxFeeFloor, ...}` |
|
||||
| quantumvm | Q-Chain | user-tx | `FlatPolicy{Fee: MinTxFeeFloor, ...}` |
|
||||
| bridgevm | B-Chain | user-tx | `FlatPolicy{Fee: MinTxFeeFloor, ...}` (deducted from bridged amount, LP-0130 §8) |
|
||||
| quantumvm | Q-Chain | **service-only** (LP-0130 §6) | `NoUserTxPolicy{}` — validator obligation, no user-payable blockspace |
|
||||
| identityvm | I-Chain | user-tx | `FlatPolicy{Fee: MinTxFeeFloor, ...}` |
|
||||
| thresholdvm | M-Chain | service-only | `NoUserTxPolicy{}` |
|
||||
| mpcvm | M-Chain | service-only (LP-0130 §7) | `NoUserTxPolicy{}` — fees pulled from originating chain's fee pool |
|
||||
| oraclevm | O-Chain | service-only | `NoUserTxPolicy{}` |
|
||||
| relayvm | R-Chain | service-only | `NoUserTxPolicy{}` |
|
||||
| graphvm | G-Chain | read-only | `NoUserTxPolicy{}` (GraphQL refuses `mutation`) |
|
||||
| evm | C-Chain | user-tx | native EVM gas (gas * gasPrice >= 0 enforced upstream) |
|
||||
| evm | C-Chain | user-tx | native EVM gas (gas * gasPrice >= 0 enforced upstream); balance is X-imported LUX (LP-0130 §10) |
|
||||
| platformvm | P-Chain | user-tx | native `TxFee` field on Config |
|
||||
| avm | X-Chain | user-tx | native `TxFee` field on Config |
|
||||
|
||||
@@ -135,6 +158,370 @@ charge less.
|
||||
- Relay (R-Chain): `~/work/lux/relay/vm/feegate.go` (re-exported by `~/work/lux/chains/relayvm/`)
|
||||
- Graph (G-Chain): `~/work/lux/chains/graphvm/feegate.go` (read-only; NoUserTxPolicy)
|
||||
|
||||
## C-Chain tx-fee routing — RewardManager to DAO Safe (P-Chain: NO CHANGE)
|
||||
|
||||
Owner tokenomics pivoted: **100% of C-Chain tx fees accrue to the chain's DAO Gov
|
||||
Safe** via the existing `rewardmanager` precompile (C-Chain only). This needs **no
|
||||
P-Chain change** — routing is `GetCoinbaseAt` → reward address on the C-Chain. See
|
||||
`~/work/lux/evm/LLM.md` → "C-Chain Tx-Fee Routing — RewardManager". The P-Chain
|
||||
`feeRewardPool` fold-in below is **NOT built** (design-only, superseded); no
|
||||
`vms/platformvm/state` change was made.
|
||||
|
||||
<details><summary>Superseded design — 50/50 burn + P-Chain staking-reward fold (dormant option)</summary>
|
||||
|
||||
If the DAO ever chooses an in-protocol 50/50 split, the C-Chain half exists (dormant,
|
||||
`FeeSplitTimestamp` gated off) and the P-Chain fold-in would be: system-triggered epoch
|
||||
export of the C-Chain vault C→P → persisted `feeRewardPool` in `vms/platformvm/state`
|
||||
(mirror the `accruedFees` singleton, upgrade-safe) → pro-rata payout at
|
||||
`vms/platformvm/txs/executor/proposal_tx_executor.go` `rewardValidatorTx` (~line 285),
|
||||
unified into `PotentialReward`, NO second mint → decrement `currentSupply` by the epoch
|
||||
burn. Model R1 (move-not-mint), conservation-exact; R2 (burn+re-mint) rejected.
|
||||
</details>
|
||||
|
||||
## v1.36.12 fleet rollout — durable rejoin fix + RewardManager→DAO Safe (IN PROGRESS 2026-07-15)
|
||||
|
||||
Rolling the durable rejoin fix (node `63f61429d1`) across all Lux nets, gated
|
||||
devnet→testnet→mainnet, + activating RewardManager fees. Two hard facts were found
|
||||
on the devnet canary that change the naive "swap image" plan:
|
||||
|
||||
**BLOCKER (fixed in v1.36.12): published `node:v1.36.11` (digest `c3cf92a6`) cannot
|
||||
run ANY EVM chain.** Its baked VM plugins were built against a stale `luxfi/api`:
|
||||
C-Chain EVM from `luxfi/evm@v1.104.8` and D-Chain dexvm from `luxfi/dex@v1.5.15`
|
||||
both resolve `api v1.0.15`; the node pins `api v1.0.16`, which APPENDED
|
||||
`InitializeResponse.Capabilities` (Quasar-export handshake, api `1f2dc5a`). Node
|
||||
decodes the field, stale plugins never encode it → `vms/rpcchainvm/zap/client.go`
|
||||
fails every EVM `Initialize` with `zap decode initialize response: unexpected EOF`.
|
||||
Native VMs (P/X/Q) unaffected. **Fix = v1.36.12** (this repo, tag pushed, ARC docker
|
||||
build run 29381442539): `EVM_VERSION v1.104.8→v1.104.9`, force `api@v1.0.16` in the
|
||||
dexvm build stage; `CHAINS_REF=v1.7.6` was already v1.0.16. Node binary unchanged
|
||||
(still the durable fix). api bump is code-free for plugins (`chains v1.7.4→v1.7.5`
|
||||
adopted it go.mod-only). **Roll v1.36.12, NOT v1.36.11.** (Also: `api 1f2dc5a` added
|
||||
`Capabilities` WITHOUT bumping `version.RPCChainVMProtocol` (42) → skew is invisible
|
||||
at handshake, only fails at Initialize decode. Consider bumping the protocol next
|
||||
api-wire change so skews fail fast.)
|
||||
|
||||
**MIGRATION: v1.36.2→v1.36.x is a P-Chain codec change (linearcodec→ZAP-native),
|
||||
one-time DB wipe + re-bootstrap.** v1.36.11/12 cannot read a v1.36.2 P-Chain zapdb
|
||||
(`loadMetadata: feeState: zap: invalid magic bytes`; `state_commit.go:116` "database
|
||||
must be wiped"). Recovery = wipe `/data/db`+`/data/chainData`, re-bootstrap from
|
||||
peers. **Cross-version bootstrap (v1.36.11 node ← v1.36.2 peers) is PROVEN working**
|
||||
(devnet luxd-1: P/X re-bootstrapped from the 4 v1.36.2 peers). Devnet startup got a
|
||||
marker-gated one-time wipe (`/data/.zap-native-migrated`): absent→wipe+set marker,
|
||||
present→NO wipe (the durable-fix no-wipe restart path). mainnet/testnet/zoo use
|
||||
`startup.sh` which already has `.wipe-cchain` (C-Chain only) + `.allow-bootstrap`
|
||||
(flips skip-bootstrap=false + EVM state-sync); for the codec migration the P-Chain
|
||||
zapdb (`/data/db`) must also be cleared. **Mainnet C-Chain is 1.08M blocks → MUST
|
||||
enable EVM state-sync for the re-sync (full replay is too slow); native VMs are tiny.**
|
||||
|
||||
**Durable fix (`63f61429d1`):** discriminator for keeping the staked beacon set is
|
||||
SYBIL-PROTECTION, not `--skip-bootstrap`. So a behind validator on a sybil-protected
|
||||
net catches up from peers even with `--skip-bootstrap=true` (which prod hardcodes),
|
||||
no wipe. Devnet added `--skip-bootstrap=true` to the inline cmd to exercise this.
|
||||
|
||||
**RewardManager (C-Chain precompile, per-net `cchain-upgrade.json` → append one
|
||||
`precompileUpgrades` entry, dated `blockTimestamp`):** proven testnet shape is
|
||||
`{"rewardManagerConfig":{"blockTimestamp":<ts>,"adminAddresses":["<admin>"],
|
||||
"initialRewardConfig":{"rewardAddress":"<reward>"}}}`. Reward addr = coinbase; 100%
|
||||
fees land there, blackhole `0x0100…00` goes flat. Addresses: **testnet ALREADY LIVE**
|
||||
(reward `0xEAbCC110fAcBfebabC66Ad6f9E7B67288e720B59`, admin `0x9011…94714`); **mainnet**
|
||||
reward+admin = DAO Gov Safe `0x8E29b816c6C35b13cE1ff68D33E245C2bda8ac3D`; **zoo**
|
||||
reward `0x229599f227231d8C90fcF1a78589F5DC4b7A6962`; **devnet** reward
|
||||
`0x8d5081153aE1cfb41f5c932fe0b6Beb7E159cF84` (idx2), admin `0x9011…94714` (idx0).
|
||||
Source ConfigMaps: devnet `luxd-chain-upgrades/cchain-upgrade.json`; mainnet+testnet
|
||||
`luxd-startup/cchain-upgrade.json`; zoo `zood-mv-genesis/upgrade.json` (`--upgrade-file`).
|
||||
|
||||
**Rollout levers (lux-operator is scaled 0/0 — sts/cm are the live source of truth;
|
||||
CRs are STALE, do not scale operator up mid-roll):** ports devnet 9650 / testnet 9640
|
||||
/ mainnet 9630 / zoo 9630; RPC path `/v1/bc/C/rpc`; container `luxd` (`zood` on zoo).
|
||||
Devnet uses an inline generated cmd; testnet/mainnet/zoo use `/scripts/startup.sh`.
|
||||
**Zoo `zood-mv` trap: RollingUpdate + hardcoded `--skip-bootstrap=true` with NO
|
||||
`.allow-bootstrap` gate → switch to OnDelete BEFORE rolling.** Lux sts are OnDelete.
|
||||
Master funded key = LUX_MNEMONIC (secret `lux-deployer`) idx0 `0x9011…94714`;
|
||||
`genesis/cmd/derivekey -mnemonic "$M"` (path m/44'/9000'/0'/0/i, `CGO_ENABLED=0`).
|
||||
|
||||
**Per-node roll protocol (ALL nets, one at a time, NEVER 2 mainnet down — quorum
|
||||
4/5):** set sts image v1.36.12 (+ rewardManager cm edit) → delete ONE pod → WAIT
|
||||
until it is back at **TIP HEIGHT matching the others** (NOT pod-Ready; a wedged node
|
||||
false-reports Ready) AND C-Chain serves RPC → only then the next. If any node fails
|
||||
to return to tip, STOP that net and report.
|
||||
|
||||
**State at pause:** v1.36.12 tag pushed + ARC build dispatched (run 29381442539).
|
||||
Devnet sts = v1.36.11 + skip-bootstrap + wipe-marker; luxd-1 migrated (P/X up on
|
||||
v1.36.11, C-Chain down = the plugin bug → will heal on v1.36.12); luxd-0/2/3/4 still
|
||||
v1.36.2 healthy (devnet C-Chain 4/5). Nothing rolled on testnet/mainnet/zoo. NEXT:
|
||||
when v1.36.12 image is ready → set devnet sts image v1.36.12, delete luxd-1, confirm
|
||||
C-Chain inits + reaches tip; then finish devnet (durable-fix proof + RewardManager),
|
||||
then gated testnet→mainnet→zoo.
|
||||
|
||||
## Crash-boot recovery armor + upstream delta verdict (vms/proposervm, test-only, 2026-07-28)
|
||||
|
||||
**Upstream review (avalanchego master `bcc851822d..c5d3c8aafe`, 6 commits): ZERO code
|
||||
ports.** grpc bump (no named bug; would cascade through luxfi/vm), README typo, typed
|
||||
sync-client scaffolding (client-side only, no serving handlers even upstream; our sync
|
||||
direction is ZAP-native — reference for task #66 only), a Firewood `debug_intermediateRoots`
|
||||
enabler (our tracers API is `.disabled`, twice N/A), and two streaming-VM-only mempool
|
||||
guards whose failure modes we already bracket (`miner/worker.go:62 targetTxsSize=1800KiB`
|
||||
build cap + txpool `ErrGasLimit`/128KB caps under the 2MiB wire cap). **Nothing in the
|
||||
delta touches proposervm — the open `vm.go:380` preferred-fetch failure gets no upstream
|
||||
help.** One upstream BRANCH does touch proposervm — `containerman17/proposervm-dedup-capability`
|
||||
(+72 vm.go): inner-bytes dedup in the block store plus an extra boot-repair arm for the
|
||||
unclean-shutdown window where the outer index lands above the inner survivor (the very
|
||||
window `OuterCommittedInnerNot` below pins) — storage-layer work, not the build-path
|
||||
preferred fetch, so it changes nothing about `vm.go:380` either. What WAS worth taking is
|
||||
their recovery-test discipline, ported as:
|
||||
|
||||
**`vm_crashboot_test.go`** — copy the COMMITTED bytes out from under a RUNNING proposervm
|
||||
(no Shutdown), boot a SECOND, cold VM over the copy (repair + metadata, Initialize order),
|
||||
assert source-equality (finality pointer, fork height, every height's envelope openable
|
||||
cold) and then BUILD on the recovered tip (outer parent == recovered envelope, inner
|
||||
parent == its inner block — the errInnerParentMismatch invariant from a cold boot).
|
||||
Matrix: nothing-accepted / first-accept / longer run / copy-while-source-runs-on, plus
|
||||
`OuterCommittedInnerNot`: the one crash window the accept path leaves open
|
||||
(`acceptPostForkBlock` commits BEFORE the inner accept) must boot via the roll-back arm
|
||||
and re-propose. Harness seams added in height_lag_repro_test.go (`testVMOnBase`,
|
||||
`acceptRangeThroughProposervm`). **Negative control executed**: deleting the
|
||||
`vm.db.Commit()` from `acceptPostForkBlock` fails every persistence-bearing scenario
|
||||
(4 of 6) — the armor bites on flush-order regressions, not just on green paths.
|
||||
|
||||
## v1.36.35 — the certified-descendant false halt + the plugin-killing map fatal (devnet 96367, 2026-07-28)
|
||||
|
||||
> Shipped as v1.36.35, not v1.36.34: the v1.36.34 tag inherited a build break that
|
||||
> `011e9bf99d` ("deps: drop go.sum lines that disagree with the checksum log") had
|
||||
> already pushed to main — it bumped `zap-proto/http` from a pseudo-version to v0.3.0,
|
||||
> whose `Server.Handler` is a `fasthttp.RequestHandler` and whose `NewTransport` is now
|
||||
> `Dial(network, addr)`, so `server/http/zap_listener.go` no longer compiled. **main was
|
||||
> unbuildable from that commit until this one.** Repaired here by bridging the one
|
||||
> net/http handler chain with `fasthttpadaptor.NewFastHTTPHandler` (the two transports
|
||||
> stay behaviourally identical — only the wire encoding differs) and moving the
|
||||
> round-trip test onto the fasthttp request/response pair. v1.36.34 produced no image
|
||||
> and its tag was deleted.
|
||||
|
||||
Rolling devnet to v1.36.33 fixed the build→verify-fail→drop loop and unmasked two
|
||||
DIFFERENT failures. Both are fixed here; each has a regression test that fails before
|
||||
and passes after.
|
||||
|
||||
**P0-1 — five validators `os.Exit(1)` on a benign state.** Each devnet node hit, once:
|
||||
|
||||
```
|
||||
error VM accepted head is CONSENSUS-CERTIFIED and conflicts with the newly finalized
|
||||
block — refusing to orphan it orphanedHeight=1364
|
||||
fatal SetPreference would orphan a CONSENSUS-CERTIFIED block — refusing (fail-closed)
|
||||
error="cannot orphan finalized block at height: 1364 to common block at height: 1363"
|
||||
```
|
||||
|
||||
at three distinct height pairs (1258/1259, 1363/1364, 1364/1365), ALWAYS with
|
||||
`certified = head − 1`, with every surviving node holding byte-identical blocks at all
|
||||
five heights — no fork anywhere. (The "1168/1169" pair in the original report never
|
||||
appears in any fatal line; those two blocks are a normal parent/child present on all
|
||||
live nodes.)
|
||||
|
||||
Two defects in `luxfi/consensus`, one crash — fixed in **consensus v1.36.12**:
|
||||
|
||||
- *Producer.* `acceptWithCertCore` releases `t.mu` across every VM call-out, then steers
|
||||
the VM with its STALE local `blockID`. A finalize that completed in that window has
|
||||
already advanced the ledger AND the EVM to `blockID+1`, so the steer is BACKWARDS and
|
||||
the EVM's accepted-irreversibility guard (`evm/core/blockchain.go:1987`,
|
||||
`commonBlock < lastAccepted`) correctly refuses it. **That guard is right and is
|
||||
untouched.** Now steers at the live build anchor (`PreferredBuildTip` →
|
||||
`ledger.BuildAnchor`), which the accept ordering (ApplyCert BEFORE VM.Accept) keeps at
|
||||
or above the VM's own accepted head. Same value the build path already uses.
|
||||
- *Classifier.* `reconcileVMToCertified` asked only "is the head the ledger's certified
|
||||
canonical at ITS OWN height?" — trivially true for every healthy node whose head is
|
||||
certified — and called that a two-blocks-at-one-height double-finalization. It never
|
||||
established that `certified` was at that same height. The ledger holds one canonical
|
||||
per height along one contiguous chain, so when both are certified at their own heights
|
||||
they lie on that one chain and the head merely DESCENDS from the target: nothing is
|
||||
orphaned. **The fail-closed halt is not weakened** — it now fires on the state that is
|
||||
actually unsafe (steering off a certified head onto a block our own ledger does not
|
||||
certify at its height).
|
||||
|
||||
Direction: NEITHER roll the head back NOR certify forward. The VM head is legitimately
|
||||
ahead and already CONTAINS the certified block; `FinalityLedger.BuildAnchor` already
|
||||
documents `head > certified` as the designed state, and rolling back is exactly what
|
||||
`blockchain.go:1987` exists to refuse. The correct action is no action — plus not issuing
|
||||
the backwards steer at all.
|
||||
|
||||
**P0-2 — the EVM plugin process dies and never comes back.** devnet luxd-1:
|
||||
|
||||
```
|
||||
fatal error: concurrent map read and map write
|
||||
vm/components/chain.(*State).getCachedBlock state.go:216
|
||||
vm/components/chain.(*State).ParseBlock state.go:267
|
||||
evm/plugin/evm.(*VM).ParseBlock vm.go:340
|
||||
vm/rpc.(*zapVMServer).handleParseBlock vm_server_zap.go:477
|
||||
```
|
||||
|
||||
`chain.State` was written against avalanchego's contract that the consensus engine holds
|
||||
the chain lock across every VM call. The ZAP VM server does NOT reinstate it — it
|
||||
dispatches ParseBlock/GetBlock and the Verify/Accept/Reject wrappers concurrently.
|
||||
`verifiedBlocks` (plain map) and `lastAcceptedBlock` (pointer) are the only State that is
|
||||
not self-synchronising; every `cache.Cacher` carries its own mutex. Fixed in **vm v1.3.3**
|
||||
by giving State one RWMutex for exactly those two, never held across a call into the
|
||||
inner VM. A Go map fatal is unrecoverable: it kills the plugin, **luxd survives and keeps
|
||||
answering `info.getNodeVersion` while its chain is gone** — pod-Ready and `/v1/health`
|
||||
both stay green — and there is no self-heal.
|
||||
|
||||
**Devnet roll result (10:23–10:45Z).** All five pods on v1.36.35, `restarts=0` on every one
|
||||
for 20+ minutes. Both fixed defects are GONE fleet-wide: `CONSENSUS-CERTIFIED` fatal = 0
|
||||
(previously all five died within minutes), `concurrent map` = 0. luxd-1, whose C-Chain had
|
||||
been dead ~45 min with no self-heal, came back at tip parity on first boot; luxd-3, stuck at
|
||||
1524, caught up immediately. A transaction of ours got a receipt with identical status,
|
||||
blockHash and resulting balance on all five nodes (`0xcb5e9a06…`, block 1636, status 0x1).
|
||||
|
||||
**NOT accepted — a THIRD, pre-existing defect blocks five-way parity.** Two nodes (luxd-2 at
|
||||
1789, luxd-4 at 1825) freeze their tip while 0/1/3 advance in lockstep, emitting
|
||||
|
||||
```
|
||||
error unexpected build block failure error="not found"
|
||||
reason="failed to fetch preferred block; no distinct last-accepted fallback"
|
||||
```
|
||||
|
||||
from `vms/proposervm/vm.go:380`. That branch is reached when the preferred block is
|
||||
unfetchable AND last-accepted is either unreadable or the same id — a local storage/index
|
||||
condition, not a steering choice. It is **NOT a regression from this release**: the identical
|
||||
line appears on binaries built long before it — mainnet luxd-1 on **v1.36.2** (2,223
|
||||
occurrences, and that is the one mainnet node still producing) and testnet luxd-3 on
|
||||
**v1.36.24** (35,693). It is also survivable: the stalled node keeps VOTING, so quorum holds
|
||||
and the chain keeps finalizing (devnet reached 1913 with two nodes frozen), and devnet luxd-1
|
||||
self-recovered from it once (1609 → 1634). Left unfixed and unchained-to — it needs its own
|
||||
diagnosis at the proposervm layer.
|
||||
|
||||
**Quorum arithmetic (reported, not changed).** Devnet and testnet both run
|
||||
`--consensus-sample-size=5 --consensus-quorum-size=4`. With only 3 live C-Chains the
|
||||
quorum is arithmetically unreachable; devnet demonstrated both directions in one session
|
||||
(pinned at 1378 on 3 live, 1378→1395→1396 the moment luxd-4 restored a 4th). This is a
|
||||
config value, not a code defect, and lowering it lowers the safety margin — left as is.
|
||||
|
||||
### Testnet 96368 roll (15:32–16:00Z) — accepted, after two blockers the devnet roll never hit
|
||||
|
||||
Testnet was frozen at **1779** with only three live C-Chains, below `--consensus-quorum-size=4`
|
||||
(`ceil(2·5/3)+1`), so no block could be accepted. luxd-1 and luxd-4 had no C-Chain at all —
|
||||
`/v1/bc/C/rpc` 404 — on `failed to repair accepted chain by height: proposervm finality index
|
||||
(height 1453 / 1463) is BEHIND the inner VM tip (height 1491)`. v1.36.35 turns that fatal into
|
||||
a repair, and it worked on the first boot of each: `proposervm finality index REBUILT from the
|
||||
local block store — index and inner tip agree fromHeight=1453 toHeight=1491`. The freeze broke
|
||||
the instant a **fourth** C-Chain came up. Final: five nodes on v1.36.35, every binary
|
||||
self-reporting `luxd/1.36.35`, `1779 → 1888` and climbing, tips in exact agreement, and one
|
||||
real transaction (`0x333d5bbd…`, block `0x728`) returning `status=0x1` with **identical
|
||||
blockHash `0x07fd0c68…` and `gasUsed=0x5208` on all five nodes**. α was NOT lowered.
|
||||
|
||||
Two defects had to be fixed first. Both are invisible on devnet and both apply to any fleet.
|
||||
|
||||
**1. The RLP startup import is fatal on re-run — it kills the C-Chain on EVERY boot.**
|
||||
Testnet's startup script passes `--import-chain-data` on every boot, relying on
|
||||
`isNothingToImportError` to make re-importing an already-imported chain a no-op. That guard
|
||||
only ever existed on `luxfi/evm` `hotfix/v1.104.9` (commit `c58d307e`, tags
|
||||
`v1.104.9-hotfix.2/3/4`); `git merge-base --is-ancestor c58d307e main` = **false**. Only the
|
||||
*other* half of that commit was forward-ported. So images built from evm main die with
|
||||
`startup import failed: no blocks imported (parsed=0)`. Proven from the two plugin binaries
|
||||
in-cluster, with a positive control:
|
||||
|
||||
| string in `plugins/mgj786NP7…` | v1.36.24 | v1.36.35 |
|
||||
|---|---|---|
|
||||
| `ImportChain: resuming from current head` | 1 | 1 ← control |
|
||||
| `nothing to import` | 1 | **0** ← the guard |
|
||||
| `no blocks imported (parsed=` | 1 | 1 |
|
||||
|
||||
Fixed **twice, on purpose**: the guard is restored in `luxfi/evm` main (with
|
||||
`startup_import_idempotency_test.go` locking the contract), and the flag is now gated on a
|
||||
per-PVC sentinel in `universe/k8s/lux-testnet/luxd-startup.yaml` — a completed one-time
|
||||
migration must not re-run forever. Sentinel pre-seeded on all five PVCs before the ConfigMap
|
||||
was patched. **Mainnet is NOT exposed**: its `luxd-startup` ConfigMap (39,719 bytes, contains
|
||||
`consensus-quorum-size` twice as a control) has zero `--import-chain-data` and no `.rlp` on disk.
|
||||
|
||||
**2. 🚨 The `luxfi/vm` map-race fix never reached the C-Chain.** node v1.36.35 bumped
|
||||
`luxfi/vm` to v1.3.3 for it, but the C-Chain is a **plugin built from `luxfi/evm`**, which
|
||||
still pinned **v1.3.1**. Read off the two binaries inside one v1.36.35 pod:
|
||||
|
||||
```
|
||||
/luxd/build/luxd github.com/luxfi/vm@v1.3.3
|
||||
/luxd/build/plugins/mgj786NP7… github.com/luxfi/vm@v1.3.1 ← verifies the blocks
|
||||
```
|
||||
|
||||
It fired on testnet luxd-0 five minutes after the roll: `fatal error: concurrent map writes`
|
||||
in `luxfi/vm@v1.3.1/components/chain/block.go:44 (*BlockWrapper).Verify` via
|
||||
`rpc/vm_server_zap.go:580 handleBlockVerify`. v1.3.3 is precisely the fix for that line — it
|
||||
takes `state.blocksLock` around `verifiedBlocks[blkID] = bw`, which v1.3.1 wrote unlocked from
|
||||
every concurrent ZAP RPC handler. `luxfi/evm` main is now on v1.3.3; **the next node image
|
||||
must be built after that bump, or this race ships again.**
|
||||
|
||||
⚠️ **The readiness probe cannot see this.** The plugin dies while luxd survives, so the pod
|
||||
stays `ready=true`, `restarts=0`, and `info.isBootstrapped(C)` keeps answering `true` while
|
||||
`eth_blockNumber` times out — a dead C-Chain still in the Service, the exact failure the probe
|
||||
was redesigned to catch. Only a per-node **tip** probe sees it. Recovery is a pod delete.
|
||||
|
||||
## v1.36.33 — the build→self-verify-fail→drop loop (devnet 96367 / testnet 96368, 2026-07-28)
|
||||
|
||||
**Symptom.** Every proposer built a block and then rejected the block it had just
|
||||
built, forever: `built block … height=1047` immediately followed by
|
||||
`built block failed verification — dropping error="inner parentID didn't match
|
||||
expected parent"`, 83–456 drops/min per node, accepted tip frozen two heights BELOW
|
||||
what the builder kept proposing (devnet tip 1045, builds 1047).
|
||||
|
||||
**Root cause (one line).** `buildChild` asked the inner VM for a block without first
|
||||
pointing the inner VM at the parent's inner block. The inner VM builds on ITS OWN
|
||||
head (`luxfi/evm` miner reads `bc.CurrentBlock()`); the verify path requires
|
||||
`child.innerBlk.Parent() == parent.innerBlk.ID()`. Two different pointers, one
|
||||
required equal to the other, nothing asserting it. The head drifts on its own:
|
||||
verifying a GOSSIPED block whose parent is the current head optimistically sets the
|
||||
head (`core/blockchain.go writeBlockAndSetHead → newTip → writeCanonicalBlockWithLogs`),
|
||||
and `VM.SetPreference` short-circuits on an unchanged outer preference so it never
|
||||
re-pushes the inner preference to drag the head back. Non-self-correcting by
|
||||
construction.
|
||||
|
||||
**Fix.** `VM.anchorInnerBuildParent` (vms/proposervm/vm.go) — one inner
|
||||
`SetPreference` at the point of use, called from BOTH build delegations
|
||||
(`postForkCommonComponents.buildChild`, `preForkBlock.buildChild`). On a healthy node
|
||||
the inner `setPreference` early-returns on `current.Hash() == block.Hash()`, so it is
|
||||
a lookup and no state change; when it fails, the head is provably not the parent's
|
||||
inner block, so refusing to build beats emitting a block we would drop.
|
||||
Regression proof: `vms/proposervm/build_inner_parent_test.go` (models the three evm
|
||||
head semantics — build-on-head, verify-advances-head, SetPreference-reorgs-head).
|
||||
|
||||
**NOT the cause, measured:** the P-Chain. `info.isBootstrapped{"chain":"P"}` = true on
|
||||
15/15 nodes and `platform.getHeight` = 0 on 15/15 **including mainnet**, whose built
|
||||
blocks also carry `pChainHeight=0`. `bootstrapped.message:["111…LpoYY"]` is the
|
||||
primary-network NET id (`chains/chains.go Nets.Bootstrapping` keys by net), not the
|
||||
P-Chain — the P-Chain's chain id prints `111…P` (`constants.PlatformChainID =
|
||||
ids.PChainID`, while `PrimaryNetworkID = ids.Empty`). The verify path DOES read
|
||||
pChainHeight before the parent check, but only as monotonicity
|
||||
(`childPChainHeight < parentPChainHeight`, and 0 < 0 is false, so it passes); every
|
||||
P-Chain-DEPENDENT validation — epoch, `GetCurrentHeight`, proposer window — is gated
|
||||
behind `consensusState == Ready` and sits AFTER the parent check. So `pChainHeight=0`
|
||||
cannot produce `errInnerParentMismatch`.
|
||||
|
||||
**Sibling failure modes on the same fleets (already fixed in 1.36.32, needs the roll):**
|
||||
outer index BEHIND the inner tip ⇒ `refusing to build`/boot repair
|
||||
(`height_backfill.go`, 7d2f01eb), and preferred-absent-locally ⇒ last-accepted
|
||||
fallback (`vm.go BuildBlock`). All three are the same invariant seen from three sides.
|
||||
|
||||
**Roll surface — measured 2026-07-28T07:5xZ, do not use the LuxNetwork CR.**
|
||||
`lux-operator` and `lux-operator-devnet` are **0/0** in `lux-system`, so nothing
|
||||
reconciles `luxnetworks.lux.cloud/luxd`; its tags are stale garbage (mainnet
|
||||
`v1.34.0`, testnet `v1.32.12` — v1.34.0 exists in no registry). The live image is on
|
||||
the **StatefulSet**, hand-maintained: mainnet `ghcr.io/luxfi/node:v1.36.2`
|
||||
(`kubectl-patch` 07-25T00:27:04Z), devnet `v1.36.25@sha256:ca497eff…`, testnet
|
||||
`v1.36.24@sha256:91e2542b…`, all three `updateStrategy: OnDelete`. Change the image in
|
||||
the universe manifest and apply, then delete one pod. Editing the CR and deleting a pod
|
||||
reboots it on the OLD image.
|
||||
|
||||
**Mainnet 96369 is NOT a clean control — 3 of 5 nodes have Mode B, invisibly.**
|
||||
luxd-0/3/4 `eth_blockNumber` = `0x10c1cf` (1098191, block ts 2026-07-24T15:46:19Z) and
|
||||
have not moved in 3+ days while luxd-1 mines (1098341, tx status 0x1, 07:58Z); luxd-2
|
||||
serves 404 (no C-Chain). Not a fork — 1098191 hashes identically on luxd-0 and luxd-1.
|
||||
3998 of luxd-0's last 4000 log lines are the same `built block … height=1098196`. The
|
||||
drop line is absent because the **binary** lacks it: `grep -c "built block failed
|
||||
verification" /luxd/build/luxd` = **0** on mainnet v1.36.2, **1** on devnet v1.36.25.
|
||||
`/v1/health` says `{"healthy":true,"error":"health reply encode failed"}` on the frozen
|
||||
node and the mining node alike — never gate on it, use tip parity.
|
||||
Two hazards for the roll: the 4 broken mainnet pods are exactly the ones on
|
||||
ControllerRevision `luxd-596857c9d6` (rev 147, kubectl-patch 07-25: GOMEMLIMIT=6GiB,
|
||||
mem limit 16Gi→12Gi, request 4Gi→7Gi) and the one mining node, luxd-1, is the only pod
|
||||
still on rev 144 — **deleting luxd-1 recreates it on the revision every other node
|
||||
broke on**. And `ghcr.io/luxfi/node:v1.36.2` has no git tag at all
|
||||
(`git ls-remote origin refs/tags/v1.36.2` → empty; v1.36.24/25 → present), so what
|
||||
mainnet runs is not reproducible from this repo.
|
||||
|
||||
## Essential Commands
|
||||
|
||||
### Release & build (canonical) — via platform.hanzo.ai, NOT GitHub Actions
|
||||
@@ -230,7 +617,7 @@ Located in `/vms/`:
|
||||
- **platformvm**: Staking, validation, network management
|
||||
- **xvm**: Asset transfers, UTXO model
|
||||
- **dexvm**: DEX with order book, perpetuals, AMM
|
||||
- **thresholdvm**: Threshold MPC and FHE for confidential computing
|
||||
- **mpcvm**: Threshold MPC and FHE for confidential computing
|
||||
- **quantumvm**: PQ consensus coordination (ML-DSA, Corona)
|
||||
- **identityvm**: Decentralized identity (DID, verifiable credentials)
|
||||
- **keyvm**: Post-quantum key management (ML-KEM, ML-DSA)
|
||||
@@ -320,11 +707,11 @@ github.com/luxfi/genesis (JSON config) → github.com/luxfi/node/genesis/build
|
||||
### CGO Dependencies
|
||||
These require CGO for full functionality (graceful fallback when disabled):
|
||||
- `consensus/quasar` - GPU NTT acceleration
|
||||
- `vms/thresholdvm/fhe` - GPU FHE operations
|
||||
- `vms/mpcvm/fhe` - GPU FHE operations
|
||||
- `x/blockdb` - zstd compression
|
||||
|
||||
### FHE (Fully Homomorphic Encryption)
|
||||
Located in `vms/thresholdvm/fhe/`:
|
||||
Located in `vms/mpcvm/fhe/`:
|
||||
- Uses `github.com/luxfi/lattice/multiparty` for DKG
|
||||
- Lattice-based cryptography only (no fallbacks)
|
||||
- Threshold decryption via Warp messaging
|
||||
@@ -639,7 +1026,7 @@ strings.Contains(errStr, "not found") // parent block not in local state
|
||||
### Known CGO Stubs
|
||||
When CGO disabled, these use CPU fallbacks:
|
||||
- `consensus/quasar/gpu_ntt_nocgo.go`
|
||||
- `vms/thresholdvm/fhe/gpu_fhe_nocgo.go`
|
||||
- `vms/mpcvm/fhe/gpu_fhe_nocgo.go`
|
||||
- `vms/zkvm/accel/accel_mlx.go`
|
||||
|
||||
### 8. ZAP CreateHandlers for VM HTTP Endpoints
|
||||
@@ -659,7 +1046,7 @@ When CGO disabled, these use CPU fallbacks:
|
||||
```bash
|
||||
curl -s -X POST -H "Content-Type: application/json" \
|
||||
-d '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}' \
|
||||
http://localhost:9640/ext/bc/C/rpc
|
||||
http://localhost:9640/v1/bc/C/rpc
|
||||
# Returns: {"jsonrpc":"2.0","id":1,"result":"0x17870"}
|
||||
```
|
||||
|
||||
@@ -668,7 +1055,7 @@ curl -s -X POST -H "Content-Type: application/json" \
|
||||
|
||||
**Behavior**:
|
||||
- **GET /**: Returns JSON node information (nodeId, networkId, version, chains, endpoints)
|
||||
- **POST /**: Proxies JSON-RPC requests directly to C-chain `/ext/bc/C/rpc`
|
||||
- **POST /**: Proxies JSON-RPC requests directly to C-chain `/v1/bc/C/rpc`
|
||||
- **OPTIONS /**: Returns CORS preflight headers
|
||||
|
||||
**Files Modified**: `server/http/router.go`, `server/http/server.go`
|
||||
@@ -735,7 +1122,7 @@ if s.validators.NumNets() != 0 {
|
||||
|
||||
**Verification**:
|
||||
```bash
|
||||
curl -s http://localhost:9650/ext/health | jq '.checks.bls'
|
||||
curl -s http://localhost:9650/v1/health | jq '.checks.bls'
|
||||
# Should show: "message": "node has the correct BLS key"
|
||||
```
|
||||
|
||||
@@ -769,7 +1156,7 @@ Testing conducted on a single Lux validator node (testnet mode, macOS):
|
||||
**Benchmark Command:**
|
||||
```bash
|
||||
cd ~/work/lux/benchmarks
|
||||
NODE_ENDPOINT="http://localhost:9640/ext/bc/C/rpc" \
|
||||
NODE_ENDPOINT="http://localhost:9640/v1/bc/C/rpc" \
|
||||
PRIVATE_KEY="<funded_key>" \
|
||||
./bin/bench tps --chains=lux --duration=60s --concurrency=5
|
||||
```
|
||||
@@ -817,4 +1204,42 @@ v2 semantic differences worth knowing (these change wire shape):
|
||||
|
||||
---
|
||||
|
||||
## Housekeeping
|
||||
|
||||
Removed 6 generated write-ups / stale root artifacts (`LAUNCH_CHECKLIST.md`,
|
||||
`rename_app.sh`, `replace_imports.sh`, `gen_zoo_addr` binary, `.ci-status-check.md`,
|
||||
`.ci-trigger`) plus the 73MB `.claude/worktrees/` agent scratch tree. Release and
|
||||
launch state live in this file, `CHANGELOG.md`, `RELEASE.md`; chain IDs/ports in
|
||||
`~/work/lux/universe/NETWORKS.yaml` and `~/work/lux/genesis/configs/`.
|
||||
|
||||
---
|
||||
|
||||
*Last Updated*: 2026-06-06
|
||||
|
||||
## ⛔ v1.36.38 — DO NOT DEPLOY (superseded by v1.36.39)
|
||||
|
||||
`v1.36.38` (commit `bd2edc135f`, chunked ancestry descent) is **tagged but must not be
|
||||
deployed**. The tag is retained for provenance — do not delete it.
|
||||
|
||||
Defects, found in owner review after tagging:
|
||||
|
||||
1. **Bootstrap ancestry responses are not trust-boundary validated.** `nameFrontier`'s chunk
|
||||
loop adds every returned `BlockRef` to the global index before checking that the response
|
||||
contains the requested cursor, forms exactly one contiguous parent path, decreases in height
|
||||
by exactly one, is cycle-free, and carries no duplicate ID with differing metadata. Fix:
|
||||
a `VerifiedAncestryChunk{Root, Blocks, Next, Complete}` validated as a unit *before* any ref
|
||||
reaches the index.
|
||||
|
||||
2. **The traversal deadline is internally inconsistent.** A single `Ancestry` request may take
|
||||
up to 12s while the entire multi-anchor walk is bounded to `bootstrapNamingTimeout = 3s`.
|
||||
A child request must never outlive its parent operation (e.g. 3s per request / 30s per
|
||||
attempt).
|
||||
|
||||
3. **Recovery still has a permanent maximum gap.** `bootstrapMaxNamingDepth = 32768` is a
|
||||
recoverability ceiling, not a per-attempt budget. Replace with `MaxBlocksPerAttempt` /
|
||||
`MaxBytesPerAttempt` / `MaxRequestsPerAttempt` / `AttemptTimeout` plus persisted
|
||||
`NamingProgress{Anchor, Cursor, Traversed, VerifiedRefs}`. On exhaustion: save the cursor and
|
||||
return incomplete, then resume from it. **Never translate "attempt budget exhausted" into
|
||||
`ErrNoBootstrapQuorum`.**
|
||||
|
||||
No image was ever built for this tag, so nothing is running it. Ship the above as `v1.36.39`.
|
||||
|
||||
@@ -7,8 +7,15 @@ CGO_ENABLED ?= 1
|
||||
FIPS_STRICT ?= 0
|
||||
|
||||
# Go 1.26 experimental features:
|
||||
# runtimesecret - zeroes stack/register state after secret.Do() for forward secrecy
|
||||
# runtimesecret - zeroes stack/register state after secret.Do() for forward secrecy.
|
||||
# It SIGSEGVs at startup under the WSL2 kernel (confirmed on go1.26.3 and go1.26.4), so enable
|
||||
# it only off-WSL; forward secrecy stays on for real Linux/macOS/production builds.
|
||||
WSL := $(shell grep -qiE 'microsoft|WSL' /proc/sys/kernel/osrelease 2>/dev/null && echo 1)
|
||||
ifeq ($(WSL),1)
|
||||
GOEXPERIMENT ?= none
|
||||
else
|
||||
GOEXPERIMENT ?= runtimesecret
|
||||
endif
|
||||
export GOEXPERIMENT
|
||||
|
||||
# FIPS 140-3 always enabled (required for blockchain/financial systems)
|
||||
@@ -212,7 +219,7 @@ run-testnet: build-fips init-chains
|
||||
node-status:
|
||||
@echo "$(GREEN)Checking node status...$(NC)"
|
||||
@curl -s -X POST --data '{"jsonrpc":"2.0","id":1,"method":"info.isBootstrapped","params":{}}' \
|
||||
-H 'content-type:application/json;' http://localhost:9630/ext/info | jq
|
||||
-H 'content-type:application/json;' http://localhost:9630/v1/info | jq
|
||||
|
||||
stop-node:
|
||||
@echo "$(YELLOW)Stopping Lux node...$(NC)"
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
Lux Node
|
||||
Copyright (c) 2019-2025 Lux Industries Inc.
|
||||
|
||||
This product includes software from avalanchego by Ava Labs, Inc.
|
||||
(https://github.com/ava-labs/avalanchego), licensed under the BSD 3-Clause
|
||||
License:
|
||||
|
||||
Copyright (C) 2019, Ava Labs, Inc.
|
||||
|
||||
Lux Node is derived from avalanchego. The Lux additions and modifications in
|
||||
this repository are licensed under the BSD 3-Clause License (see the LICENSE
|
||||
file). The BSD 3-Clause terms of the upstream avalanchego code are retained;
|
||||
this NOTICE preserves the required Ava Labs copyright attribution at the
|
||||
repository level.
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
|
||||
Ethereum Virtual Machine support is derived from go-ethereum by The
|
||||
go-ethereum Authors (https://github.com/ethereum/go-ethereum). In this
|
||||
repository that code is NOT vendored in-tree; it is consumed as an external
|
||||
Go module through the Lux fork github.com/luxfi/geth, which retains
|
||||
go-ethereum's original licenses:
|
||||
|
||||
* The go-ethereum library packages are licensed under the GNU Lesser
|
||||
General Public License, version 3 (LGPL-3.0-or-later).
|
||||
* The go-ethereum command-line tools are licensed under the GNU General
|
||||
Public License, version 3 (GPL-3.0).
|
||||
|
||||
Copyright (C) The go-ethereum Authors
|
||||
|
||||
COPYLEFT NOTICE: The LGPL-3.0/GPL-3.0 terms continue to apply to the
|
||||
go-ethereum-derived portions provided via github.com/luxfi/geth, and are NOT
|
||||
superseded by the BSD 3-Clause license of this repository. Distribution of
|
||||
compiled Lux Node binaries must honor LGPL-3.0 for the linked go-ethereum
|
||||
library code (source availability for that code and its modifications, and
|
||||
the ability for users to relink against a modified library). The luxfi/geth
|
||||
source, including Lux modifications, is published at
|
||||
https://github.com/luxfi/geth.
|
||||
+1
-1
@@ -3066,7 +3066,7 @@ This version is backwards compatible to [v1.9.0](https://github.com/luxfi/node/r
|
||||
- Fixed `x/merkledb.ChangeProof#getLargestKey` to correctly handle no changes
|
||||
- Added test for `xvm/txs/executor.SemanticVerifier#verifyFxUsage` with multiple valid fxs
|
||||
- Fixed CPU + bandwidth performance regression during vertex processing
|
||||
- Added example usage of the `/ext/index/X/block` API
|
||||
- Added example usage of the `/v1/index/X/block` API
|
||||
- Reduced the default value of `--consensus-optimal-processing` from `50` to `10`
|
||||
- Updated the year in the license header
|
||||
|
||||
|
||||
+1
-1
@@ -145,7 +145,7 @@ Production implementations live in `lux/crypto/` and `lux/lattice/`. Formal veri
|
||||
| `audits/2025-12-30-other-vms-audit.md` | Secondary VMs |
|
||||
| `audits/2025-12-30-platformvm-audit.md` | PlatformVM (P-Chain) |
|
||||
| `audits/2025-12-30-proposervm-evm-audit.md` | ProposerVM and EVM integration |
|
||||
| `audits/2025-12-30-thresholdvm-audit.md` | ThresholdVM (T-Chain) |
|
||||
| `audits/2025-12-30-mpcvm-audit.md` | ThresholdVM (T-Chain) |
|
||||
| `audits/2025-12-30-warp-audit.md` | Warp cross-chain messaging |
|
||||
| `audits/2025-12-30-zkvm-audit.md` | ZKVM (Z-Chain) |
|
||||
|
||||
|
||||
@@ -73,12 +73,6 @@ tasks:
|
||||
- task: generate-mocks
|
||||
- task: check-clean-branch
|
||||
|
||||
check-generate-protobuf:
|
||||
desc: Checks that generated protobuf is up-to-date (requires a clean git working tree)
|
||||
cmds:
|
||||
- task: generate-protobuf
|
||||
- task: check-clean-branch
|
||||
|
||||
check-go-mod-tidy:
|
||||
desc: Checks that go.mod and go.sum are up-to-date (requires a clean git working tree)
|
||||
cmds:
|
||||
@@ -101,10 +95,6 @@ tasks:
|
||||
- cmd: grep -lr -E '^// Code generated - DO NOT EDIT\.$' tests/load/c | xargs -r rm
|
||||
- cmd: go generate ./tests/load/c/...
|
||||
|
||||
generate-protobuf:
|
||||
desc: Generates protobuf
|
||||
cmd: ./scripts/protobuf_codegen.sh
|
||||
|
||||
ginkgo-build:
|
||||
desc: Runs ginkgo against the current working directory
|
||||
cmd: ./bin/ginkgo build {{.USER_WORKING_DIR}}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package chains
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
version "github.com/luxfi/version"
|
||||
"github.com/luxfi/vm/chain"
|
||||
)
|
||||
|
||||
// recordingConnector is a chain.ChainVM that records the version it is handed on
|
||||
// Connected. Every other method comes from the embedded (nil) interface and is
|
||||
// never invoked by the connect path.
|
||||
type recordingConnector struct {
|
||||
chain.ChainVM
|
||||
|
||||
mu sync.Mutex
|
||||
called bool
|
||||
gotVersion *version.Application
|
||||
}
|
||||
|
||||
func (c *recordingConnector) Connected(_ context.Context, _ ids.NodeID, nodeVersion *chain.VersionInfo) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.called = true
|
||||
c.gotVersion = nodeVersion
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *recordingConnector) Disconnected(context.Context, ids.NodeID) error { return nil }
|
||||
|
||||
// TestBlockHandlerConnectedWithVersionDeliversRealVersion is the regression
|
||||
// guard for RED CRITICAL #1 (C-Chain nil-version panic).
|
||||
//
|
||||
// Before the fix blockHandler.Connected forwarded connector.Connected(ctx,
|
||||
// nodeID, nil) — a hardcoded nil version. proposervm promotes Connected to the
|
||||
// inner C-Chain VM (coreth), whose state-sync peer tracker compares peer
|
||||
// versions; a nil version there dereferences nil (version.Application.Compare)
|
||||
// and PANICS a state-syncing node (a fresh join, or a validator rejoining after
|
||||
// falling behind — the launch's core invariant).
|
||||
//
|
||||
// The fix plumbs the REAL peer version through ConnectedWithVersion to the
|
||||
// connector. This test drives the real blockHandler with a real version and
|
||||
// asserts the connector receives that non-nil version — never nil.
|
||||
func TestBlockHandlerConnectedWithVersionDeliversRealVersion(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
rc := &recordingConnector{}
|
||||
bh := newBlockHandler(
|
||||
nil, // BlockBuilder — unused by the connect path
|
||||
rc, // connector under test
|
||||
log.Noop(), // logger
|
||||
nil, // engine
|
||||
nil, // net
|
||||
nil, // msgCreator
|
||||
ids.Empty, // chainID
|
||||
ids.Empty, // networkID
|
||||
nil, // beacons
|
||||
ids.NodeID{}, // selfNodeID
|
||||
false, // expectsStakedBeacons
|
||||
)
|
||||
|
||||
nodeID := ids.GenerateTestNodeID()
|
||||
peerVersion := &version.Application{Name: "lux", Major: 1, Minor: 36, Patch: 27}
|
||||
|
||||
require.NoError(bh.ConnectedWithVersion(context.Background(), nodeID, peerVersion))
|
||||
|
||||
rc.mu.Lock()
|
||||
defer rc.mu.Unlock()
|
||||
require.True(rc.called, "connector.Connected must be invoked")
|
||||
require.NotNil(rc.gotVersion, "connector must receive a NON-nil version (coreth dereferences it in state-sync)")
|
||||
require.Equal("lux", rc.gotVersion.Name)
|
||||
require.Equal(1, rc.gotVersion.Major)
|
||||
require.Equal(36, rc.gotVersion.Minor)
|
||||
require.Equal(27, rc.gotVersion.Patch)
|
||||
}
|
||||
|
||||
// TestBlockHandlerConnectedDedupsButStillCarriesVersion confirms the version
|
||||
// survives the once-only dedup: the first (versioned) dispatch reaches the
|
||||
// connector; a duplicate is a no-op (not a nil-version overwrite).
|
||||
func TestBlockHandlerConnectedDedupsButStillCarriesVersion(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
rc := &recordingConnector{}
|
||||
bh := newBlockHandler(nil, rc, log.Noop(), nil, nil, nil, ids.Empty, ids.Empty, nil, ids.NodeID{}, false)
|
||||
|
||||
nodeID := ids.GenerateTestNodeID()
|
||||
peerVersion := &version.Application{Name: "lux", Major: 1, Minor: 36, Patch: 27}
|
||||
|
||||
require.NoError(bh.ConnectedWithVersion(context.Background(), nodeID, peerVersion))
|
||||
// A duplicate connect (e.g. dispatched again per tracked network) must be a
|
||||
// no-op — never a second call that could clobber the stored version with nil.
|
||||
require.NoError(bh.ConnectedWithVersion(context.Background(), nodeID, nil))
|
||||
|
||||
rc.mu.Lock()
|
||||
defer rc.mu.Unlock()
|
||||
require.NotNil(rc.gotVersion, "the deduped duplicate must not overwrite the real version with nil")
|
||||
require.Equal(27, rc.gotVersion.Patch)
|
||||
}
|
||||
+112
-23
@@ -57,14 +57,28 @@ const (
|
||||
// tip, so a single (even >⅔-stake) validator cannot alone determine the frontier.
|
||||
// Capped at the beacon-set size for tiny networks.
|
||||
bootstrapMinAgreeingBeacons = 2
|
||||
// bootstrapNamingWindow bounds the ancestry the ANCESTOR-TOLERANT frontier tally fetches
|
||||
// per candidate anchor to resolve the bleeding-edge skew between honest beacons. On a live
|
||||
// chain that skew is tiny (the canary was ONE block: 2 producers at N, 1 at N+1), so a
|
||||
// single 256-block window covers it with enormous margin. A ⅔-common height more than this
|
||||
// far below the highest tip is not a healthy bleeding-edge split — the tally names nothing
|
||||
// (the loop then retries / fails safe), never a wrong block. Matches the consensus descent's
|
||||
// per-round fetch size.
|
||||
// bootstrapNamingWindow is the PER-FETCH ancestry chunk of the ANCESTOR-TOLERANT frontier
|
||||
// tally. Ancestry returns FULL BLOCKS, so one fetch must stay inside a network message —
|
||||
// this is a TRANSPORT bound, matching the consensus descent's per-round fetch size.
|
||||
//
|
||||
// It was previously ALSO doing two other jobs: the total ancestry budget, and a HEALTH
|
||||
// HEURISTIC ("a ⅔-common height more than this far below the highest tip is not a healthy
|
||||
// bleeding-edge split"). That heuristic is true for a LIVE chain and FALSE for a HALTED
|
||||
// one, where the skew between a straggler and a node that ran on alone is arbitrarily large
|
||||
// and perfectly healthy — no fork, just a stopped chain. Conflating the three WEDGED
|
||||
// mainnet 96369: responders were split 1098726 (1 node) / 1098191 (2 nodes), 1098191 IS an
|
||||
// ancestor of 1098726, and the ⅔-of-responder floor would have named it — but the gap was
|
||||
// 535 blocks, the single 256-block fetch never reached down far enough for the high tip to
|
||||
// vouch for it, and the tally named nothing on every retry, forever. Recovery from a halt
|
||||
// is exactly when this path matters most, and it was disabled precisely then.
|
||||
// The total budget is now maxNamingDepth; the health heuristic is gone.
|
||||
bootstrapNamingWindow = 256
|
||||
// bootstrapMaxNamingDepth is the TOTAL ancestry one anchor may be walked down, in
|
||||
// bootstrapNamingWindow-sized chunks. Purely a resource bound (with maxNamingAnchors and
|
||||
// bootstrapNamingTimeout): 32768 covers ~9h of 1s blocks of halt-skew, and the common
|
||||
// healthy case never fetches at all — a single tip clearing the floor takes the exact fast
|
||||
// path with zero fetches. Safety does NOT come from this number; see maxNamingDepth().
|
||||
bootstrapMaxNamingDepth = 32768
|
||||
// maxNamingAnchors bounds how many DISTINCT reported tips the ancestor-tolerant tally will
|
||||
// fetch ancestry for in one round. Honest beacons cluster on a handful of adjacent tips, so
|
||||
// this is never reached in practice; it caps the work a Byzantine swarm reporting many
|
||||
@@ -157,8 +171,27 @@ func (b *blockHandler) sampleAncestorBeacons() (set.Set[ids.NodeID], bool) {
|
||||
if len(connected) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
start := int(b.bsRotor.Add(1)-1) % len(connected)
|
||||
// Prefer beacons that reported an ahead tip in the last frontier round — they HOLD the
|
||||
// ancestry the descent needs, so asking them (rather than a rotated peer that may be at
|
||||
// genesis) is what lets a re-bootstrapping node obtain ancestry even when ≥2 peers are still
|
||||
// at genesis. This is ava's PeerTracker "ask a prover" bias sourced from the frontier replies
|
||||
// we already have (no separate tracker). Fill any remaining slots from the rotated full set so
|
||||
// the sample never shrinks below what blind rotation would pick (defense against a stale/empty
|
||||
// ahead-set). Empty ahead-set ⇒ pure rotation, identical to prior behavior.
|
||||
b.bsMu.Lock()
|
||||
ahead := b.bsAheadBeacons
|
||||
b.bsMu.Unlock()
|
||||
|
||||
sample := set.NewSet[ids.NodeID](bootstrapAncestorSample)
|
||||
for _, id := range connected {
|
||||
if sample.Len() >= bootstrapAncestorSample {
|
||||
break
|
||||
}
|
||||
if ahead != nil && ahead.Contains(id) {
|
||||
sample.Add(id)
|
||||
}
|
||||
}
|
||||
start := int(b.bsRotor.Add(1)-1) % len(connected)
|
||||
for i := 0; i < len(connected) && sample.Len() < bootstrapAncestorSample; i++ {
|
||||
sample.Add(connected[(start+i)%len(connected)])
|
||||
}
|
||||
@@ -182,6 +215,19 @@ func (b *blockHandler) fullyConnectedBeacons(weights map[ids.NodeID]uint64, conn
|
||||
return external > 0 && len(connected) >= external
|
||||
}
|
||||
|
||||
// hasExternalBeacons reports whether the staked beacon set contains any validator OTHER than this
|
||||
// node. False for a self-only (single-validator) set — there is then no peer to sync from, so the
|
||||
// node's own tip IS the frontier (FrontierTip returns FrontierNoBeacons). The set is read from
|
||||
// P-chain STATE, not connectivity, so this is a TOPOLOGY fact (a genuine single-validator net),
|
||||
// never an eclipse artifact (an eclipse drops peers from `connected`, never from `weights`).
|
||||
func (b *blockHandler) hasExternalBeacons(weights map[ids.NodeID]uint64) bool {
|
||||
external := len(weights)
|
||||
if _, selfIsBeacon := weights[b.selfNodeID]; selfIsBeacon {
|
||||
external--
|
||||
}
|
||||
return external > 0
|
||||
}
|
||||
|
||||
// withSelfVote returns `replies` plus the node's OWN accepted frontier as a beacon reply — the
|
||||
// SELF-VOTE. The node is itself a beacon (selfNodeID in `weights`, the trust anchor) and knows its
|
||||
// own accepted tip (lastID) with certainty, so it vouches for it exactly as a connected peer's reply
|
||||
@@ -292,6 +338,21 @@ func (b *blockHandler) FrontierTip(ctx context.Context) (ids.ID, chainbootstrap.
|
||||
return ids.Empty, chainbootstrap.FrontierConnecting
|
||||
}
|
||||
|
||||
// SELF-ONLY staked set: the configured validator set is exactly THIS node — a genuine
|
||||
// single-validator network (a sybil-protected devnet, or the sole validator of an L1). There
|
||||
// is no OTHER beacon to sync from, so the node's own accepted tip IS the network frontier →
|
||||
// immediate start (FrontierNoBeacons). This is NOT an eclipse: the staked set is read from
|
||||
// P-chain STATE, not connectivity, so a hidden peer would still appear in `weights`; only a
|
||||
// genuine single-validator topology reaches here. Placed AFTER the P-ready gate so a boot-race
|
||||
// partial set (transiently self-only mid-P-replay) WAITS above rather than false-completing here.
|
||||
// This is the sybil-protected single-validator counterpart to the empty-set FrontierNoBeacons
|
||||
// path: a multi-validator staked set (the ≥2 case) still runs the ⅔-by-stake quorum below.
|
||||
if !b.hasExternalBeacons(weights) {
|
||||
b.logger.Debug("bootstrap frontier: self-only staked set (single validator) — NoBeacons, nothing to sync to",
|
||||
log.Stringer("chainID", b.chainID))
|
||||
return ids.Empty, chainbootstrap.FrontierNoBeacons
|
||||
}
|
||||
|
||||
// THE MASS-RECOVERY FIX. The acceptance decision is the BootstrapPolicy (a SEPARATE object
|
||||
// with a SEPARATE threat model — bootstrap_trust.go), NOT the ⅔-of-CURRENT-total-stake
|
||||
// consensus floor. The prior code required a ⅔-by-stake quorum of the WHOLE validator set to
|
||||
@@ -318,7 +379,7 @@ func (b *blockHandler) FrontierTip(ctx context.Context) (ids.ID, chainbootstrap.
|
||||
switch {
|
||||
case err == nil:
|
||||
// A configured-beacon quorum named a safe sync anchor (NOT a finality cert — the loop
|
||||
// re-executes the descent and re-enters consensus, where ConsensusQuorum alone governs).
|
||||
// re-executes the descent and re-enters consensus, where FinalityQuorum alone governs).
|
||||
// With the P-ready gate above, this judgement is over the TRUE FULL staked set, so a tip the
|
||||
// quorum actively names is a real frontier (when it equals our own held tip, a ⅔-of-responders
|
||||
// supermajority is AT our height, so we ARE at the network frontier — the loop's Accepted()-shortcut
|
||||
@@ -538,6 +599,17 @@ func (b *blockHandler) collectFrontierReplies(ctx context.Context, connected []i
|
||||
|
||||
replies := make([]BeaconReply, 0, len(connected))
|
||||
seen := make(map[ids.NodeID]struct{}, len(connected))
|
||||
// ahead = beacons reporting a tip this node has NOT accepted (they hold blocks we lack, so
|
||||
// they can SERVE the ancestry the descent needs). sampleAncestorBeacons prefers them so the
|
||||
// GetAncestors sample targets peers that can serve — never a peer still at genesis (our own
|
||||
// tip). Recorded per round; publishes into b.bsAheadBeacons before returning.
|
||||
ahead := set.NewSet[ids.NodeID](len(connected))
|
||||
record := func() []BeaconReply {
|
||||
b.bsMu.Lock()
|
||||
b.bsAheadBeacons = ahead
|
||||
b.bsMu.Unlock()
|
||||
return replies
|
||||
}
|
||||
deadline := time.After(bootstrapFrontierWindow)
|
||||
for {
|
||||
select {
|
||||
@@ -551,13 +623,16 @@ func (b *blockHandler) collectFrontierReplies(ctx context.Context, connected []i
|
||||
}
|
||||
seen[rep.nodeID] = struct{}{}
|
||||
replies = append(replies, BeaconReply{NodeID: rep.nodeID, Tip: rep.tip, Weight: w})
|
||||
if _, accepted := b.acceptedHeight(rep.tip); !accepted {
|
||||
ahead.Add(rep.nodeID) // reported a tip we have not accepted → genuinely ahead
|
||||
}
|
||||
if len(seen) >= len(connected) {
|
||||
return replies // every connected beacon answered — resolve now, do not wait the window
|
||||
return record() // every connected beacon answered — resolve now, do not wait the window
|
||||
}
|
||||
case <-deadline:
|
||||
return replies
|
||||
return record()
|
||||
case <-ctx.Done():
|
||||
return replies
|
||||
return record()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -581,7 +656,15 @@ func (b *blockHandler) Ancestors(ctx context.Context, blockID ids.ID, maxBlocks
|
||||
requestID := b.requestIDCounter
|
||||
b.contextRequestMu.Unlock()
|
||||
|
||||
ch := make(chan [][]byte, 1)
|
||||
// Buffer the whole sample so EVERY sampled beacon's reply can queue — the loop
|
||||
// then skips the EMPTY ones (a beacon that lacks the requested block, e.g. a peer
|
||||
// still at genesis) and returns the first NON-EMPTY batch. With a size-1 channel a
|
||||
// fast empty reply won the race and starved a slower peer that actually held the
|
||||
// ancestry, so a re-bootstrapping node with ≥2 peers at genesis could keep drawing
|
||||
// empties and stall. This mirrors the proven avalanchego contract: an empty Ancestors
|
||||
// reply means "this peer can't serve — take another's," never "done" (getter serves an
|
||||
// EXPLICIT empty batch when it lacks the block; see GetContext).
|
||||
ch := make(chan [][]byte, bootstrapAncestorSample)
|
||||
b.bsMu.Lock()
|
||||
b.bsAncestorCh[requestID] = ch
|
||||
b.bsMu.Unlock()
|
||||
@@ -597,13 +680,19 @@ func (b *blockHandler) Ancestors(ctx context.Context, blockID ids.ID, maxBlocks
|
||||
}
|
||||
b.net.Send(msg, sample, b.networkID, 0)
|
||||
|
||||
select {
|
||||
case blocks := <-ch:
|
||||
return blocks, nil
|
||||
case <-time.After(bootstrapAncestorsTimeout):
|
||||
return nil, nil
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
deadline := time.After(bootstrapAncestorsTimeout)
|
||||
for {
|
||||
select {
|
||||
case blocks := <-ch:
|
||||
if len(blocks) == 0 {
|
||||
continue // this beacon can't serve the block — wait for a peer that can
|
||||
}
|
||||
return blocks, nil
|
||||
case <-deadline:
|
||||
return nil, nil // no beacon in the sample served — the loop re-samples (rotated)
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -816,7 +905,7 @@ func (b *blockHandler) BootstrapFailure() error {
|
||||
// is correctly failing safe DOWN (VM in Bootstrapping, serving nothing as head) and waiting for the
|
||||
// quorum to return; the network cannot make progress without it. monitorBootstrap's no-progress
|
||||
// watchdog polls this so it does NOT force-STOP a node that is deliberately waiting — which, given
|
||||
// the K8s probes only poll the always-green /ext/health/liveness, would be a permanent brick. It is
|
||||
// the K8s probes only poll the always-green /v1/health/liveness, would be a permanent brick. It is
|
||||
// the discriminator between "stuck on a served gap" (a real stall → stop) and "waiting for the
|
||||
// quorum" (self-heal → keep waiting). Distinct from BootstrapFailed (a terminal/structural fail).
|
||||
func (b *blockHandler) BootstrapConnecting() bool { return b.bootstrapConnecting.Load() }
|
||||
@@ -921,7 +1010,7 @@ func (b *blockHandler) runBootstrapThenPoll(ctx context.Context) {
|
||||
// no-progress watchdog treats it as a deliberate WAIT, not a stall: the node stays in Bootstrapping
|
||||
// (serving nothing as head, NEVER live at the stale height) and CONVERGES the instant the quorum
|
||||
// returns — the in-process self-heal the K8s probes do NOT provide (they poll the always-green
|
||||
// /ext/health/liveness, so a fail-safe-DOWN node is never restarted). A STRUCTURAL failure (deep gap
|
||||
// /v1/health/liveness, so a fail-safe-DOWN node is never restarted). A STRUCTURAL failure (deep gap
|
||||
// → state-sync) or an exhausted attempt bound returns false WITHOUT going Ready, bootstrapFailed
|
||||
// recording the reason so monitorBootstrap surfaces it. The node NEVER false-completes at its stale
|
||||
// height, and a transient outage NEVER becomes a permanent brick.
|
||||
@@ -963,7 +1052,7 @@ func (b *blockHandler) runInitialSync(ctx context.Context) bool {
|
||||
// (eclipse / partition / a majority co-restart still in flight) is RE-ATTEMPTED — the node stays
|
||||
// in Bootstrapping (engine alive, VM serving nothing as head, never live at the stale height)
|
||||
// and CONVERGES the instant the quorum returns. This is the recovery the K8s probes do NOT
|
||||
// provide (they all poll the always-green /ext/health/liveness, so a fail-safe-DOWN node is
|
||||
// provide (they all poll the always-green /v1/health/liveness, so a fail-safe-DOWN node is
|
||||
// never restarted). bootstrapMaxAttempts ≤ 0 ⇒ retry until the quorum returns or shutdown; a
|
||||
// test pins it to 1 to assert the single-attempt terminal fail-safe. A STRUCTURAL failure (deep
|
||||
// gap → state-sync) is NOT retried — a retry cannot fix it; it is surfaced for the operator.
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
consensuschain "github.com/luxfi/consensus/engine/chain"
|
||||
cblock "github.com/luxfi/consensus/engine/chain/block"
|
||||
chainbootstrap "github.com/luxfi/consensus/engine/chain/bootstrap"
|
||||
"github.com/luxfi/constants"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/math/set"
|
||||
@@ -217,6 +218,13 @@ type bsBeaconNet struct {
|
||||
serveAncestors bool // beacons serve ancestry (false models name-only beacons)
|
||||
ancestorsEmpty bool // beacons REPLY to GetAncestors but serve an EMPTY batch (cross-version / withholding)
|
||||
|
||||
// emptyResponders models a MIXED fleet on a WIPE-path recovery: the named beacons REPLY to
|
||||
// GetAncestors with an EMPTY batch (they lack the block — e.g. still at genesis after a
|
||||
// simultaneous wipe), while the rest serve the real ancestry. When set, the mock delivers the
|
||||
// empties FIRST, proving the fetcher (blockHandler.Ancestors) skips them and still returns the
|
||||
// non-empty batch — never starved by a peer that can't serve. Requires serveAncestors=true.
|
||||
emptyResponders set.Set[ids.NodeID]
|
||||
|
||||
// tipFor optionally overrides the tip a specific beacon reports (models DISAGREEMENT —
|
||||
// beacons connected but split across tips, so no ⅔ quorum forms → FrontierNoQuorum).
|
||||
tipFor map[ids.NodeID]ids.ID
|
||||
@@ -334,6 +342,23 @@ func (n *bsBeaconNet) Send(msg message.OutboundMessage, nodeIDs set.Set[ids.Node
|
||||
n.bh.deliverBootstrapFrontier(n.malicious, n.forgedTip)
|
||||
}
|
||||
case "ancestors":
|
||||
if n.emptyResponders != nil {
|
||||
// Mixed fleet: emptyResponders reply EMPTY (they lack the block), the rest serve.
|
||||
// Deliver the empties FIRST so the test proves Ancestors skips them and returns the
|
||||
// slower non-empty batch (the WIPE-path ≥2-at-genesis case).
|
||||
var servers []ids.NodeID
|
||||
for id := range nodeIDs {
|
||||
if n.emptyResponders.Contains(id) {
|
||||
n.bh.deliverBootstrapAncestors(m.requestID, nil)
|
||||
} else {
|
||||
servers = append(servers, id)
|
||||
}
|
||||
}
|
||||
for range servers {
|
||||
n.bh.deliverBootstrapAncestors(m.requestID, n.frame(m.blockID))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if n.serveAncestors {
|
||||
n.bh.deliverBootstrapAncestors(m.requestID, n.frame(m.blockID))
|
||||
} else if n.ancestorsEmpty {
|
||||
@@ -512,6 +537,66 @@ func TestNodeBootstrap_EmptyNodeConvergesViaTransport(t *testing.T) {
|
||||
require.True(t, bh.Accepted(ctx, chain[N].id), "node must hold the tip after sync")
|
||||
}
|
||||
|
||||
// TestNodeBootstrap_WipePath_MixedGenesisPeers_ObtainsAncestry is the regression guard for the
|
||||
// WIPE-path ≥2-at-genesis stall (deliverable 3). A re-bootstrapping node samples a fleet where
|
||||
// SOME beacons reply to GetAncestors with an EMPTY batch (they lack the block — still at genesis
|
||||
// after a simultaneous wipe) while the rest serve the real ancestry, and the empties arrive FIRST.
|
||||
// With the pre-fix size-1 reply channel + first-reply-wins, the empty reply won the race and the
|
||||
// good peer's non-empty batch was dropped, so the descent got nothing every round and stalled. The
|
||||
// fix buffers the whole sample and SKIPS empty batches, returning the first non-empty one — so the
|
||||
// node obtains ancestry from the peers that CAN serve, even when ≥2 peers are at genesis.
|
||||
func TestNodeBootstrap_WipePath_MixedGenesisPeers_ObtainsAncestry(t *testing.T) {
|
||||
const N = 30
|
||||
chain, byID := buildBSChain(N, -1)
|
||||
vm := newBSVM(chain)
|
||||
bh, chainID, beacons := newBSHandlerAndEngine(t, vm, 5)
|
||||
|
||||
// 2 of 5 beacons are "still at genesis" — they reply EMPTY to GetAncestors. The mock
|
||||
// delivers those empties FIRST so a first-reply-wins fetcher would be starved.
|
||||
empties := set.NewSet[ids.NodeID](2)
|
||||
empties.Add(beacons[0], beacons[1])
|
||||
bh.net = &bsBeaconNet{
|
||||
bh: bh, chainID: chainID, connected: beacons, byID: byID, tip: chain[N],
|
||||
serveAncestors: true, emptyResponders: empties,
|
||||
}
|
||||
bh.msgCreator = bsMsgBuilder{}
|
||||
|
||||
ctx := context.Background()
|
||||
require.NoError(t, runBS(t, bh), "node must converge despite ≥2 peers serving empty ancestry")
|
||||
|
||||
last, _ := vm.LastAccepted(ctx)
|
||||
require.Equal(t, chain[N].id, last, "node must sync to the tip via the peers that CAN serve")
|
||||
require.True(t, bh.Accepted(ctx, chain[N].id))
|
||||
}
|
||||
|
||||
// TestSampleAncestorBeacons_PrefersAheadBeacons proves change 2: the ancestry sample PREFERS
|
||||
// beacons the frontier round found genuinely AHEAD (they hold the ancestry), so a re-bootstrapping
|
||||
// node asks peers that can serve rather than wasting the bounded sample on genesis peers. With more
|
||||
// connected beacons than the sample size, blind rotation would periodically EXCLUDE any given
|
||||
// beacon; the preference guarantees the recorded ahead-beacon is ALWAYS sampled.
|
||||
func TestSampleAncestorBeacons_PrefersAheadBeacons(t *testing.T) {
|
||||
const numBeacons = 8 // > bootstrapAncestorSample (4), so rotation alone would sometimes miss one
|
||||
chain, byID := buildBSChain(1, -1)
|
||||
vm := newBSVM(chain)
|
||||
bh, chainID, beacons := newBSHandlerAndEngine(t, vm, numBeacons)
|
||||
bh.net = &bsBeaconNet{bh: bh, chainID: chainID, connected: beacons, byID: byID, tip: chain[1]}
|
||||
bh.msgCreator = bsMsgBuilder{}
|
||||
|
||||
// Record ONE beacon as ahead (the frontier round's output). It must appear in EVERY sample.
|
||||
ahead := beacons[numBeacons-1]
|
||||
bh.bsMu.Lock()
|
||||
bh.bsAheadBeacons = set.Of(ahead)
|
||||
bh.bsMu.Unlock()
|
||||
|
||||
for i := 0; i < 2*numBeacons; i++ {
|
||||
sample, ok := bh.sampleAncestorBeacons()
|
||||
require.True(t, ok)
|
||||
require.LessOrEqual(t, sample.Len(), bootstrapAncestorSample)
|
||||
require.True(t, sample.Contains(ahead),
|
||||
"the recorded ahead-beacon must be preferred into every ancestry sample (call %d)", i)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRED_FrozenVMLastAccepted_ConvergesOffFinalizedLedger is the regression guard for red
|
||||
// HIGH-1: the convergence-recognition must ride the IN-PROCESS consensus finalized ledger, NOT
|
||||
// the VM's LastAccepted cache — which the real ZAP client FREEZES at the boot snapshot for the
|
||||
@@ -775,9 +860,9 @@ func TestNodeBootstrap_NoBeaconSet_ReportsNoBeacons(t *testing.T) {
|
||||
func TestNodeBootstrap_FreshNet_SelfVoteUnderFullConnectivity_CaughtUp(t *testing.T) {
|
||||
const N = 5
|
||||
chain, byID := buildBSChain(N, -1)
|
||||
vm := newBSVM(chain) // the node is at genesis (lastAccepted = genesis)
|
||||
vm := newBSVM(chain) // the node is at genesis (lastAccepted = genesis)
|
||||
bh, chainID, beacons := newBSHandlerAndEngine(t, vm, 2) // 2 equal-weight (100) beacons
|
||||
bh.selfNodeID = beacons[0] // THIS node is beacon 0 — it is itself a validator
|
||||
bh.selfNodeID = beacons[0] // THIS node is beacon 0 — it is itself a validator
|
||||
bh.msgCreator = bsMsgBuilder{}
|
||||
|
||||
// FULL connectivity: the ONE other beacon is connected and reports GENESIS (a fresh net — every
|
||||
@@ -824,7 +909,7 @@ func TestNodeBootstrap_SelfVote_PeerAheadDefeatsCaughtUp(t *testing.T) {
|
||||
func TestNodeBootstrap_SelfVote_PartialConnectivityFailsSafe(t *testing.T) {
|
||||
const N = 5
|
||||
chain, byID := buildBSChain(N, -1)
|
||||
vm := newBSVM(chain) // node at genesis
|
||||
vm := newBSVM(chain) // node at genesis
|
||||
bh, chainID, beacons := newBSHandlerAndEngine(t, vm, 3) // self + 2 others
|
||||
bh.selfNodeID = beacons[0]
|
||||
bh.msgCreator = bsMsgBuilder{}
|
||||
@@ -1528,7 +1613,7 @@ func TestRED_TipHolderCoRestartGoesReadyAtOwnTip(t *testing.T) {
|
||||
// It must NOT go live at its stale height (safety) and must NOT permanently give up (liveness): it
|
||||
// stays in Bootstrapping, RE-ATTEMPTS (bootstrapMaxAttempts ≤ 0 ⇒ until the quorum returns), and
|
||||
// CONVERGES the instant the quorum comes back — all IN-PROCESS, with no pod restart (the K8s probes
|
||||
// poll the always-green /ext/health/liveness and would never restart it). This is the bounded
|
||||
// poll the always-green /v1/health/liveness and would never restart it). This is the bounded
|
||||
// re-bootstrap retry that closes the "permanent brick" RED flagged.
|
||||
func TestRED_MajorityOutageSelfHealsWhenQuorumReturns(t *testing.T) {
|
||||
const N, K = 30, 16 // stale at N; the live frontier (once the quorum returns) is N+K
|
||||
@@ -1852,8 +1937,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()
|
||||
|
||||
@@ -1948,3 +2033,129 @@ func TestRED_BehindNode_PeerConnectDelay_NeverFalseCompletesAtStale(t *testing.T
|
||||
require.Greater(t, net.peerInfoCalls, net.connectAfterCalls,
|
||||
"the loop must have WAITED through the connecting passes before naming the frontier")
|
||||
}
|
||||
|
||||
// TestChainExpectsStakedBeacons_SybilDrivesNotSkipBootstrap pins the ROOT-CAUSE decision of the
|
||||
// rejoin wedge (tasks #66/#74): whether a chain syncs its bootstrap frontier against the STAKED
|
||||
// primary-network set is driven by SYBIL PROTECTION, NOT --skip-bootstrap. Production validators
|
||||
// set --skip-bootstrap (to skip the initial bootstrap WAIT); the prior `!m.SkipBootstrap && ...`
|
||||
// made that flag also EMPTY the beacon set, so a behind native chain (C/X/Q) reported
|
||||
// FrontierNoBeacons, named its STALE local tip the network frontier, went live there, and never
|
||||
// caught up across restarts until a manual chaindata wipe. skip-bootstrap is not even an input to
|
||||
// the fixed decision — that is the point.
|
||||
func TestChainExpectsStakedBeacons_SybilDrivesNotSkipBootstrap(t *testing.T) {
|
||||
require.True(t, chainExpectsStakedBeacons(true, true, false),
|
||||
"sybil-protected native non-platform chain (C/X/Q) MUST expect staked beacons → peer-sync a behind validator (regardless of skip-bootstrap)")
|
||||
require.False(t, chainExpectsStakedBeacons(false, true, false),
|
||||
"non-sybil (dev / single-node) native chain → NO staked beacons: the empty-beacon immediate-start path")
|
||||
require.False(t, chainExpectsStakedBeacons(true, false, false),
|
||||
"a non-native chain does not sync against the primary staked set here")
|
||||
require.False(t, chainExpectsStakedBeacons(true, true, true),
|
||||
"the platform chain anchors to its OWN CustomBeacons, never the staked-set frontier quorum")
|
||||
}
|
||||
|
||||
// TestChainValidatesOnPrimaryNetwork_RealHashChainID is the regression the hardcoded-bool test
|
||||
// above could NOT catch: it exercises the ACTUAL discriminator buildChain feeds into
|
||||
// chainExpectsStakedBeacons. The durable rejoin fix (#66/#74) shipped INERT in production because
|
||||
// the discriminator keyed off the blockchain ID via ids.IsNativeChain — false for every deployed
|
||||
// C/X/Q (they carry HASH blockchain IDs; ids.IsNativeChain only matches the symbolic 111...C alias
|
||||
// form, which no deployed chain uses). So a real behind C-Chain still got empty beacons under
|
||||
// --skip-bootstrap and wedged. The fix keys off the validating Net (chainParams.ChainID) instead.
|
||||
func TestChainValidatesOnPrimaryNetwork_RealHashChainID(t *testing.T) {
|
||||
// Real deployed C-Chain blockchain IDs (devnet + mainnet) are HASHES, and ids.IsNativeChain
|
||||
// is false for them — the exact trap that disabled the fix.
|
||||
for _, s := range []string{
|
||||
"21HieZngQW8unBnSTbdQ9PcPAz6hhPLGrPzZhTvjC8KgjE95Bg", // devnet C-Chain
|
||||
"2wRdZGeca1qkxzNCq88NWDF5nJ5A9o623vRJKd3FsjRYvuVvvt", // mainnet C-Chain
|
||||
} {
|
||||
id, err := ids.FromString(s)
|
||||
require.NoError(t, err)
|
||||
require.False(t, ids.IsNativeChain(id),
|
||||
"trap: ids.IsNativeChain is false for real hash C-Chain ID %s — do NOT discriminate on the blockchain ID", s)
|
||||
}
|
||||
|
||||
// C/X/Q validate on the PRIMARY NETWORK: chainParams.ChainID == PrimaryNetworkID. That is the
|
||||
// signal buildChain now passes, so the durable fix fires for the real (hash-ID) C-Chain.
|
||||
require.True(t, chainValidatesOnPrimaryNetwork(constants.PrimaryNetworkID))
|
||||
require.True(t, chainExpectsStakedBeacons(true, chainValidatesOnPrimaryNetwork(constants.PrimaryNetworkID), false),
|
||||
"a sybil-protected primary-network non-platform chain (C/X/Q) MUST expect staked beacons regardless of its hash blockchain ID")
|
||||
|
||||
// An L2 validates on its OWN sovereign net, not the primary network → stays on the
|
||||
// empty-beacon single-node path (behavior unchanged for L2s).
|
||||
l2Net := ids.GenerateTestID()
|
||||
require.False(t, chainValidatesOnPrimaryNetwork(l2Net))
|
||||
require.False(t, chainExpectsStakedBeacons(true, chainValidatesOnPrimaryNetwork(l2Net), false))
|
||||
}
|
||||
|
||||
// TestNodeBootstrap_SelfOnlyStakedSet_ReportsNoBeacons covers the single-VALIDATOR counterpart of
|
||||
// the fix: a sybil-protected chain whose staked set is EXACTLY this node (a single-validator
|
||||
// devnet, or the sole validator of an L1) has no OTHER beacon to sync from, so FrontierTip reports
|
||||
// FrontierNoBeacons (immediate start at the node's own tip) rather than hanging in FrontierConnecting.
|
||||
// This is what lets skip-bootstrap stop emptying native beacons WITHOUT bricking a single-validator
|
||||
// net. It is NOT an eclipse: the staked set is read from P-chain STATE (hasExternalBeacons), so a
|
||||
// hidden peer would still appear in `weights`.
|
||||
func TestNodeBootstrap_SelfOnlyStakedSet_ReportsNoBeacons(t *testing.T) {
|
||||
const N = 10
|
||||
chain, byID := buildBSChain(N, -1)
|
||||
vm := newBSVMAt(chain, N) // the sole validator IS at the frontier
|
||||
self := ids.GenerateTestNodeID()
|
||||
bh, chainID := newBSHandlerWeighted(t, vm, map[ids.NodeID]uint64{self: 100})
|
||||
bh.selfNodeID = self // the staked set is EXACTLY this node — a genuine single-validator net
|
||||
bh.net = &bsBeaconNet{bh: bh, chainID: chainID, byID: byID, tip: chain[N]}
|
||||
bh.msgCreator = bsMsgBuilder{}
|
||||
|
||||
bh.bsActive.Store(true)
|
||||
tip, status := bh.FrontierTip(context.Background())
|
||||
bh.bsActive.Store(false)
|
||||
require.Equal(t, chainbootstrap.FrontierNoBeacons, status,
|
||||
"self-only staked set (single validator) → NoBeacons (nothing to sync to), never a FrontierConnecting hang")
|
||||
require.Equal(t, ids.Empty, tip)
|
||||
|
||||
// Discriminator: add ONE other validator and the SAME node must now run the quorum (has a peer
|
||||
// to sync from) — proving the self-only shortcut fires ONLY for a genuinely alone validator.
|
||||
require.True(t, bh.hasExternalBeacons(map[ids.NodeID]uint64{self: 100, ids.GenerateTestNodeID(): 100}),
|
||||
"a ≥2-validator staked set has an external beacon → runs the ⅔-by-stake quorum, not the self-only shortcut")
|
||||
}
|
||||
|
||||
// TestNodeBootstrap_BehindValidator_StakedSet_CatchesUpNoWipe is THE REJOIN INVARIANT (tasks
|
||||
// #66/#74), the code reproduction of the mainnet luxd-0 wedge: a staked validator that fell behind
|
||||
// must sync from its peers on restart and reach the network frontier WITHOUT a chaindata wipe.
|
||||
//
|
||||
// The node reloads its STALE on-disk tip at height M (the "restart" — no wipe) and holds a
|
||||
// sybil-protected native-chain staked beacon set (expectsStakedBeacons=true — exactly what
|
||||
// buildChain now leaves in place under --skip-bootstrap, instead of emptying it). The 4 healthy
|
||||
// producers name+serve the frontier N over the real GetAcceptedFrontier/GetAncestors transport, so
|
||||
// the behind node fetch+executes M+1..N and ends ACCEPTED at N — never stuck at the stale M.
|
||||
func TestNodeBootstrap_BehindValidator_StakedSet_CatchesUpNoWipe(t *testing.T) {
|
||||
const N = 60 // network frontier (the healthy producers)
|
||||
const M = 42 // our STALE local height after a restart — behind by N-M, NO wipe
|
||||
chain, byID := buildBSChain(N, -1)
|
||||
vm := newBSVMAt(chain, M) // reload the stale on-disk tip (the restart)
|
||||
|
||||
// A 5-validator staked set (equal stake): this node is one, the other 4 are the connected,
|
||||
// serving producers at the frontier N. This mirrors lux-mainnet's 5-validator C-Chain.
|
||||
weights := map[ids.NodeID]uint64{}
|
||||
producers := make([]ids.NodeID, 4)
|
||||
for i := range producers {
|
||||
producers[i] = ids.GenerateTestNodeID()
|
||||
weights[producers[i]] = 100
|
||||
}
|
||||
self := ids.GenerateTestNodeID()
|
||||
weights[self] = 100
|
||||
bh, chainID := newBSHandlerWeighted(t, vm, weights)
|
||||
bh.selfNodeID = self
|
||||
require.True(t, bh.expectsStakedBeacons,
|
||||
"a native sybil chain expects staked beacons even under skip-bootstrap — the fix that keeps peer-sync alive")
|
||||
|
||||
bh.net = &bsBeaconNet{bh: bh, chainID: chainID, connected: producers, byID: byID, tip: chain[N], serveAncestors: true}
|
||||
bh.msgCreator = bsMsgBuilder{}
|
||||
|
||||
ctx := context.Background()
|
||||
require.NoError(t, runBS(t, bh),
|
||||
"behind validator must converge to the frontier from its peers — no wipe, chain keeps finalizing")
|
||||
|
||||
last, _ := vm.LastAccepted(ctx)
|
||||
require.Equal(t, chain[N].id, last,
|
||||
"REJOIN: behind validator caught up from peers to frontier N=%d (was stuck at stale M=%d, no wipe)", N, M)
|
||||
require.True(t, bh.Accepted(ctx, chain[N].id),
|
||||
"node holds + ACCEPTED the frontier tip after the rejoin (genuine catch-up, not a stale false-complete)")
|
||||
}
|
||||
|
||||
+115
-21
@@ -12,7 +12,7 @@
|
||||
// to recover from. Bootstrap trust was braided into consensus finality, and finality's ⅔ rule
|
||||
// is mathematically unsatisfiable during a mass outage.
|
||||
//
|
||||
// The fix is a type split, NOT a renamed threshold. ConsensusQuorum decides FINALITY
|
||||
// The fix is a type split, NOT a renamed threshold. FinalityQuorum decides FINALITY
|
||||
// (> ⅔ of CURRENT stake — UNCHANGED). BootstrapTrust decides whether a fetched frontier is
|
||||
// SAFE TO BEGIN SYNC FROM: a quorum of AUTHENTICATED CONFIGURED beacons that RESPOND, gated by
|
||||
// a response FLOOR (MinResponses) and an agreement threshold over the RESPONDERS (not over the
|
||||
@@ -24,6 +24,7 @@ package chains
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
@@ -37,30 +38,30 @@ import (
|
||||
|
||||
// BootstrapTrust is not a consensus-finality oracle.
|
||||
// It selects a weak-subjective sync frontier from authenticated configured beacons.
|
||||
// Live block acceptance remains governed exclusively by ConsensusQuorum.
|
||||
// Live block acceptance remains governed exclusively by FinalityQuorum.
|
||||
type BootstrapTrust interface {
|
||||
// AcceptsFrontier returns the block an empty/behind node may BEGIN SYNCING FROM, selected
|
||||
// from the authenticated configured beacons' frontier replies — or an error
|
||||
// (ErrInsufficientBootstrapResponses / ErrNoBootstrapQuorum) when no trusted frontier can be
|
||||
// named this round. The returned Frontier is a sync ANCHOR, never a consensus certificate
|
||||
// (see the type comment): the node must still re-execute every block it descends to before
|
||||
// re-entering live consensus, where ConsensusQuorum alone governs acceptance.
|
||||
// re-entering live consensus, where FinalityQuorum alone governs acceptance.
|
||||
AcceptsFrontier(ctx context.Context, replies []BeaconReply) (*Frontier, error)
|
||||
}
|
||||
|
||||
// ConsensusQuorum decides FINALITY: whether a weight is a finalizing supermajority (> ⅔) of the
|
||||
// FinalityQuorum decides FINALITY: whether a weight is a finalizing supermajority (> ⅔) of the
|
||||
// CURRENT validator set. This is the live-consensus rule; bootstrap does NOT change it. It is a
|
||||
// SEPARATE named type from BootstrapTrust precisely so the distinction is explicit and testable:
|
||||
// a frontier that AcceptsFrontier admits is "safe to sync from", and in general it does NOT
|
||||
// satisfy HasFinality (3 of 5 responders is a valid sync anchor; 3 of 5 stake is not finality).
|
||||
type ConsensusQuorum interface {
|
||||
type FinalityQuorum interface {
|
||||
HasFinality(weight, total StakeWeight) bool
|
||||
}
|
||||
|
||||
// StakeWeight is validator stake in the units the validator manager reports (Weight/Light).
|
||||
type StakeWeight = uint64
|
||||
|
||||
// twoThirdsFinality is the production ConsensusQuorum: > ⅔ of the CURRENT total stake, exactly
|
||||
// twoThirdsFinality is the production FinalityQuorum: > ⅔ of the CURRENT total stake, exactly
|
||||
// the rule the live cert-gate uses (consensusconfig.TwoThirdsStakeFloor). Defined here only to
|
||||
// give the live rule a name to CONTRAST bootstrap trust against — it is not wired into the live
|
||||
// path (that already enforces ⅔ inside consensus), and bootstrap never calls it to ACCEPT.
|
||||
@@ -70,10 +71,10 @@ func (twoThirdsFinality) HasFinality(weight, total StakeWeight) bool {
|
||||
return weight > consensusconfig.TwoThirdsStakeFloor(total)
|
||||
}
|
||||
|
||||
// DefaultConsensusQuorum returns the live ⅔-of-current-stake finality rule — the thing bootstrap
|
||||
// DefaultFinalityQuorum returns the live ⅔-of-current-stake finality rule — the thing bootstrap
|
||||
// trust is explicitly NOT. Used by the test suite to prove a bootstrap-accepted frontier does
|
||||
// not constitute finality.
|
||||
func DefaultConsensusQuorum() ConsensusQuorum { return twoThirdsFinality{} }
|
||||
func DefaultFinalityQuorum() FinalityQuorum { return twoThirdsFinality{} }
|
||||
|
||||
var (
|
||||
// ErrInsufficientBootstrapResponses: fewer than MinResponses configured beacons answered.
|
||||
@@ -127,9 +128,48 @@ type AncestrySource interface {
|
||||
// Checkpoint is an operator-pinned (id, height) the recovering node may anchor to when too few
|
||||
// beacons respond to form a quorum — the EXPLICIT override for INVARIANT 2's "1 of N reachable"
|
||||
// case. Absent (nil) ⇒ the default policy REJECTS rather than trusting a captured minority.
|
||||
//
|
||||
// INVARIANT 4 (a checkpoint is a SIGNED weak-subjectivity anchor, not a bare config value): the
|
||||
// checkpoint carries a cryptographic Signature by the configured checkpoint AUTHORITY over its
|
||||
// (id, height), and AcceptsFrontier trusts it ONLY when CheckpointVerifier authenticates that
|
||||
// signature. A (id,height) present in a flag/config but UNSIGNED — or signed by a non-authority key
|
||||
// — is REJECTED (fail closed). This is the crucial hardening: the checkpoint is the one path that
|
||||
// bypasses the beacon quorum, so a compromised flag must NOT be able to inject a false sync anchor
|
||||
// without ALSO forging the authority's signature. It is a cryptographic vouch, NEVER a ⅔-live-stake
|
||||
// tally (that conflation is the very deadlock BootstrapTrust exists to avoid).
|
||||
type Checkpoint struct {
|
||||
ID ids.ID
|
||||
Height uint64
|
||||
// Signature is the checkpoint authority's signature over this checkpoint's canonical (id,height)
|
||||
// bytes. Verified by CheckpointVerifier before the anchor is trusted; an empty signature is
|
||||
// never accepted.
|
||||
Signature []byte
|
||||
}
|
||||
|
||||
// CheckpointVerifier authenticates a Checkpoint's Signature against the configured checkpoint
|
||||
// AUTHORITY key(s). The node injects a real implementation backed by a PROVEN primitive (Ed25519 /
|
||||
// BLS — never custom crypto); the policy stays free of any crypto dependency, exactly like
|
||||
// AncestrySource and heightOf. A nil verifier means no signed anchor is configured, so any
|
||||
// Checkpoint is untrusted and the below-floor case fails closed.
|
||||
type CheckpointVerifier interface {
|
||||
// VerifyCheckpoint reports whether sig is a valid signature over (id, height) by the configured
|
||||
// checkpoint authority. It MUST reject an empty signature and be signature-safe (constant-time
|
||||
// compare on the primitive). It is the sole authority on whether a pinned anchor may be trusted.
|
||||
VerifyCheckpoint(id ids.ID, height uint64, sig []byte) bool
|
||||
}
|
||||
|
||||
// CanonicalCheckpointMessage is the exact byte string a checkpoint authority signs and
|
||||
// CheckpointVerifier authenticates: a domain-separated, fixed-layout encoding of (id, height) so a
|
||||
// signature can never be transplanted from another context. 8-byte big-endian height after the
|
||||
// 32-byte id, under a distinct domain tag.
|
||||
func CanonicalCheckpointMessage(id ids.ID, height uint64) []byte {
|
||||
const domain = "lux-bootstrap-checkpoint-v1\x00"
|
||||
msg := make([]byte, 0, len(domain)+len(id)+8)
|
||||
msg = append(msg, domain...)
|
||||
msg = append(msg, id[:]...)
|
||||
var h [8]byte
|
||||
binary.BigEndian.PutUint64(h[:], height)
|
||||
return append(msg, h[:]...)
|
||||
}
|
||||
|
||||
// Ratio is an exact rational threshold (e.g. 2/3, 3/4). A value clears it iff
|
||||
@@ -152,7 +192,7 @@ func (r Ratio) floorOf(whole uint64) uint64 {
|
||||
}
|
||||
|
||||
// BootstrapPolicy is the default BootstrapTrust: a CONFIGURED-BEACON quorum with a response
|
||||
// FLOOR and an agreement threshold over the RESPONDERS — a SEPARATE object from ConsensusQuorum
|
||||
// FLOOR and an agreement threshold over the RESPONDERS — a SEPARATE object from FinalityQuorum
|
||||
// with a SEPARATE threat model. It does NOT pass "reachable stake" into the ⅔-of-current-stake
|
||||
// finality rule (that conflation IS the mass-recovery deadlock). It reuses the ancestor-tolerant
|
||||
// common-ancestor tally only for HOW to find the agreed frontier; the ACCEPTANCE gate is the
|
||||
@@ -197,12 +237,19 @@ type BootstrapPolicy struct {
|
||||
// network, or a fleet unanimously AT the tip).
|
||||
MinFrontierHeight uint64
|
||||
// Checkpoint is the OPTIONAL operator override for the below-floor case (INVARIANT 2). nil ⇒
|
||||
// reject below the floor.
|
||||
// reject below the floor. When set, it is trusted ONLY if CheckpointVerifier authenticates its
|
||||
// signature (INVARIANT 4).
|
||||
Checkpoint *Checkpoint
|
||||
// NamingWindow bounds the ancestry fetched per anchor; MaxAnchors bounds how many distinct
|
||||
// reported tips are resolved. Both default to the package constants when zero.
|
||||
NamingWindow int
|
||||
MaxAnchors int
|
||||
// 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 is the per-FETCH ancestry chunk (Ancestry returns FULL blocks, so one fetch
|
||||
// must stay inside a network message); MaxNamingDepth is the TOTAL ancestry a single anchor
|
||||
// may be walked down in NamingWindow-sized chunks; MaxAnchors bounds how many distinct
|
||||
// reported tips are resolved. All default to the package constants when zero.
|
||||
NamingWindow int
|
||||
MaxNamingDepth int
|
||||
MaxAnchors int
|
||||
// NamingTimeout TOTAL-bounds the ancestor-tolerant resolution (all anchor fetches combined) so
|
||||
// a partition that ANSWERS the frontier query but WITHHOLDS ancestry cannot make the decision
|
||||
// hang — it returns what it found (or nothing → ErrNoBootstrapQuorum) and the caller's bounded
|
||||
@@ -255,6 +302,17 @@ func (p *BootstrapPolicy) namingWindow() int {
|
||||
return bootstrapNamingWindow
|
||||
}
|
||||
|
||||
// maxNamingDepth is the TOTAL ancestry one anchor may be walked down, in namingWindow-sized
|
||||
// chunks. It is a RESOURCE bound and nothing else. Safety comes from hash-verified ancestry
|
||||
// (a forged chain cannot link), MinResponders distinct voters, the ⅔-of-RESPONDER-stake floor,
|
||||
// and the full re-Verify every block gets on the descent — none of which depend on this number.
|
||||
func (p *BootstrapPolicy) maxNamingDepth() int {
|
||||
if p.MaxNamingDepth > 0 {
|
||||
return p.MaxNamingDepth
|
||||
}
|
||||
return bootstrapMaxNamingDepth
|
||||
}
|
||||
|
||||
func (p *BootstrapPolicy) maxAnchors() int {
|
||||
if p.MaxAnchors > 0 {
|
||||
return p.MaxAnchors
|
||||
@@ -330,6 +388,15 @@ func (p *BootstrapPolicy) AcceptsFrontier(ctx context.Context, replies []BeaconR
|
||||
// operator explicitly pinned a checkpoint to anchor from.
|
||||
if !p.floorMet(responders, responderWeight) {
|
||||
if p.Checkpoint != nil {
|
||||
// INVARIANT 4: the checkpoint bypasses the beacon quorum, so trust it ONLY when the
|
||||
// configured authority SIGNED this exact (id,height). A checkpoint present in config but
|
||||
// unsigned, or signed by a non-authority key, is REJECTED (fail closed) — a compromised
|
||||
// flag cannot inject a false sync anchor without also forging the authority's signature.
|
||||
if p.CheckpointVerifier == nil || len(p.Checkpoint.Signature) == 0 ||
|
||||
!p.CheckpointVerifier.VerifyCheckpoint(p.Checkpoint.ID, p.Checkpoint.Height, p.Checkpoint.Signature) {
|
||||
return nil, fmt.Errorf("%w: a checkpoint is pinned but its authority signature did not verify",
|
||||
ErrInsufficientBootstrapResponses)
|
||||
}
|
||||
return &Frontier{
|
||||
ID: p.Checkpoint.ID,
|
||||
Height: p.Checkpoint.Height,
|
||||
@@ -452,14 +519,41 @@ func (p *BootstrapPolicy) nameFrontier(ctx context.Context, stakeOnTip map[ids.I
|
||||
break
|
||||
}
|
||||
fetches++
|
||||
refs, err := p.Source.Ancestry(ctx, tip, p.namingWindow())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, ref := range refs {
|
||||
if _, ok := index[ref.ID]; !ok {
|
||||
index[ref.ID] = ref
|
||||
// CHUNKED DESCENT. namingWindow is the per-FETCH size (Ancestry returns FULL blocks, so
|
||||
// one fetch must fit a network message) — NOT the total ancestry worth walking. Keep
|
||||
// following the parent chain in window-sized chunks up to maxNamingDepth, so a fleet
|
||||
// that HALTED with a straggler far below the highest tip still resolves its ⅔-backed
|
||||
// common ancestor. A single un-chunked window silently capped this at 256 and wedged
|
||||
// mainnet at a 535-block gap. ctx already TOTAL-bounds every fetch (namingTimeout).
|
||||
cur := tip
|
||||
for depth := 0; depth < p.maxNamingDepth(); {
|
||||
// The deadline must bind the WALK, not just each fetch: a Byzantine peer serving a
|
||||
// long fabricated chain cheaply (or an in-process Source) would otherwise run the
|
||||
// full depth budget before anyone checked the clock.
|
||||
if ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
refs, err := p.Source.Ancestry(ctx, cur, p.namingWindow())
|
||||
if err != nil || len(refs) == 0 {
|
||||
break
|
||||
}
|
||||
deepest, first := BlockRef{}, true
|
||||
for _, ref := range refs {
|
||||
if _, ok := index[ref.ID]; !ok {
|
||||
index[ref.ID] = ref
|
||||
}
|
||||
if first || ref.Height < deepest.Height {
|
||||
deepest, first = ref, false
|
||||
}
|
||||
}
|
||||
depth += len(refs) // len(refs) >= 1 here, so depth strictly advances -> terminates
|
||||
if deepest.Parent == ids.Empty {
|
||||
break // reached genesis
|
||||
}
|
||||
if _, covered := index[deepest.Parent]; covered {
|
||||
break // an earlier anchor's chain already covers everything below
|
||||
}
|
||||
cur = deepest.Parent
|
||||
}
|
||||
}
|
||||
if len(index) == 0 {
|
||||
|
||||
+272
-12
@@ -16,6 +16,7 @@ package chains
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -302,7 +303,7 @@ func TestBootstrapTrust_F_SplitReachableAncestrySelectsCommonAncestor(t *testing
|
||||
|
||||
// TestBootstrapTrust_G_FinalityUnchanged proves INVARIANT 3: a bootstrap-accepted frontier is NOT
|
||||
// finality. The SAME 3-of-5 support that AcceptsFrontier admits as a sync anchor does NOT satisfy
|
||||
// ConsensusQuorum.HasFinality — live block acceptance still requires > ⅔ of CURRENT validator
|
||||
// FinalityQuorum.HasFinality — live block acceptance still requires > ⅔ of CURRENT validator
|
||||
// stake (4 of 5 here). The bootstrap quorum cannot finalize a block.
|
||||
func TestBootstrapTrust_G_FinalityUnchanged(t *testing.T) {
|
||||
const w uint64 = 100
|
||||
@@ -321,39 +322,76 @@ func TestBootstrapTrust_G_FinalityUnchanged(t *testing.T) {
|
||||
require.Equal(t, frontier, f.ID)
|
||||
require.Equal(t, StakeWeight(3*w), f.Weight, "the frontier is backed by exactly the 3 responders")
|
||||
|
||||
// ConsensusQuorum says that SAME 3-of-5 weight is NOT finality — the decisions are different
|
||||
// FinalityQuorum says that SAME 3-of-5 weight is NOT finality — the decisions are different
|
||||
// objects with different thresholds. Finality is unchanged: it still needs > ⅔ (4 of 5).
|
||||
cq := DefaultConsensusQuorum()
|
||||
cq := DefaultFinalityQuorum()
|
||||
require.False(t, cq.HasFinality(3*w, total),
|
||||
"INVARIANT 3: a bootstrap-accepted frontier (3 of 5) is NOT a finalizing supermajority")
|
||||
require.True(t, cq.HasFinality(4*w, total),
|
||||
"finality UNCHANGED: > ⅔ of current stake (4 of 5) still finalizes")
|
||||
require.False(t, cq.HasFinality(f.Weight, total),
|
||||
"the bootstrap quorum's own backing weight cannot finalize a block")
|
||||
|
||||
// AFTER-SYNC (the owner's "bootstrap is not a finality bypass"): the node has now SYNCED to the
|
||||
// frontier via BootstrapTrust and re-entered live consensus. A NEW block that collects the SAME
|
||||
// 3-of-5 stake STILL does not finalize — bootstrap admitted a sync ANCHOR, it did not lower the
|
||||
// finality bar. Live acceptance returns to strict > ⅔ of CURRENT stake, exactly as before any
|
||||
// bootstrap. HasFinality is stateless in the bootstrap outcome, which is the whole point: there
|
||||
// is no code path by which "we bootstrapped from 3/5" leaks into the finality decision.
|
||||
require.False(t, cq.HasFinality(3*w, total),
|
||||
"AFTER syncing from a 3-of-5 bootstrap frontier, live finality STILL needs > ⅔ (4 of 5) — no bypass")
|
||||
}
|
||||
|
||||
// ----- checkpoint override (complements B) ----------------------------------
|
||||
|
||||
// edCheckpointAuthority is a test checkpoint authority backed by Ed25519 — a PROVEN primitive, no
|
||||
// custom crypto. It signs a checkpoint's canonical (id,height) message and verifies against its own
|
||||
// public key, rejecting an empty signature and any key that is not the configured authority.
|
||||
type edCheckpointAuthority struct {
|
||||
priv ed25519.PrivateKey
|
||||
pub ed25519.PublicKey
|
||||
}
|
||||
|
||||
func newEdCheckpointAuthority(t *testing.T) *edCheckpointAuthority {
|
||||
t.Helper()
|
||||
pub, priv, err := ed25519.GenerateKey(nil)
|
||||
require.NoError(t, err)
|
||||
return &edCheckpointAuthority{priv: priv, pub: pub}
|
||||
}
|
||||
|
||||
func (a *edCheckpointAuthority) sign(id ids.ID, height uint64) []byte {
|
||||
return ed25519.Sign(a.priv, CanonicalCheckpointMessage(id, height))
|
||||
}
|
||||
|
||||
// VerifyCheckpoint implements CheckpointVerifier: authenticate against the authority's public key.
|
||||
func (a *edCheckpointAuthority) VerifyCheckpoint(id ids.ID, height uint64, sig []byte) bool {
|
||||
return len(sig) != 0 && ed25519.Verify(a.pub, CanonicalCheckpointMessage(id, height), sig)
|
||||
}
|
||||
|
||||
// TestBootstrapTrust_CheckpointOverride: below the response floor (1 of 5), the DEFAULT is reject
|
||||
// (test B), but an operator who pins a checkpoint gets the explicit override — the node anchors to
|
||||
// the pinned (id,height) instead of trusting the lone beacon. This is the sanctioned escape hatch
|
||||
// for a deeply-partitioned node, NEVER an open-ended ≥1-beacon acceptance.
|
||||
// (test B), but an operator who pins a SIGNED checkpoint gets the explicit override — the node
|
||||
// anchors to the authenticated (id,height) instead of trusting the lone beacon. This is the
|
||||
// sanctioned escape hatch for a deeply-partitioned node, NEVER an open-ended ≥1-beacon acceptance,
|
||||
// and (INVARIANT 4) NEVER a bare unsigned config value.
|
||||
func TestBootstrapTrust_CheckpointOverride(t *testing.T) {
|
||||
beacons := nodeIDs(5)
|
||||
authority := newEdCheckpointAuthority(t)
|
||||
ckptID := ids.GenerateTestID()
|
||||
const ckptHeight = uint64(1_082_796)
|
||||
policy := &BootstrapPolicy{
|
||||
TrustedBeacons: equalBeacons(beacons, equalStake),
|
||||
MinResponses: 3,
|
||||
Checkpoint: &Checkpoint{ID: ckptID, Height: 1_082_796},
|
||||
TrustedBeacons: equalBeacons(beacons, equalStake),
|
||||
MinResponses: 3,
|
||||
Checkpoint: &Checkpoint{ID: ckptID, Height: ckptHeight, Signature: authority.sign(ckptID, ckptHeight)},
|
||||
CheckpointVerifier: authority,
|
||||
}
|
||||
// 1 reachable beacon — below the floor — but a checkpoint is pinned.
|
||||
// 1 reachable beacon — below the floor — but a SIGNED checkpoint is pinned.
|
||||
f, err := policy.AcceptsFrontier(context.Background(), []BeaconReply{
|
||||
reply(beacons[0], ids.GenerateTestID(), equalStake),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.True(t, f.FromCheckpoint, "below the floor with a pinned checkpoint → anchor to the checkpoint")
|
||||
require.True(t, f.FromCheckpoint, "below the floor with a SIGNED checkpoint → anchor to the checkpoint")
|
||||
require.Equal(t, ckptID, f.ID)
|
||||
require.Equal(t, uint64(1_082_796), f.Height)
|
||||
require.Equal(t, ckptHeight, f.Height)
|
||||
|
||||
// Without the checkpoint the same 1-of-5 is rejected (the default — never trust the lone beacon).
|
||||
policy.Checkpoint = nil
|
||||
@@ -363,6 +401,56 @@ func TestBootstrapTrust_CheckpointOverride(t *testing.T) {
|
||||
require.ErrorIs(t, err, ErrInsufficientBootstrapResponses)
|
||||
}
|
||||
|
||||
// TestBootstrapTrust_CheckpointMustBeSigned is INVARIANT 4: a checkpoint that is present but not
|
||||
// AUTHENTICATED is REJECTED (fail closed). A compromised flag/config that pins a false (id,height)
|
||||
// cannot inject a sync anchor without the authority's signature. Four rejection modes, one accept.
|
||||
func TestBootstrapTrust_CheckpointMustBeSigned(t *testing.T) {
|
||||
beacons := nodeIDs(5)
|
||||
authority := newEdCheckpointAuthority(t)
|
||||
attacker := newEdCheckpointAuthority(t) // a DIFFERENT key — not the configured authority
|
||||
ckptID := ids.GenerateTestID()
|
||||
const h = uint64(500_000)
|
||||
lone := []BeaconReply{reply(beacons[0], ids.GenerateTestID(), equalStake)} // 1-of-5, below floor
|
||||
|
||||
base := func() *BootstrapPolicy {
|
||||
return &BootstrapPolicy{TrustedBeacons: equalBeacons(beacons, equalStake), MinResponses: 3, CheckpointVerifier: authority}
|
||||
}
|
||||
|
||||
// (1) UNSIGNED checkpoint (empty signature) → rejected even with a verifier wired.
|
||||
p := base()
|
||||
p.Checkpoint = &Checkpoint{ID: ckptID, Height: h}
|
||||
_, err := p.AcceptsFrontier(context.Background(), lone)
|
||||
require.ErrorIs(t, err, ErrInsufficientBootstrapResponses, "an UNSIGNED checkpoint must be rejected")
|
||||
|
||||
// (2) signed by a NON-AUTHORITY (attacker) key → rejected.
|
||||
p = base()
|
||||
p.Checkpoint = &Checkpoint{ID: ckptID, Height: h, Signature: attacker.sign(ckptID, h)}
|
||||
_, err = p.AcceptsFrontier(context.Background(), lone)
|
||||
require.ErrorIs(t, err, ErrInsufficientBootstrapResponses, "a checkpoint signed by a non-authority key must be rejected")
|
||||
|
||||
// (3) authority signature over a DIFFERENT (id,height) — replay onto a forged anchor → rejected.
|
||||
p = base()
|
||||
forgedID := ids.GenerateTestID()
|
||||
p.Checkpoint = &Checkpoint{ID: forgedID, Height: h, Signature: authority.sign(ckptID, h)} // sig binds ckptID, not forgedID
|
||||
_, err = p.AcceptsFrontier(context.Background(), lone)
|
||||
require.ErrorIs(t, err, ErrInsufficientBootstrapResponses, "a signature transplanted to a different (id,height) must be rejected")
|
||||
|
||||
// (4) NO verifier configured → any checkpoint is untrusted (fail closed).
|
||||
p = base()
|
||||
p.CheckpointVerifier = nil
|
||||
p.Checkpoint = &Checkpoint{ID: ckptID, Height: h, Signature: authority.sign(ckptID, h)}
|
||||
_, err = p.AcceptsFrontier(context.Background(), lone)
|
||||
require.ErrorIs(t, err, ErrInsufficientBootstrapResponses, "no verifier ⇒ even a validly-signed checkpoint is untrusted")
|
||||
|
||||
// (accept) authority signs the exact pinned (id,height) → trusted.
|
||||
p = base()
|
||||
p.Checkpoint = &Checkpoint{ID: ckptID, Height: h, Signature: authority.sign(ckptID, h)}
|
||||
f, err := p.AcceptsFrontier(context.Background(), lone)
|
||||
require.NoError(t, err)
|
||||
require.True(t, f.FromCheckpoint)
|
||||
require.Equal(t, ckptID, f.ID)
|
||||
}
|
||||
|
||||
// ----- safety guard for the global ancestor-tolerant tally ------------------
|
||||
|
||||
// TestBootstrapTrust_ForkAtSharedGenesisFailsSafe is the load-bearing guard for the
|
||||
@@ -828,3 +916,175 @@ func TestBootstrapTrust_EclipseOwnHeightNotNamedRoutesToCaughtUp(t *testing.T) {
|
||||
require.Equal(t, refs[N].ID, f.ID, "boundary: N is named iff its height is STRICTLY ABOVE MinFrontierHeight")
|
||||
require.Equal(t, uint64(N), f.Height)
|
||||
}
|
||||
|
||||
// ----- H: HALT-SKEW RECOVERY (mainnet 96369 wedge) ---------------------------
|
||||
|
||||
// TestBootstrapTrust_H_HaltSkewDeeperThanOneWindow reproduces the live mainnet 96369 wedge and
|
||||
// proves the chunked descent fixes it.
|
||||
//
|
||||
// SHAPE (measured per-node, in-pod, 2026-07-28): the fleet HALTED below its α=4-of-5 threshold.
|
||||
// One node ran on alone to 1098726 while two stayed at 1098191; 1098191 IS an ancestor of
|
||||
// 1098726 (no fork — luxd-3/luxd-4's `latest` IS 1098191, they hold nothing competing), so the
|
||||
// ⅔-of-RESPONDER floor is satisfied at 1098191 by all three responders and it MUST be named.
|
||||
//
|
||||
// It was not. nameFrontier fetched one bootstrapNamingWindow (256) of ancestry per anchor, and
|
||||
// the gap is 535 — so the high tip's ancestry never reached down far enough to vouch for the
|
||||
// common block. 1098191 held only its 2 direct responders (2 of 3 = below the ⅔ floor of 2,
|
||||
// which the strict `>` rejects), the tally named NOTHING, and every retry did the same. The
|
||||
// node sat at height 0 forever and mainnet could not regain quorum.
|
||||
//
|
||||
// The gap here (535) is deliberately > one window (256) and < maxNamingDepth. With the
|
||||
// single-fetch window this FAILS (frontier is nil → ErrNoBootstrapQuorum); with the chunked
|
||||
// descent the walk continues past the first chunk and names the common ancestor.
|
||||
func TestBootstrapTrust_H_HaltSkewDeeperThanOneWindow(t *testing.T) {
|
||||
const w uint64 = 100
|
||||
const gap = 535 // luxd-1 1098726 - luxd-3/luxd-4 1098191, the measured mainnet skew
|
||||
|
||||
// common is the last block the whole fleet accepted; `ahead` extends it by `gap`.
|
||||
refs, byID := refChain(1000)
|
||||
common := refs[1000]
|
||||
prev := common
|
||||
for i := 0; i < gap; i++ {
|
||||
c := childRef(prev)
|
||||
byID[c.ID] = c
|
||||
prev = c
|
||||
}
|
||||
ahead := prev
|
||||
require.Equal(t, common.Height+gap, ahead.Height)
|
||||
require.Greater(t, gap, bootstrapNamingWindow, "gap MUST exceed one fetch window or this proves nothing")
|
||||
require.Less(t, gap, bootstrapMaxNamingDepth, "gap must stay inside the total depth budget")
|
||||
|
||||
beacons := nodeIDs(3) // the three responders with a live C-Chain
|
||||
policy := &BootstrapPolicy{
|
||||
TrustedBeacons: equalBeacons(beacons, w),
|
||||
MinResponses: 3,
|
||||
Source: &stubAncestry{byID: byID},
|
||||
}
|
||||
replies := []BeaconReply{
|
||||
reply(beacons[0], ahead.ID, w), // luxd-1, ran on alone
|
||||
reply(beacons[1], common.ID, w), // luxd-3
|
||||
reply(beacons[2], common.ID, w), // luxd-4
|
||||
}
|
||||
|
||||
f, err := policy.AcceptsFrontier(context.Background(), replies)
|
||||
require.NoError(t, err, "a fleet split across ONE chain must name a frontier, not wedge")
|
||||
require.NotNil(t, f)
|
||||
require.Equal(t, common.ID, f.ID,
|
||||
"must name the ⅔-backed COMMON ancestor: the high tip vouches for it via ancestry")
|
||||
require.Equal(t, common.Height, f.Height)
|
||||
}
|
||||
|
||||
// TestBootstrapTrust_H_HaltSkewBeyondDepthStillFailsSafe pins the resource bound's edge: a skew
|
||||
// DEEPER than maxNamingDepth still names nothing rather than guessing. Depth is a work bound, so
|
||||
// exhausting it must degrade to the SAME fail-safe as before, never to a wrong block.
|
||||
func TestBootstrapTrust_H_HaltSkewBeyondDepthStillFailsSafe(t *testing.T) {
|
||||
const w uint64 = 100
|
||||
refs, byID := refChain(10)
|
||||
common := refs[10]
|
||||
prev := common
|
||||
for i := 0; i < 64; i++ {
|
||||
c := childRef(prev)
|
||||
byID[c.ID] = c
|
||||
prev = c
|
||||
}
|
||||
ahead := prev
|
||||
|
||||
beacons := nodeIDs(3)
|
||||
policy := &BootstrapPolicy{
|
||||
TrustedBeacons: equalBeacons(beacons, w),
|
||||
MinResponses: 3,
|
||||
NamingWindow: 4, // tiny chunk...
|
||||
MaxNamingDepth: 8, // ...and a depth budget far shallower than the 64-block skew
|
||||
Source: &stubAncestry{byID: byID},
|
||||
}
|
||||
_, err := policy.AcceptsFrontier(context.Background(), []BeaconReply{
|
||||
reply(beacons[0], ahead.ID, w),
|
||||
reply(beacons[1], common.ID, w),
|
||||
reply(beacons[2], common.ID, w),
|
||||
})
|
||||
require.Error(t, err, "a skew beyond the depth budget must fail SAFE, never name a guess")
|
||||
}
|
||||
|
||||
// TestBootstrapTrust_H_AcceptanceMatrix pins the owner-specified recovery matrix at the policy
|
||||
// layer. Each case is the SAME five-validator mainnet shape with a different responder pattern.
|
||||
//
|
||||
// The rule being pinned is "highest verifiably-vouched descendant wins", NOT "numerically highest
|
||||
// tip advertised". A tip whose ancestry does not link into the fleet's chain earns NO credit no
|
||||
// matter how high it claims to be, so a Byzantine node cannot pull the fleet onto a fabricated
|
||||
// chain by advertising a tall one.
|
||||
func TestBootstrapTrust_H_AcceptanceMatrix(t *testing.T) {
|
||||
const w uint64 = 100
|
||||
mk := func() (BlockRef, BlockRef, map[ids.ID]BlockRef) {
|
||||
refs, byID := refChain(600)
|
||||
common := refs[600]
|
||||
prev := common
|
||||
for i := 0; i < 535; i++ { // the measured mainnet skew, > one 256 window
|
||||
c := childRef(prev)
|
||||
byID[c.ID] = c
|
||||
prev = c
|
||||
}
|
||||
return common, prev, byID
|
||||
}
|
||||
|
||||
t.Run("1_high_2_low_2_unavailable__names_common_ancestor", func(t *testing.T) {
|
||||
common, ahead, byID := mk()
|
||||
b := nodeIDs(3)
|
||||
p := &BootstrapPolicy{TrustedBeacons: equalBeacons(b, w), MinResponses: 3, Source: &stubAncestry{byID: byID}}
|
||||
f, err := p.AcceptsFrontier(context.Background(), []BeaconReply{
|
||||
reply(b[0], ahead.ID, w), reply(b[1], common.ID, w), reply(b[2], common.ID, w)})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, common.ID, f.ID)
|
||||
})
|
||||
|
||||
t.Run("3_high_2_unavailable__names_high_tip", func(t *testing.T) {
|
||||
_, ahead, byID := mk()
|
||||
b := nodeIDs(3)
|
||||
p := &BootstrapPolicy{TrustedBeacons: equalBeacons(b, w), MinResponses: 3, Source: &stubAncestry{byID: byID}}
|
||||
f, err := p.AcceptsFrontier(context.Background(), []BeaconReply{
|
||||
reply(b[0], ahead.ID, w), reply(b[1], ahead.ID, w), reply(b[2], ahead.ID, w)})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ahead.ID, f.ID, "unanimous high tip must be named, not an ancestor")
|
||||
})
|
||||
|
||||
t.Run("fabricated_tall_tip_2_low__rejects_fake_names_common", func(t *testing.T) {
|
||||
common, _, byID := mk()
|
||||
// A Byzantine node advertises a tip on a chain of its own that never links into the
|
||||
// fleet's history. Its ancestry earns credit only for ITS OWN blocks, never for the
|
||||
// fleet's — so it cannot outvote the two honest low responders.
|
||||
fakeRefs, fakeByID := refChain(9_000_000)
|
||||
fake := fakeRefs[9_000_000]
|
||||
for id, r := range fakeByID {
|
||||
byID[id] = r
|
||||
}
|
||||
b := nodeIDs(3)
|
||||
p := &BootstrapPolicy{TrustedBeacons: equalBeacons(b, w), MinResponses: 3, Source: &stubAncestry{byID: byID}}
|
||||
f, err := p.AcceptsFrontier(context.Background(), []BeaconReply{
|
||||
reply(b[0], fake.ID, w), reply(b[1], common.ID, w), reply(b[2], common.ID, w)})
|
||||
// The fake tip earns credit ONLY on its own disjoint chain (100), far below the ⅔ floor
|
||||
// of 200, so it can never be named however tall it claims to be. `common` earns exactly
|
||||
// 200 from its two honest backers — and the floor is STRICT (`> floor`), so 200 is not
|
||||
// enough either: the Byzantine node withheld the third vouch by sitting on a chain that
|
||||
// does not link. Correct outcome is a SAFE HALT, not "fall back to the low tip".
|
||||
// This is the honest BFT trade: 1 of 3 responders can cost LIVENESS, never SAFETY.
|
||||
require.Error(t, err, "must halt safely; must NOT follow a taller unvouched chain")
|
||||
require.Nil(t, f)
|
||||
})
|
||||
|
||||
t.Run("two_conflicting_branches_same_height__halts_safely", func(t *testing.T) {
|
||||
refs, byID := refChain(600)
|
||||
common := refs[600]
|
||||
// Two disjoint branches of equal height off the common block, each backed by one node,
|
||||
// and NO responder majority on either. Nothing may be named by height tiebreak.
|
||||
l, r := childRef(common), childRef(common)
|
||||
byID[l.ID], byID[r.ID] = l, r
|
||||
b := nodeIDs(3)
|
||||
p := &BootstrapPolicy{
|
||||
TrustedBeacons: equalBeacons(b, w), MinResponses: 3,
|
||||
MinFrontierHeight: common.Height, // node already holds `common`; only a tip AHEAD may be named
|
||||
Source: &stubAncestry{byID: byID},
|
||||
}
|
||||
_, err := p.AcceptsFrontier(context.Background(), []BeaconReply{
|
||||
reply(b[0], l.ID, w), reply(b[1], r.ID, w), reply(b[2], common.ID, w)})
|
||||
require.Error(t, err, "conflicting equal-height branches must halt safely, never pick by height")
|
||||
})
|
||||
}
|
||||
|
||||
+35
-4
@@ -46,17 +46,48 @@ func (s *Nets) GetOrCreate(chainID ids.ID) (nets.Net, bool) {
|
||||
return chain, true
|
||||
}
|
||||
|
||||
// IsChainBootstrapped reports whether the given chain has finished initial sync
|
||||
// (reached the network frontier and transitioned its VM to normal operation) on
|
||||
// this node — Bootstrapped(chainID) was called for it in its validation net. A
|
||||
// chain that is merely tracked (its sync goroutine launched) but has NOT converged
|
||||
// reads false. This is the per-chain truth manager.IsBootstrapped / info.isBootstrapped
|
||||
// key on, replacing the mere-existence test that returned true the instant a chain
|
||||
// was added to the manager (the premature-true masking bug: a C-Chain stalled at
|
||||
// genesis reported bootstrapped=true). A chainID is added to exactly one net's
|
||||
// tracking, so the first net that reports it bootstrapped is authoritative.
|
||||
func (s *Nets) IsChainBootstrapped(chainID ids.ID) bool {
|
||||
if s == nil {
|
||||
return false // no net tracking wired ⇒ nothing has been marked bootstrapped
|
||||
}
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
|
||||
for _, chain := range s.chains {
|
||||
if chain.IsChainBootstrapped(chainID) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Bootstrapping returns the chainIDs of any chains that are still
|
||||
// bootstrapping.
|
||||
//
|
||||
// s.chains is keyed by NET id, not chain id. Reporting the key named the net
|
||||
// instead of the chain: every primary-network chain that failed to converge
|
||||
// surfaced in /v1/health as the single ID
|
||||
// "11111111111111111111111111111111LpoYY" — constants.PrimaryNetworkID, i.e.
|
||||
// ids.Empty — a "chain" the chain manager has never heard of, so the operator
|
||||
// chasing it got "there is no chain with alias/ID". Worse, N stuck chains
|
||||
// collapsed into one indistinguishable entry. Ask each net which of ITS chains
|
||||
// are still bootstrapping; the net owns that set.
|
||||
func (s *Nets) Bootstrapping() []ids.ID {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
|
||||
chainsBootstrapping := make([]ids.ID, 0, len(s.chains))
|
||||
for chainID, chain := range s.chains {
|
||||
if !chain.IsBootstrapped() {
|
||||
chainsBootstrapping = append(chainsBootstrapping, chainID)
|
||||
}
|
||||
for _, chain := range s.chains {
|
||||
chainsBootstrapping = append(chainsBootstrapping, chain.Bootstrapping()...)
|
||||
}
|
||||
|
||||
return chainsBootstrapping
|
||||
|
||||
+44
-2
@@ -158,12 +158,54 @@ func TestNetsBootstrapping(t *testing.T) {
|
||||
chain, ok := chains.GetOrCreate(netID)
|
||||
require.True(ok)
|
||||
|
||||
// Start bootstrapping
|
||||
// Start bootstrapping. What comes back is the CHAIN that is syncing, never
|
||||
// the net that holds it — this assertion used to demand netID, which is
|
||||
// how the phantom "11111111111111111111111111111111LpoYY" survived review.
|
||||
chain.AddChain(chainID)
|
||||
bootstrapping := chains.Bootstrapping()
|
||||
require.Contains(bootstrapping, netID)
|
||||
require.Equal([]ids.ID{chainID}, bootstrapping)
|
||||
require.NotContains(bootstrapping, netID)
|
||||
|
||||
// Finish bootstrapping
|
||||
chain.Bootstrapped(chainID)
|
||||
require.Empty(chains.Bootstrapping())
|
||||
}
|
||||
|
||||
// The "bootstrapped" health check publishes Nets.Bootstrapping() verbatim as
|
||||
// its message. s.chains is keyed by NET id, so returning the key reported
|
||||
// constants.PrimaryNetworkID (ids.Empty, cb58
|
||||
// "11111111111111111111111111111111LpoYY") as though it were an unbootstrapped
|
||||
// CHAIN. Operators saw a chain ID the chain manager denies exists, and every
|
||||
// stuck chain on the net collapsed into that one phantom entry.
|
||||
func TestNetsBootstrappingReportsChainsNotNets(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
chains, err := NewNets(ids.EmptyNodeID, map[ids.ID]nets.Config{
|
||||
constants.PrimaryNetworkID: {},
|
||||
})
|
||||
require.NoError(err)
|
||||
|
||||
primary, _ := chains.GetOrCreate(constants.PrimaryNetworkID)
|
||||
cChainID := ids.GenerateTestID()
|
||||
dChainID := ids.GenerateTestID()
|
||||
primary.AddChain(cChainID)
|
||||
primary.AddChain(dChainID)
|
||||
|
||||
bootstrapping := chains.Bootstrapping()
|
||||
|
||||
// The phantom: never the net's own ID.
|
||||
require.NotContains(bootstrapping, constants.PrimaryNetworkID)
|
||||
require.NotContains(
|
||||
bootstrapping,
|
||||
ids.Empty,
|
||||
"health check reported the primary NET id as an unbootstrapped chain",
|
||||
)
|
||||
// Both stuck chains must be individually nameable.
|
||||
require.ElementsMatch([]ids.ID{cChainID, dChainID}, bootstrapping)
|
||||
|
||||
primary.Bootstrapped(cChainID)
|
||||
require.Equal([]ids.ID{dChainID}, chains.Bootstrapping())
|
||||
|
||||
primary.Bootstrapped(dChainID)
|
||||
require.Empty(chains.Bootstrapping())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// context_chunk_test.go — the heavy-block self-heal fix: GetContext must bound its Ancestors
|
||||
// response by SERIALIZED SIZE (not just block COUNT) so it stays under the peer message cap.
|
||||
//
|
||||
// Benchmark-proven live bug: under 250-trader DEX load, heavy blocks made a 256-block context
|
||||
// response sum to 3.4-5.7 MB > the 2 MB compressor cap; msgCreator.Ancestors FAILED to build,
|
||||
// so a behind validator received NOTHING and could never resync (permanently stuck while the tip
|
||||
// advanced). This pins the fix: the response is chunked to fit the budget, always serving at
|
||||
// least one block so the behind node makes progress every round.
|
||||
|
||||
package chains
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
consensuschain "github.com/luxfi/consensus/engine/chain"
|
||||
consensusblock "github.com/luxfi/consensus/engine/chain/block"
|
||||
"github.com/luxfi/constants"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/node/message"
|
||||
)
|
||||
|
||||
// heavyBlock is a consensuschain.Block of a controlled byte size; only the methods GetContext
|
||||
// touches (ID/Parent/Bytes) are overridden — the rest of the interface is embedded and unused.
|
||||
type heavyBlock struct {
|
||||
consensusblock.Block
|
||||
id, parent ids.ID
|
||||
bytes []byte
|
||||
}
|
||||
|
||||
func (b *heavyBlock) ID() ids.ID { return b.id }
|
||||
func (b *heavyBlock) Parent() ids.ID { return b.parent }
|
||||
func (b *heavyBlock) Bytes() []byte { return b.bytes }
|
||||
|
||||
// sizeStubVM serves heavyBlocks by id (only GetBlock is called by GetContext).
|
||||
type sizeStubVM struct {
|
||||
consensuschain.BlockBuilder
|
||||
blocks map[ids.ID]consensusblock.Block
|
||||
}
|
||||
|
||||
func (v *sizeStubVM) GetBlock(_ context.Context, id ids.ID) (consensusblock.Block, error) {
|
||||
b, ok := v.blocks[id]
|
||||
if !ok {
|
||||
return nil, errors.New("not found")
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// sizeRecMsg records the containers GetContext hands to Ancestors, so the test can measure the
|
||||
// assembled response size (the input the real zstd compressor would reject above the cap).
|
||||
type sizeRecMsg struct {
|
||||
message.OutboundMsgBuilder
|
||||
containers [][]byte
|
||||
}
|
||||
|
||||
func (m *sizeRecMsg) Ancestors(_ ids.ID, _ uint32, containers [][]byte) (message.OutboundMessage, error) {
|
||||
m.containers = containers
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func TestGetContext_ChunksBySize_FitsUnderCap(t *testing.T) {
|
||||
// A 100-block chain of ~150 KiB blocks. Packed by COUNT alone (up to maxContextBlocks=256,
|
||||
// i.e. all 100), the response is ~15 MB — 7x over the 2 MB cap, so the old handler's message
|
||||
// failed to build. Bounded by SIZE, it serves only as many as fit.
|
||||
const nBlocks = 100
|
||||
const blkSize = 150 * 1024
|
||||
|
||||
vm := &sizeStubVM{blocks: map[ids.ID]consensusblock.Block{}}
|
||||
parent := ids.Empty
|
||||
var tip ids.ID
|
||||
for i := 0; i < nBlocks; i++ {
|
||||
id := ids.GenerateTestID()
|
||||
vm.blocks[id] = &heavyBlock{id: id, parent: parent, bytes: make([]byte, blkSize)}
|
||||
parent = id
|
||||
tip = id
|
||||
}
|
||||
|
||||
msg := &sizeRecMsg{}
|
||||
bh := &blockHandler{
|
||||
logger: log.NewNoOpLogger(),
|
||||
vm: vm,
|
||||
msgCreator: msg,
|
||||
net: &redStubNet{},
|
||||
chainID: ids.GenerateTestID(),
|
||||
networkID: ids.GenerateTestID(),
|
||||
maxContextBlocks: 256,
|
||||
}
|
||||
|
||||
if err := bh.GetContext(context.Background(), ids.GenerateTestNodeID(), 1, time.Now().Add(time.Second), tip); err != nil {
|
||||
t.Fatalf("GetContext: %v", err)
|
||||
}
|
||||
|
||||
total := 0
|
||||
for _, c := range msg.containers {
|
||||
total += len(c)
|
||||
}
|
||||
budget := constants.DefaultMaxMessageSize - 128*1024 // the handler's byteBudget
|
||||
|
||||
if len(msg.containers) == 0 {
|
||||
t.Fatal("must serve at least one block (a behind node must always make progress)")
|
||||
}
|
||||
if total > budget {
|
||||
t.Fatalf("context response %d bytes exceeds budget %d — the real Ancestors compressor would REJECT it "+
|
||||
"(cap %d), stranding a behind validator (the live bug)", total, budget, constants.DefaultMaxMessageSize)
|
||||
}
|
||||
if len(msg.containers) >= nBlocks {
|
||||
t.Fatalf("expected SIZE truncation (fewer than %d blocks), got %d — response not chunked", nBlocks, len(msg.containers))
|
||||
}
|
||||
t.Logf("size-chunked: %d blocks, %d payload bytes (budget %d, cap %d) — fits, so the compressor accepts it",
|
||||
len(msg.containers), total, budget, constants.DefaultMaxMessageSize)
|
||||
}
|
||||
|
||||
// A single heavy block is ALWAYS served even if it alone exceeds the budget — the walk must never
|
||||
// deadlock (the trust-tiered validator cap gives such a block the send headroom; a stranger's
|
||||
// tight cap correctly rejects it downstream).
|
||||
func TestGetContext_SingleOversizeBlock_StillServed(t *testing.T) {
|
||||
oversize := constants.DefaultMaxMessageSize + 1<<20 // > the cap on its own
|
||||
id := ids.GenerateTestID()
|
||||
vm := &sizeStubVM{blocks: map[ids.ID]consensusblock.Block{
|
||||
id: &heavyBlock{id: id, parent: ids.Empty, bytes: make([]byte, oversize)},
|
||||
}}
|
||||
msg := &sizeRecMsg{}
|
||||
bh := &blockHandler{
|
||||
logger: log.NewNoOpLogger(), vm: vm, msgCreator: msg, net: &redStubNet{},
|
||||
chainID: ids.GenerateTestID(), networkID: ids.GenerateTestID(), maxContextBlocks: 256,
|
||||
}
|
||||
if err := bh.GetContext(context.Background(), ids.GenerateTestNodeID(), 1, time.Now().Add(time.Second), id); err != nil {
|
||||
t.Fatalf("GetContext: %v", err)
|
||||
}
|
||||
if len(msg.containers) != 1 {
|
||||
t.Fatalf("a single (even oversize) block must be served so the walk never deadlocks, got %d blocks", len(msg.containers))
|
||||
}
|
||||
}
|
||||
+375
-61
@@ -58,6 +58,7 @@ import (
|
||||
"github.com/luxfi/constants"
|
||||
"github.com/luxfi/container/buffer"
|
||||
"github.com/luxfi/crypto/bls"
|
||||
"github.com/luxfi/crypto/mldsa"
|
||||
"github.com/luxfi/filesystem/perms"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/math/set"
|
||||
@@ -68,6 +69,7 @@ import (
|
||||
"github.com/luxfi/node/upgrade"
|
||||
"github.com/luxfi/node/vms"
|
||||
validators "github.com/luxfi/validators"
|
||||
version "github.com/luxfi/version"
|
||||
"github.com/luxfi/vm/fx"
|
||||
|
||||
// "github.com/luxfi/node/vms/metervm" // Temporarily disabled - needs consensus package updates
|
||||
@@ -101,6 +103,16 @@ const (
|
||||
defaultChannelSize = 1
|
||||
initialQueueSize = 3
|
||||
|
||||
// vmStartupTimeout bounds each VM startup/lifecycle operation (Initialize,
|
||||
// Linearize, SetState, CreateHandlers, router AddChain, state-sync). It must
|
||||
// be generous enough to cover a cold coreth "Regenerate historical state"
|
||||
// pass after an unclean shutdown (observed 67-134s on mainnet C-Chain),
|
||||
// which the previous hardcoded 30s budget blew — the context was cancelled
|
||||
// mid-init, so the VM was marked failed and its C-Chain route never
|
||||
// registered (the recurring post-restart 404 that needed a second restart).
|
||||
// Bounded so a genuinely hung VM still surfaces rather than hanging forever.
|
||||
vmStartupTimeout = 10 * time.Minute
|
||||
|
||||
luxNamespace = constants.PlatformName + utilmetric.NamespaceSeparator + "lux"
|
||||
handlerNamespace = constants.PlatformName + utilmetric.NamespaceSeparator + "handler"
|
||||
meterchainvmNamespace = constants.PlatformName + utilmetric.NamespaceSeparator + "meterchainvm"
|
||||
@@ -160,6 +172,20 @@ var (
|
||||
_ Manager = (*manager)(nil)
|
||||
)
|
||||
|
||||
// quasarExportVM is the OPTIONAL two-tier-consensus (v1.36) export sink a VM may
|
||||
// expose: the consensus engine pushes each Quasar (⅔-by-stake) EXPORT-FINAL
|
||||
// frontier advance in, and re-seeds from the VM's durable height on boot, so the
|
||||
// VM's `finalized`/`safe` tags and cross-chain export gate track ⅔-stake
|
||||
// finality instead of the reorgable Nova accept tip. NOT part of chain.ChainVM —
|
||||
// generic VMs never implement it and run Nova-only. For a plugin VM the concrete
|
||||
// implementation is in another process; the rpcchainvm client carries these
|
||||
// across the boundary and reports whether the plugin advertised the capability
|
||||
// via SupportsQuasarExport (see the wiring in createChain).
|
||||
type quasarExportVM interface {
|
||||
SetLastQuasarFinalized(uint64)
|
||||
LastQuasarHeight() uint64
|
||||
}
|
||||
|
||||
// Manager manages the chains running on this node.
|
||||
// It can:
|
||||
// - Create a chain
|
||||
@@ -219,7 +245,7 @@ type ChainParameters struct {
|
||||
FxIDs []ids.ID
|
||||
// Invariant: Only used when [ID] is the P-chain ID.
|
||||
CustomBeacons validators.Manager
|
||||
// Name of the chain (used for HTTP routing alias, e.g., /ext/bc/zoo/rpc)
|
||||
// Name of the chain (used for HTTP routing alias, e.g., /v1/bc/zoo/rpc)
|
||||
Name string
|
||||
}
|
||||
|
||||
@@ -339,8 +365,19 @@ type ManagerConfig struct {
|
||||
SybilProtectionEnabled bool
|
||||
StakingTLSSigner crypto.Signer
|
||||
StakingTLSCert *staking.Certificate
|
||||
StakingBLSKey bls.Signer
|
||||
TracingEnabled bool
|
||||
// Strict-PQ proposer identity. When StakingMLDSASigner is non-nil, chains wrap
|
||||
// their proposervm with ML-DSA-65 block signing so the signed block's
|
||||
// Proposer() matches the ML-DSA-keyed validator set. Nil ⇒ classical TLS-leaf.
|
||||
StakingMLDSASigner *mldsa.PrivateKey
|
||||
StakingMLDSAPub []byte
|
||||
// ProposerWindowDuration overrides the proposervm proposer-slot spacing (0 ⇒
|
||||
// the 5s mainnet default). Small local/dev nets set it low for fast cadence.
|
||||
ProposerWindowDuration time.Duration
|
||||
// ProposerMinBlockDelay overrides the proposervm minimum block delay (0 ⇒ the
|
||||
// 1s default). High-throughput / DEX nets set it low (e.g. 1ms).
|
||||
ProposerMinBlockDelay time.Duration
|
||||
StakingBLSKey bls.Signer
|
||||
TracingEnabled bool
|
||||
// Must not be used unless [TracingEnabled] is true as this may be nil.
|
||||
Tracer trace.Tracer
|
||||
Log log.Logger
|
||||
@@ -743,7 +780,7 @@ func (m *manager) createChain(chainParams ChainParameters) {
|
||||
// plugin shows up later (e.g. via lpm install).
|
||||
//
|
||||
// The old behavior (always register a failing health check) made
|
||||
// /ext/health return 503 for any opted-out chain, which made
|
||||
// /v1/health return 503 for any opted-out chain, which made
|
||||
// kubelet liveness probes kill the validator pod. That made
|
||||
// chain participation effectively all-or-nothing per validator.
|
||||
// Now: validators participate per-plugin, opt-in.
|
||||
@@ -828,7 +865,7 @@ func (m *manager) createChain(chainParams ChainParameters) {
|
||||
m.Log.Info("VM implements CreateHandlers, calling it",
|
||||
log.Stringer("chainID", chainParams.ID),
|
||||
)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), vmStartupTimeout)
|
||||
defer cancel()
|
||||
handlers, err := vm.CreateHandlers(ctx)
|
||||
if err != nil {
|
||||
@@ -849,13 +886,13 @@ func (m *manager) createChain(chainParams ChainParameters) {
|
||||
chainBase := fmt.Sprintf("bc/%s", chainAlias)
|
||||
chainIDBase := fmt.Sprintf("bc/%s", chainParams.ID.String())
|
||||
|
||||
// AddRoute will build the full path as /ext/<base><endpoint>
|
||||
// AddRoute will build the full path as /v1/<base><endpoint>
|
||||
m.Server.AddRoute(handler, chainBase, endpoint)
|
||||
if chainAlias != chainParams.ID.String() {
|
||||
m.Server.AddRoute(handler, chainIDBase, endpoint)
|
||||
}
|
||||
|
||||
// Also register with chain name alias for user-friendly routing (e.g., /ext/bc/zoo/rpc)
|
||||
// Also register with chain name alias for user-friendly routing (e.g., /v1/bc/zoo/rpc)
|
||||
if chainParams.Name != "" {
|
||||
nameLower := strings.ToLower(chainParams.Name)
|
||||
nameBase := fmt.Sprintf("bc/%s", nameLower)
|
||||
@@ -895,7 +932,7 @@ func (m *manager) createChain(chainParams ChainParameters) {
|
||||
|
||||
// Register chain with the router for message routing
|
||||
if m.ManagerConfig.Router != nil {
|
||||
routeCtx, routeCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
routeCtx, routeCancel := context.WithTimeout(context.Background(), vmStartupTimeout)
|
||||
defer routeCancel()
|
||||
m.ManagerConfig.Router.AddChain(routeCtx, chainParams.ID, chain.Handler)
|
||||
}
|
||||
@@ -928,16 +965,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("║ → /ext/bc/" + chainParams.ID.String())
|
||||
m.Log.Info("║ → /v1/bc/" + chainParams.ID.String())
|
||||
if chainAlias != chainParams.ID.String() {
|
||||
m.Log.Info("║ → /ext/bc/" + chainAlias)
|
||||
m.Log.Info("║ → /v1/bc/" + chainAlias)
|
||||
}
|
||||
m.Log.Info("╚══════════════════════════════════════════════════════════════════╝")
|
||||
|
||||
// Tell the chain to start processing messages.
|
||||
// If the X, P, or C Chain panics, do not attempt to recover
|
||||
if chain.Engine != nil {
|
||||
startCtx, startCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
startCtx, startCancel := context.WithTimeout(context.Background(), vmStartupTimeout)
|
||||
defer startCancel()
|
||||
chain.Engine.Start(startCtx, !m.CriticalChains.Contains(chainParams.ID))
|
||||
|
||||
@@ -1095,18 +1132,27 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
|
||||
}
|
||||
|
||||
// expectsStakedBeacons gates the EMPTY-beacon-set behavior of the bootstrap frontier
|
||||
// quorum (see blockHandler.expectsStakedBeacons). True ONLY for a native NON-platform
|
||||
// chain (C/X/Q/...) on a sybil-protected network — those sync against the STAKED
|
||||
// quorum (see blockHandler.expectsStakedBeacons). True for a native NON-platform chain
|
||||
// (C/X/Q/...) on a SYBIL-PROTECTED (real staked) network — those sync against the STAKED
|
||||
// primary-network validator set (m.Validators, populated by the already-bootstrapped
|
||||
// P-chain), so an empty set means "not yet loaded / misconfig → wait then fail safe",
|
||||
// NOT "single-node → done". The P-chain (CustomBeacons may be empty under endpoint-only
|
||||
// --bootstrap-nodes) and skip-bootstrap keep the "empty ⇒ nothing to sync to" behavior.
|
||||
// Computed BEFORE the skip-bootstrap override so it is false in single-node mode.
|
||||
expectsStakedBeacons := !m.SkipBootstrap && ids.IsNativeChain(chainParams.ID) && !isPlatformChain
|
||||
// NOT "single-node → done". Driven by sybil-protection, NOT --skip-bootstrap: a production
|
||||
// validator sets skip-bootstrap and that must NOT make it masquerade as single-node and
|
||||
// wedge behind at its stale local tip (tasks #66/#74). See chainExpectsStakedBeacons.
|
||||
// Discriminate on the validating Net (chainParams.ChainID), NOT the blockchain ID:
|
||||
// deployed C/X/Q carry HASH blockchain IDs, and ids.IsNativeChain only matches the
|
||||
// symbolic 111...C alias form (no deployed chain has it). Keying off the blockchain ID
|
||||
// (the prior ids.IsNativeChain(chainParams.ID)) was therefore ALWAYS false for the real
|
||||
// C-Chain, leaving this fix inert — the exact wedge #66/#74 set out to kill fired anyway.
|
||||
isPrimaryNetworkChain := chainValidatesOnPrimaryNetwork(chainParams.ChainID)
|
||||
expectsStakedBeacons := chainExpectsStakedBeacons(m.SybilProtectionEnabled, isPrimaryNetworkChain, isPlatformChain)
|
||||
|
||||
// In skip-bootstrap mode, use empty beacons for all chains
|
||||
// This enables single-node development mode
|
||||
if m.SkipBootstrap {
|
||||
// skip-bootstrap forces empty beacons (single-node immediate-start) for every chain EXCEPT
|
||||
// one that syncs against the staked set — those MUST always catch a behind validator up from
|
||||
// its peers, so emptying their beacons (the prior UNCONDITIONAL override) is precisely the
|
||||
// rejoin wedge this fixes. A genuine single-node / dev net runs sybil-protection OFF, so its
|
||||
// native chains still fall here and get the empty-beacon immediate-start path.
|
||||
if m.SkipBootstrap && !expectsStakedBeacons {
|
||||
beacons = &emptyValidatorManager{}
|
||||
m.Log.Info("skip-bootstrap enabled - using empty beacons for single-node mode")
|
||||
}
|
||||
@@ -1162,7 +1208,7 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
|
||||
vmConfigBytes := m.injectAutominingConfig(chainParams.VMID, chainConfig.Config)
|
||||
vmConfigBytes = m.injectSecurityProfileConfig(chainParams.VMID, vmConfigBytes)
|
||||
// CONSENSUS-SAFETY (single-proposer-per-height): re-wrap multi-validator
|
||||
// linear chains in proposervm so block production follows the Snowman++
|
||||
// linear chains in proposervm so block production follows the proposervm's
|
||||
// proposer schedule — exactly ONE validator builds height H, the rest wait
|
||||
// and vote. Without it every validator's engine calls BuildBlock
|
||||
// UNCONDITIONALLY at every height off a slightly-different mempool, so two
|
||||
@@ -1199,6 +1245,10 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
|
||||
return nil, fmt.Errorf("refusing to start multi-node chain %s with non-BFT consensus params: %w", chainParams.ID, err)
|
||||
}
|
||||
}
|
||||
// v1.36 "Nova": the round-scoped VIEW-CHANGE (prevote/POL/lock) was DELETED from the
|
||||
// consensus engine (174af3c31). Nova metastable sampling is the sole decider and the ⅔
|
||||
// Quasar attestation trails it — there is no view-change to opt into, so the former
|
||||
// LUX_CONSENSUS_VIEW_CHANGE env gate is gone. Keep the braid dead.
|
||||
_, innerIsDAGNative := vmTyped.(interface {
|
||||
Linearize(context.Context, ids.ID, chan<- vm.Message) error
|
||||
})
|
||||
@@ -1243,24 +1293,31 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
|
||||
if regErr != nil {
|
||||
return nil, fmt.Errorf("failed to register proposervm metrics for chain %s: %w", chainParams.ID, regErr)
|
||||
}
|
||||
minBlkDelay := proposervm.DefaultMinBlockDelay
|
||||
if m.ProposerMinBlockDelay > 0 {
|
||||
minBlkDelay = m.ProposerMinBlockDelay
|
||||
}
|
||||
engineVM = proposervm.New(vmTyped, proposervm.Config{
|
||||
Upgrades: m.Upgrades,
|
||||
NetworkID: networkID, // CRITICAL-3: windower uses the SAME set ID as the cert side
|
||||
MinBlkDelay: proposervm.DefaultMinBlockDelay,
|
||||
NumHistoricalBlocks: proposervm.DefaultNumHistoricalBlocks,
|
||||
StakingLeafSigner: m.StakingTLSSigner,
|
||||
StakingCertLeaf: m.StakingTLSCert,
|
||||
Registerer: proposervmReg,
|
||||
Upgrades: m.Upgrades,
|
||||
NetworkID: networkID, // CRITICAL-3: windower uses the SAME set ID as the cert side
|
||||
MinBlkDelay: minBlkDelay,
|
||||
NumHistoricalBlocks: proposervm.DefaultNumHistoricalBlocks,
|
||||
StakingLeafSigner: m.StakingTLSSigner,
|
||||
StakingCertLeaf: m.StakingTLSCert,
|
||||
StakingMLDSASigner: m.StakingMLDSASigner,
|
||||
StakingMLDSAPub: m.StakingMLDSAPub,
|
||||
ProposerWindowDuration: m.ProposerWindowDuration,
|
||||
Registerer: proposervmReg,
|
||||
})
|
||||
m.Log.Info("wrapping chain VM in proposervm for single-proposer-per-height block production",
|
||||
log.Stringer("chainID", chainParams.ID),
|
||||
log.Int("K", consensusParams.K),
|
||||
log.Stringer("windowerNetworkID", networkID),
|
||||
log.Duration("minBlockDelay", proposervm.DefaultMinBlockDelay))
|
||||
log.Duration("minBlockDelay", minBlkDelay))
|
||||
}
|
||||
|
||||
m.Log.Info("initializing VM", log.Stringer("chainID", chainParams.ID))
|
||||
initCtx, initCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
initCtx, initCancel := context.WithTimeout(context.Background(), vmStartupTimeout)
|
||||
defer initCancel()
|
||||
// Initialize THROUGH engineVM. proposervm.Initialize builds its windower
|
||||
// from chainRuntime.ValidatorState, then initializes the inner VM with the
|
||||
@@ -1328,7 +1385,7 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
|
||||
}); ok {
|
||||
m.Log.Info("linearizing DAG-native VM into linear block mode",
|
||||
log.Stringer("chainID", chainParams.ID))
|
||||
linCtx, linCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
linCtx, linCancel := context.WithTimeout(context.Background(), vmStartupTimeout)
|
||||
if err := linearVM.Linearize(linCtx, ids.Empty, toEngine); err != nil {
|
||||
linCancel()
|
||||
m.Log.Error("failed to linearize VM",
|
||||
@@ -1361,7 +1418,7 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
|
||||
}); ok {
|
||||
m.Log.Info("transitioning VM to bootstrapping (initial sync gates normal operation)",
|
||||
log.Stringer("chainID", chainParams.ID))
|
||||
stateCtx, stateCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
stateCtx, stateCancel := context.WithTimeout(context.Background(), vmStartupTimeout)
|
||||
if err := stateVM.SetState(stateCtx, uint32(vm.Bootstrapping)); err != nil {
|
||||
stateCancel()
|
||||
m.Log.Error("failed to transition VM to bootstrapping",
|
||||
@@ -1505,7 +1562,7 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
|
||||
// the proposervm, whose SignedBlock carries the real P-chain height
|
||||
// (selectChildPChainHeight = max(GetCurrentHeight, parentH)) and exposes
|
||||
// PChainHeight() — the SAME value the engine's pChainHeightOf reads. That
|
||||
// is precisely the Snowman++ mechanism newPChainHeightVM was a stand-in
|
||||
// is precisely the proposervm mechanism newPChainHeightVM was a stand-in
|
||||
// for, so stacking both would double-stamp the height. We keep
|
||||
// newPChainHeightVM only for the unwrapped K>1 chains (P-Chain, X-Chain).
|
||||
if blockBuilder != nil && !wrapInProposerVM {
|
||||
@@ -1516,8 +1573,56 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
|
||||
log.Stringer("networkID", networkID))
|
||||
}
|
||||
}
|
||||
// EXPORT-FRONTIER BRIDGE (two-tier consensus, v1.36). VM.Accept now advances the
|
||||
// local NOVA (bare-majority) accept tip, which is reorgable and MUST NOT be exported.
|
||||
// Push each EXPORT (Quasar, ⅔-by-stake) frontier advance into the VM so the EVM
|
||||
// `finalized`/`safe` block tags and the warp cross-chain export gate resolve to the
|
||||
// Quasar tip, NEVER the Nova tip (the "semantic collapse" the split exists to prevent).
|
||||
//
|
||||
// Capability-gated, not just interface-gated: the C-Chain EVM runs as a SEPARATE
|
||||
// rpcchainvm plugin process, so vmTyped here is the rpcchainvm *Client, which carries
|
||||
// SetLastQuasarFinalized/LastQuasarHeight for EVERY plugin (they cross the ZAP
|
||||
// boundary). The client learns from the plugin's Initialize handshake whether the
|
||||
// concrete VM actually implements the export capability and reports it via
|
||||
// SupportsQuasarExport — false → the observer stays unwired and this chain is Nova-only
|
||||
// with no per-finalization cross-process no-op. A VM WITHOUT the probe (an in-process
|
||||
// VM whose concrete export methods we hold directly) is treated as capable, preserving
|
||||
// the direct-wire path. Push into the RAW inner VM (vmTyped) — the eth/warp backends
|
||||
// live there, not on the proposervm wrapper.
|
||||
//
|
||||
// Ordering: the observer MUST be set on netCfg BEFORE NewRuntime captures it; the boot
|
||||
// re-seed needs the constructed engine and so runs after. exportVM (nil unless capable)
|
||||
// carries the wired/not-wired decision across that split — a value, not a re-derived
|
||||
// predicate.
|
||||
var exportVM quasarExportVM
|
||||
if qvm, ok := vmTyped.(quasarExportVM); ok {
|
||||
capable := true
|
||||
if probe, hasProbe := vmTyped.(interface{ SupportsQuasarExport() bool }); hasProbe {
|
||||
capable = probe.SupportsQuasarExport()
|
||||
}
|
||||
if capable {
|
||||
exportVM = qvm
|
||||
netCfg.QuasarObserver = func(_ ids.ID, height uint64) {
|
||||
qvm.SetLastQuasarFinalized(height)
|
||||
}
|
||||
m.Log.Info("wired EXPORT-frontier (quasar) bridge into the VM (finalized/safe + warp gate track ⅔-stake finality)",
|
||||
log.Stringer("chainID", chainParams.ID))
|
||||
}
|
||||
}
|
||||
consensusEngine := consensuschain.NewRuntime(netCfg)
|
||||
|
||||
// Re-seed the consensus EXPORT frontier from the VM's DURABLE Quasar height so
|
||||
// GetQuasarTip / QuasarHeight do not regress on restart (the in-memory frontier resets
|
||||
// to (Empty,0) until a fresh ⅔-stake cert re-forms; the VM persisted the export height).
|
||||
// Advance-only; the observer above refines it as new certs land this session.
|
||||
if exportVM != nil {
|
||||
if h := exportVM.LastQuasarHeight(); h > 0 {
|
||||
consensusEngine.SyncQuasarFrontier(ids.Empty, h)
|
||||
m.Log.Info("re-seeded consensus export (quasar) frontier from the VM's durable height on boot",
|
||||
log.Stringer("chainID", chainParams.ID), log.Uint64("quasarHeight", h))
|
||||
}
|
||||
}
|
||||
|
||||
// Start the consensus engine with a LIFETIME context (not a timeout):
|
||||
// engine.Start parents all four long-running loops (poll, vote, pipeline,
|
||||
// re-poll) to this ctx, so a WithTimeout here kills them ~30s after the
|
||||
@@ -1532,7 +1637,7 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
|
||||
m.Log.Info("consensus engine started with Lux consensus (Photon → Wave → Focus)",
|
||||
log.Stringer("chainID", chainParams.ID))
|
||||
if blockBuilder != nil {
|
||||
syncCtx, syncCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
syncCtx, syncCancel := context.WithTimeout(context.Background(), vmStartupTimeout)
|
||||
defer syncCancel()
|
||||
lastAcceptedID, height, err := consensuschain.SyncStateFromVM(syncCtx, blockBuilder, consensusEngine.Transitive)
|
||||
if err != nil {
|
||||
@@ -1600,7 +1705,10 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
|
||||
// through (blockBuilder == the P-chain-height wrapper on K>1, the inner VM
|
||||
// on K==1), so the container bytes it parses match the bytes the engine
|
||||
// framed — one codec, no raw-vs-wrapped split.
|
||||
bh := newBlockHandler(blockBuilder, m.Log, consensusEngine, m.Net, m.MsgCreator, chainParams.ID, networkID, beacons, m.NodeID, expectsStakedBeacons)
|
||||
// engineVM is the connectable VM (the proposervm on a wrapped chain, the
|
||||
// inner VM otherwise); it carries Connected/Disconnected, which the router
|
||||
// dispatches so the P-chain uptime tracker observes validator connectivity.
|
||||
bh := newBlockHandler(blockBuilder, engineVM, m.Log, consensusEngine, m.Net, m.MsgCreator, chainParams.ID, networkID, beacons, m.NodeID, expectsStakedBeacons)
|
||||
// Gate this native chain's bootstrap frontier-TRUST on the P-chain having finished its
|
||||
// initial sync, so the staked beacon set (and thus the stake-majority floor denominator)
|
||||
// is the TRUE full validator set, not a partial mid-replay set. Wired ONLY for native
|
||||
@@ -1801,7 +1909,7 @@ func (m *manager) createDAG(
|
||||
}
|
||||
|
||||
// Create a context for VM initialization with timeout
|
||||
initCtx, cancelInit := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
initCtx, cancelInit := context.WithTimeout(context.Background(), vmStartupTimeout)
|
||||
defer cancelInit() // Ensure cleanup on function exit
|
||||
|
||||
// Initialize VM if it supports Initialize
|
||||
@@ -1969,7 +2077,7 @@ func (m *manager) createDAG(
|
||||
|
||||
// Register HTTP handlers for DAG VMs (exchangevm, qvm, etc.)
|
||||
adapter := &dagVMAdapter{underlying: vmImpl}
|
||||
dagHandlerCtx, dagHandlerCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
dagHandlerCtx, dagHandlerCancel := context.WithTimeout(context.Background(), vmStartupTimeout)
|
||||
defer dagHandlerCancel()
|
||||
handlers, err := adapter.CreateHandlers(dagHandlerCtx)
|
||||
if err != nil {
|
||||
@@ -2080,7 +2188,7 @@ func (m *manager) monitorBootstrap(engine Engine, h handler.Handler, sb nets.Net
|
||||
// this true. The no-progress watchdog treats it as a deliberate quorum WAIT, not a stall: it must
|
||||
// NOT force-STOP a node that is correctly failing safe DOWN and waiting for its quorum to return
|
||||
// (the network cannot progress without the quorum, and the K8s probes — all polling the
|
||||
// always-green /ext/health/liveness — would never restart it, so a stop here is a permanent
|
||||
// always-green /v1/health/liveness — would never restart it, so a stop here is a permanent
|
||||
// brick). The node stays in Bootstrapping (serving nothing as head) and converges when the quorum
|
||||
// returns. nil for degenerate handlers (legacy behavior).
|
||||
type bootstrapConnector interface{ BootstrapConnecting() bool }
|
||||
@@ -2252,6 +2360,17 @@ func watchBootstrapProgress(
|
||||
}
|
||||
}
|
||||
|
||||
// IsBootstrapped reports whether [id] has ACTUALLY finished initial sync on this
|
||||
// node: the chain exists AND its validation net has marked it bootstrapped —
|
||||
// monitorBootstrap called sb.Bootstrapped(id) only after runInitialSync reached the
|
||||
// named network frontier and transitioned the VM to normal operation (head advanced
|
||||
// to the frontier, eth-RPC serving live). The old body returned true the instant the
|
||||
// chain merely EXISTED in m.chains — set right after createChain launched the (async,
|
||||
// possibly-stalling) bootstrap goroutine — so a C-Chain stalled at genesis (head 0x0,
|
||||
// never converged) still reported info.isBootstrapped(C)=true, masking the stall from
|
||||
// any readiness gate keyed on it. Keying on the SAME sb.Bootstrapped signal the health
|
||||
// check (m.Nets.Bootstrapping) already uses makes info.isBootstrapped track real state,
|
||||
// so a hands-off rolling upgrade's wait-for-healthy gate can trust it.
|
||||
func (m *manager) IsBootstrapped(id ids.ID) bool {
|
||||
m.chainsLock.Lock()
|
||||
_, exists := m.chains[id]
|
||||
@@ -2259,9 +2378,7 @@ func (m *manager) IsBootstrapped(id ids.ID) bool {
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
// Bootstrapped chains start in NormalOp
|
||||
return true
|
||||
return m.Nets.IsChainBootstrapped(id)
|
||||
}
|
||||
|
||||
func (m *manager) GetChains() []ChainInfo {
|
||||
@@ -2271,10 +2388,12 @@ func (m *manager) GetChains() []ChainInfo {
|
||||
result := make([]ChainInfo, 0, len(m.chains))
|
||||
for id, info := range m.chains {
|
||||
result = append(result, ChainInfo{
|
||||
ID: id,
|
||||
Name: info.Name,
|
||||
VMID: info.VMID,
|
||||
Bootstrapped: true,
|
||||
ID: id,
|
||||
Name: info.Name,
|
||||
VMID: info.VMID,
|
||||
// Real per-chain convergence, not mere existence (same fix as IsBootstrapped):
|
||||
// a tracked-but-still-syncing chain reports Bootstrapped=false.
|
||||
Bootstrapped: m.Nets.IsChainBootstrapped(id),
|
||||
})
|
||||
}
|
||||
return result
|
||||
@@ -2577,6 +2696,43 @@ func (m *manager) getOrMakeVMGatherer(vmID ids.ID) (metrics.MultiGatherer, error
|
||||
return vmGatherer, nil
|
||||
}
|
||||
|
||||
// chainExpectsStakedBeacons reports whether a chain must sync its bootstrap frontier against the
|
||||
// STAKED primary-network validator set — the ⅔-by-stake quorum that names the network frontier
|
||||
// (blockHandler.expectsStakedBeacons, the C1 forged-chain gate). When true, --skip-bootstrap does
|
||||
// NOT empty the chain's beacon set (buildChain below), so a behind validator always catches up
|
||||
// from its peers.
|
||||
//
|
||||
// It is driven by SYBIL PROTECTION — the true "this is a real staked network" signal — and NOT by
|
||||
// --skip-bootstrap. Production validators set --skip-bootstrap to skip the initial bootstrap WAIT,
|
||||
// but that flag must NEVER disable peer-sync on a staked network. Keying this decision off
|
||||
// --skip-bootstrap (the prior `!m.SkipBootstrap && ...`) is exactly what wedged a behind native
|
||||
// chain (C/X/Q...): with skip-bootstrap set, the chain got expectsStakedBeacons=false + empty
|
||||
// beacons, so FrontierTip reported FrontierNoBeacons ("nothing to sync to"), the node named its
|
||||
// STALE local last-accepted the network frontier, went live there, and never caught up across
|
||||
// restarts until a manual chaindata wipe (tasks #66/#74). A genuine single-node / dev network runs
|
||||
// sybil-protection OFF, so it still takes the empty-beacon immediate-start path. The platform chain
|
||||
// anchors to its own CustomBeacons (not the staked set), so it is excluded here as before; a
|
||||
// single-VALIDATOR staked net (self-only set) is handled downstream by FrontierTip's
|
||||
// hasExternalBeacons rule, which reads the LOADED set.
|
||||
func chainExpectsStakedBeacons(sybilProtectionEnabled, isPrimaryNetworkChain, isPlatformChain bool) bool {
|
||||
return sybilProtectionEnabled && isPrimaryNetworkChain && !isPlatformChain
|
||||
}
|
||||
|
||||
// chainValidatesOnPrimaryNetwork reports whether a chain's validating Net IS the primary
|
||||
// network — the correct discriminator for the "native" C/X/Q/... set that syncs its bootstrap
|
||||
// frontier against the primary-network STAKED validator set. It keys off the validating Net
|
||||
// (subnet) ID, NOT the blockchain ID: deployed C/X/Q carry HASH blockchain IDs, whereas
|
||||
// ids.IsNativeChain only ever matches the symbolic 111...C alias form that NO deployed chain
|
||||
// uses (only the P-chain, at PlatformChainID=111...P, has a symbolic blockchain ID — and it is
|
||||
// excluded as the platform chain anyway). Keying the durable-rejoin discriminator off the
|
||||
// blockchain ID (the prior ids.IsNativeChain(chainParams.ID)) therefore silently excluded every
|
||||
// real C/X/Q and left the fix inert across restarts (#66/#74). The validating Net is
|
||||
// PrimaryNetworkID for C/X/Q and each sovereign L1's own net ID for an L2, so this correctly
|
||||
// keeps the empty-beacon single-node path for L2s while re-enabling peer-sync for C/X/Q.
|
||||
func chainValidatesOnPrimaryNetwork(validatingNetID ids.ID) bool {
|
||||
return validatingNetID == constants.PrimaryNetworkID
|
||||
}
|
||||
|
||||
// emptyValidatorManager implements validators.Manager with no validators
|
||||
type emptyValidatorManager struct{}
|
||||
|
||||
@@ -2692,6 +2848,15 @@ type blockHandler struct {
|
||||
chainID ids.ID // Chain ID for message routing
|
||||
networkID ids.ID // Network ID for validator routing
|
||||
|
||||
// connector receives peer connect/disconnect notifications routed from the
|
||||
// node's chainRouter and forwards them to this chain's VM (e.g. the P-chain
|
||||
// uptime tracker; other VMs use them for their own peer sets). connectedNodes
|
||||
// dedups delivery so a peer the router dispatches once per tracked network is
|
||||
// forwarded to the VM exactly once. A nil connector makes forwarding a no-op.
|
||||
connector chain.ChainVM
|
||||
connMu sync.Mutex
|
||||
connectedNodes set.Set[ids.NodeID]
|
||||
|
||||
// Context sync support - when a block fails verification due to missing context,
|
||||
// we request the prerequisite blocks from the peer to catch up
|
||||
pendingContext map[ids.ID]contextRequest // Map from blockID to pending context request
|
||||
@@ -2723,13 +2888,21 @@ type blockHandler struct {
|
||||
// tells monitorBootstrap's no-progress watchdog this is a deliberate WAIT for the quorum to
|
||||
// return (the network cannot make progress without it), NOT a stall — so the watchdog does not
|
||||
// force-STOP a node that is correctly failing safe and waiting, which (given the K8s probes only
|
||||
// poll the always-green /ext/health/liveness) would otherwise be a permanent brick.
|
||||
// poll the always-green /v1/health/liveness) would otherwise be a permanent brick.
|
||||
bootstrapConnecting gatomic.Bool
|
||||
bsActive gatomic.Bool // true while the bootstrap loop is driving
|
||||
bsMu sync.Mutex // guards bsFrontierCh + bsAncestorCh
|
||||
bsMu sync.Mutex // guards bsFrontierCh + bsAncestorCh + bsAheadBeacons
|
||||
bsFrontierCh chan bsFrontierReply // weighted frontier replies for the current FrontierTip
|
||||
bsAncestorCh map[uint32]chan [][]byte // requestID -> ancestors reply for the current Ancestors
|
||||
bsRotor gatomic.Uint32 // round-robins the Ancestors peer sample (M1: no monopoly)
|
||||
// bsAheadBeacons is the set of beacons whose accepted tip, reported in the most recent
|
||||
// FrontierTip round, is a block this node does NOT hold — i.e. beacons genuinely AHEAD that
|
||||
// therefore HAVE the ancestry the descent needs. sampleAncestorBeacons prefers them so a
|
||||
// re-bootstrapping node asks GetAncestors of peers that can serve, never wasting the sample on
|
||||
// peers still at genesis (the ava PeerTracker "ask a prover" bias, without a full tracker).
|
||||
// Recorded in collectFrontierReplies; empty ⇒ sampleAncestorBeacons falls back to the full
|
||||
// rotated set (identical to prior behavior). Guarded by bsMu.
|
||||
bsAheadBeacons set.Set[ids.NodeID]
|
||||
|
||||
// vmReady transitions the VM to NORMAL OPERATION (vm.Ready → the EVM's
|
||||
// onNormalOperationsStarted: block building, mempool gossip, validator dispatch).
|
||||
@@ -2826,7 +2999,7 @@ type blockHandler struct {
|
||||
// DOWN — never live at a stale height) and CONVERGES the instant the quorum returns. ≤0 ⇒
|
||||
// UNLIMITED (retry until the quorum returns or shutdown) — the production default, because a
|
||||
// node without a quorum must keep trying to rejoin and the K8s liveness probe does NOT restart
|
||||
// it (all luxd probes poll the always-green /ext/health/liveness). A STRUCTURAL failure (deep
|
||||
// it (all luxd probes poll the always-green /v1/health/liveness). A STRUCTURAL failure (deep
|
||||
// gap → state-sync) is never retried regardless. Tests pin it to 1 to assert the single-attempt
|
||||
// terminal fail-safe in isolation.
|
||||
bootstrapMaxAttempts int
|
||||
@@ -2858,9 +3031,10 @@ type contextRequest struct {
|
||||
timestamp time.Time
|
||||
}
|
||||
|
||||
func newBlockHandler(vm consensuschain.BlockBuilder, logger log.Logger, engine *consensuschain.Runtime, net network.Network, msgCreator message.OutboundMsgBuilder, chainID ids.ID, networkID ids.ID, beacons validators.Manager, selfNodeID ids.NodeID, expectsStakedBeacons bool) *blockHandler {
|
||||
func newBlockHandler(vm consensuschain.BlockBuilder, connector chain.ChainVM, logger log.Logger, engine *consensuschain.Runtime, net network.Network, msgCreator message.OutboundMsgBuilder, chainID ids.ID, networkID ids.ID, beacons validators.Manager, selfNodeID ids.NodeID, expectsStakedBeacons bool) *blockHandler {
|
||||
return &blockHandler{
|
||||
vm: vm,
|
||||
connector: connector,
|
||||
logger: logger,
|
||||
engine: engine,
|
||||
net: net,
|
||||
@@ -2873,6 +3047,7 @@ func newBlockHandler(vm consensuschain.BlockBuilder, logger log.Logger, engine *
|
||||
pendingContext: make(map[ids.ID]contextRequest),
|
||||
maxContextBlocks: 256, // Default max context blocks to request/serve
|
||||
pendingQbits: make(map[ids.ID][]QbitEvent),
|
||||
connectedNodes: set.NewSet[ids.NodeID](16),
|
||||
bsAncestorCh: make(map[uint32]chan [][]byte),
|
||||
}
|
||||
}
|
||||
@@ -3094,14 +3269,45 @@ func (b *blockHandler) requestContext(ctx context.Context, nodeID ids.NodeID, bl
|
||||
return
|
||||
}
|
||||
|
||||
nodeSet := set.NewSet[ids.NodeID](1)
|
||||
nodeSet.Add(nodeID)
|
||||
// PEER SELECTION (defect #3). The consensus layer signals "I hold a VERIFIED cert
|
||||
// for a block I don't track — fetch it" by passing ids.EmptyNodeID (topology.go:
|
||||
// requestCatchup(cert.Position.BlockID, ids.EmptyNodeID)); picking a real peer is
|
||||
// the node layer's job. The prior code blindly Add(EmptyNodeID) + Send, so
|
||||
// GetAncestors went to ZERO peers (the "sentTo=0" spam on the frozen fleet) and the
|
||||
// certified-but-untracked block was NEVER fetched — the node saw the cert, could not
|
||||
// finalize, and never recovered. When nodeID is Empty, sample real connected peers
|
||||
// that track this chain's network (the SAME selection pollFrontierOnce uses); a valid
|
||||
// cert already gated this request, so asking any network peer is sound (the served
|
||||
// gap is cert-verified on accept).
|
||||
nodeSet := set.NewSet[ids.NodeID](frontierPollSample)
|
||||
if nodeID == ids.EmptyNodeID {
|
||||
for _, p := range b.net.PeerInfo(nil) {
|
||||
if p.TrackedChains.Contains(b.networkID) {
|
||||
nodeSet.Add(p.ID)
|
||||
if nodeSet.Len() >= frontierPollSample {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
nodeSet.Add(nodeID)
|
||||
}
|
||||
if nodeSet.Len() == 0 {
|
||||
// No reachable peer to serve the block. Release the pending slot so a later tick
|
||||
// (frontier poll → AcceptedFrontier, or a re-gossiped cert) can retry — otherwise
|
||||
// the block stays pinned unrequestable until the TTL reap.
|
||||
b.contextRequestMu.Lock()
|
||||
delete(b.pendingContext, blockID)
|
||||
b.contextRequestMu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
sentTo := b.net.Send(msg, nodeSet, b.networkID, 0)
|
||||
b.logger.Info("requested context for missing prerequisites",
|
||||
log.Stringer("from", nodeID),
|
||||
log.Stringer("blockID", blockID),
|
||||
log.Uint32("requestID", requestID),
|
||||
log.Int("asked", nodeSet.Len()),
|
||||
log.Int("sentTo", sentTo.Len()))
|
||||
}
|
||||
|
||||
@@ -3191,10 +3397,31 @@ func (b *blockHandler) GetContext(ctx context.Context, nodeID ids.NodeID, reques
|
||||
log.Stringer("containerID", containerID),
|
||||
log.Uint32("requestID", requestID))
|
||||
|
||||
// Collect context blocks (walk parent chain)
|
||||
// Collect context blocks (walk parent chain).
|
||||
//
|
||||
// SIZE-CHUNKING (the heavy-DEX-block self-heal fix). The response is the Ancestors
|
||||
// wire message, whose UNCOMPRESSED size the peer compressor refuses above the message
|
||||
// cap (constants.DefaultMaxMessageSize; the zstd compressor bounds its input to prevent a
|
||||
// decompression bomb). The old loop bounded ONLY by COUNT (maxContextBlocks=256), so under
|
||||
// heavy DEX load 256 blocks summed to 3.4-5.7 MB > the 2 MB cap, msgCreator.Ancestors FAILED
|
||||
// to build, and the behind validator got NOTHING — it could never resync and fell
|
||||
// permanently behind (the benchmark-proven stall). We now ALSO bound by serialized size:
|
||||
// stop before the accumulated payload would exceed the budget, but ALWAYS include at least
|
||||
// one block so a behind node makes progress every round; the requester re-requests for the
|
||||
// remaining gap (GetAncestors/context is already a multi-round, oldest-first fill). A single
|
||||
// block that alone exceeds the budget is still served (best-effort) so the walk never
|
||||
// deadlocks — the trust-tiered validator cap (peer layer) gives such a block the headroom to
|
||||
// actually send; for a stranger it will be refused by the tight cap, which is correct.
|
||||
var containers [][]byte
|
||||
currentID := containerID
|
||||
|
||||
// Leave margin under the cap for the p2p envelope (chainID, requestID, per-container length
|
||||
// prefixes, compression framing) so the assembled message stays comfortably below the limit.
|
||||
const contextResponseMargin = 128 * 1024 // 128 KiB
|
||||
byteBudget := constants.DefaultMaxMessageSize - contextResponseMargin
|
||||
accumulated := 0
|
||||
truncatedForSize := false
|
||||
|
||||
for i := 0; i < b.maxContextBlocks; i++ {
|
||||
// First check pending blocks (for recently proposed but not yet accepted blocks)
|
||||
// This is critical: when we propose a block and send PullQuery, other validators
|
||||
@@ -3225,6 +3452,14 @@ func (b *blockHandler) GetContext(ctx context.Context, nodeID ids.NodeID, reques
|
||||
certBytes, _ = b.engine.CertForBlock(blk.ID())
|
||||
}
|
||||
entry := encodeCatchupEntry(blk.Bytes(), certBytes)
|
||||
|
||||
// SIZE GATE: stop before exceeding the budget — but never drop the FIRST block, so a
|
||||
// behind node always receives at least one block per request and cannot deadlock.
|
||||
if len(containers) > 0 && accumulated+len(entry) > byteBudget {
|
||||
truncatedForSize = true
|
||||
break
|
||||
}
|
||||
accumulated += len(entry)
|
||||
containers = append([][]byte{entry}, containers...)
|
||||
|
||||
// Get parent ID for next iteration
|
||||
@@ -3260,6 +3495,8 @@ func (b *blockHandler) GetContext(ctx context.Context, nodeID ids.NodeID, reques
|
||||
log.Stringer("to", nodeID),
|
||||
log.Stringer("containerID", containerID),
|
||||
log.Int("numBlocks", len(containers)),
|
||||
log.Int("payloadBytes", accumulated),
|
||||
log.Bool("truncatedForSize", truncatedForSize),
|
||||
log.Int("sentTo", sentTo.Len()))
|
||||
|
||||
return nil
|
||||
@@ -3397,15 +3634,28 @@ func (b *blockHandler) AcceptedFrontier(ctx context.Context, nodeID ids.NodeID,
|
||||
if b.deliverBootstrapFrontier(nodeID, containerID) {
|
||||
return nil
|
||||
}
|
||||
if _, err := b.vm.GetBlock(ctx, containerID); err == nil {
|
||||
return nil // we already have the peer's tip — not behind
|
||||
}
|
||||
if b.engine != nil {
|
||||
if blk, err := b.vm.GetBlock(ctx, containerID); err == nil {
|
||||
// HAVE-BLOCK, LACK-FINALIZATION (defect #5). Holding the peer's tip block does NOT
|
||||
// mean we are caught up. A verified-but-unfinalized block (we voted for it, but the
|
||||
// α-of-K cert never reached us) leaves us behind on the CERT, not the block bytes.
|
||||
// The prior check returned "not behind" on GetBlock success, so such a node NEVER
|
||||
// fetched the missing cert and sat stuck at its unfinalized height forever (the exact
|
||||
// "verified 288 but no cert" condition). Only "have the block AND it is finalized
|
||||
// here" is truly not-behind; otherwise fall through to fetch the cert-carrying gap so
|
||||
// AcceptCatchupBlock can finalize it on its verified cert (no re-vote).
|
||||
if b.engine == nil {
|
||||
return nil
|
||||
}
|
||||
if fin, ok := b.engine.FinalizedBlockAtHeight(blk.Height()); ok && fin == containerID {
|
||||
return nil // have the block AND finalized it — truly not behind
|
||||
}
|
||||
// have the block but not finalized here → behind on the cert → fetch below
|
||||
} else if b.engine != nil {
|
||||
if _, found := b.engine.GetPendingBlock(containerID); found {
|
||||
return nil // already tracked
|
||||
return nil // already tracked (pending) — the live path is handling it
|
||||
}
|
||||
}
|
||||
b.requestContext(ctx, nodeID, containerID) // behind → fetch the gap
|
||||
b.requestContext(ctx, nodeID, containerID) // behind (missing block OR its cert) → fetch the gap
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3816,9 +4066,69 @@ func (b *blockHandler) GetStateSummary(ctx context.Context, nodeID ids.NodeID, r
|
||||
func (b *blockHandler) StateSummary(ctx context.Context, nodeID ids.NodeID, requestID uint32, summary []byte) error {
|
||||
return nil
|
||||
}
|
||||
func (b *blockHandler) Connected(ctx context.Context, nodeID ids.NodeID) error { return nil }
|
||||
func (b *blockHandler) Disconnected(ctx context.Context, nodeID ids.NodeID) error { return nil }
|
||||
func (b *blockHandler) HealthCheck(ctx context.Context) (interface{}, error) { return nil, nil }
|
||||
|
||||
// Connected satisfies handler.Handler, whose interface carries no peer version.
|
||||
// The real delivery path is ConnectedWithVersion (chainRouter uses it via the
|
||||
// versionedConnector capability); this nil-version entry exists only for the
|
||||
// interface contract and any non-router caller.
|
||||
func (b *blockHandler) Connected(ctx context.Context, nodeID ids.NodeID) error {
|
||||
return b.connect(ctx, nodeID, nil)
|
||||
}
|
||||
|
||||
// ConnectedWithVersion forwards a peer connection WITH its real application
|
||||
// version to this chain's VM. chainRouter invokes this (detecting the
|
||||
// versionedConnector capability) so the version survives to the inner VM:
|
||||
// proposervm promotes Connected to the C-Chain (coreth), whose state-sync peer
|
||||
// tracker compares peer versions — a nil version there dereferences nil and
|
||||
// panics a state-syncing node (a fresh join OR a validator rejoining after
|
||||
// falling behind). This mirrors avalanchego, which delivers msg.NodeVersion to
|
||||
// engine.Connected.
|
||||
func (b *blockHandler) ConnectedWithVersion(ctx context.Context, nodeID ids.NodeID, nodeVersion *version.Application) error {
|
||||
return b.connect(ctx, nodeID, nodeVersion)
|
||||
}
|
||||
|
||||
// connect forwards a peer connection to this chain's VM exactly once, carrying
|
||||
// nodeVersion (nil only on the interface path). The P-chain VM records it in its
|
||||
// uptime tracker; the C-Chain/X-Chain VMs store the version for their peer sets.
|
||||
// A nil connector makes this a no-op. On a forwarding error the dedup entry is
|
||||
// rolled back so a subsequent dispatch of the same connection retries.
|
||||
func (b *blockHandler) connect(ctx context.Context, nodeID ids.NodeID, nodeVersion *version.Application) error {
|
||||
if b.connector == nil {
|
||||
return nil
|
||||
}
|
||||
b.connMu.Lock()
|
||||
if b.connectedNodes.Contains(nodeID) {
|
||||
b.connMu.Unlock()
|
||||
return nil
|
||||
}
|
||||
b.connectedNodes.Add(nodeID)
|
||||
b.connMu.Unlock()
|
||||
|
||||
if err := b.connector.Connected(ctx, nodeID, nodeVersion); err != nil {
|
||||
b.connMu.Lock()
|
||||
b.connectedNodes.Remove(nodeID)
|
||||
b.connMu.Unlock()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Disconnected forwards a peer disconnection to this chain's VM exactly once.
|
||||
func (b *blockHandler) Disconnected(ctx context.Context, nodeID ids.NodeID) error {
|
||||
if b.connector == nil {
|
||||
return nil
|
||||
}
|
||||
b.connMu.Lock()
|
||||
if !b.connectedNodes.Contains(nodeID) {
|
||||
b.connMu.Unlock()
|
||||
return nil
|
||||
}
|
||||
b.connectedNodes.Remove(nodeID)
|
||||
b.connMu.Unlock()
|
||||
|
||||
return b.connector.Disconnected(ctx, nodeID)
|
||||
}
|
||||
func (b *blockHandler) HealthCheck(ctx context.Context) (interface{}, error) { return nil, nil }
|
||||
func (b *blockHandler) Stop(ctx context.Context) {
|
||||
if b.pollerCancel != nil {
|
||||
b.pollerCancel()
|
||||
@@ -4105,6 +4415,10 @@ 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
|
||||
|
||||
@@ -123,8 +123,14 @@ func (m *manager) authorizeChainActivation(chainID ids.ID) (authorized bool, rea
|
||||
// authorization from an NFT held at an address it does not control.
|
||||
|
||||
// The gate consults the X-Chain UTXO set, so the X-Chain must already be
|
||||
// bootstrapped (created and tracked) on this node. If not, we cannot decide
|
||||
// yet — signal the caller to defer.
|
||||
// bootstrapped on this node. IsBootstrapped now keys on REAL convergence
|
||||
// (sb.Bootstrapped), not mere presence — safe here because X-Chain is a DAG
|
||||
// chain marked bootstrapped SYNCHRONOUSLY inside createChain (Engine == nil
|
||||
// path) before the sequential chain-creator dequeues any re-pushed gated chain,
|
||||
// so IsBootstrapped(X) is already true when a gated chain re-runs. INVARIANT: if
|
||||
// X-Chain is ever linearized into an engine chain (async Bootstrapped via
|
||||
// monitorBootstrap), retryPendingGatedChains must be made to re-drain after X
|
||||
// converges, else gated chains park forever. If not bootstrapped yet, defer.
|
||||
if !m.IsBootstrapped(m.XChainID) {
|
||||
return false, false
|
||||
}
|
||||
|
||||
@@ -9,10 +9,12 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/luxfi/constants"
|
||||
"github.com/luxfi/container/buffer"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/math/set"
|
||||
"github.com/luxfi/node/nets"
|
||||
lux "github.com/luxfi/utxo"
|
||||
"github.com/luxfi/utxo/nftfx"
|
||||
"github.com/luxfi/utxo/secp256k1fx"
|
||||
@@ -163,8 +165,10 @@ func TestHoldsAuthorizationNFT(t *testing.T) {
|
||||
|
||||
// newGateManager builds the minimal manager needed to exercise the gate:
|
||||
// only the fields authorizeChainActivation reads. xChainVM, when non-nil, is
|
||||
// installed as the X-Chain's tracked VM so IsBootstrapped(XChainID) is true and
|
||||
// xChainUTXOReader() resolves to it.
|
||||
// installed as the X-Chain's tracked VM AND marked bootstrapped in its net so
|
||||
// IsBootstrapped(XChainID) is true (the gate consults the X-Chain UTXO set, which
|
||||
// is only valid once the X-Chain has finished initial sync — mere presence in
|
||||
// m.chains is no longer sufficient) and xChainUTXOReader() resolves to it.
|
||||
func newGateManager(
|
||||
xChainID ids.ID,
|
||||
stakingAddr ids.ShortID,
|
||||
@@ -172,17 +176,29 @@ func newGateManager(
|
||||
critical set.Set[ids.ID],
|
||||
xChainVM xChainUTXOReader,
|
||||
) *manager {
|
||||
netsTracker, err := NewNets(ids.GenerateTestNodeID(), map[ids.ID]nets.Config{
|
||||
constants.PrimaryNetworkID: {},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
m := &manager{
|
||||
chains: make(map[ids.ID]*chainInfo),
|
||||
gatedAttempts: make(map[ids.ID]int),
|
||||
}
|
||||
m.Log = log.NewNoOpLogger()
|
||||
m.Nets = netsTracker
|
||||
m.XChainID = xChainID
|
||||
m.StakingXAddress = stakingAddr
|
||||
m.ChainAuthorizations = authz
|
||||
m.CriticalChains = critical
|
||||
if xChainVM != nil {
|
||||
m.chains[xChainID] = &chainInfo{Name: "X-Chain", VM: xChainVM}
|
||||
// The X-Chain has finished initial sync (its UTXO set is valid) — the real
|
||||
// precondition the gate depends on, now reflected in the net tracking.
|
||||
sb, _ := netsTracker.GetOrCreate(constants.PrimaryNetworkID)
|
||||
sb.AddChain(xChainID)
|
||||
sb.Bootstrapped(xChainID)
|
||||
}
|
||||
return m
|
||||
}
|
||||
@@ -278,11 +294,14 @@ func TestAuthorizeChainActivation(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("gated chain, X-Chain tracked but VM lacks reader, opts out", func(t *testing.T) {
|
||||
// X-Chain is in m.chains (IsBootstrapped true) but its VM does not
|
||||
// satisfy xChainUTXOReader. The gate must fail closed (ready, !authz),
|
||||
// not panic.
|
||||
// X-Chain is bootstrapped (in m.chains AND marked bootstrapped in its net, so
|
||||
// IsBootstrapped true) but its VM does not satisfy xChainUTXOReader. The gate must
|
||||
// fail closed (ready, !authz), not panic.
|
||||
m := newGateManager(xChainID, addr, collection, nil, nil)
|
||||
m.chains[xChainID] = &chainInfo{Name: "X-Chain", VM: struct{}{}}
|
||||
sb, _ := m.Nets.GetOrCreate(constants.PrimaryNetworkID)
|
||||
sb.AddChain(xChainID)
|
||||
sb.Bootstrapped(xChainID)
|
||||
authorized, ready := m.authorizeChainActivation(gatedChain)
|
||||
require.True(t, ready)
|
||||
require.False(t, authorized)
|
||||
|
||||
@@ -177,6 +177,69 @@ func TestIsBootstrapped(t *testing.T) {
|
||||
require.False(m.IsBootstrapped(chainID))
|
||||
}
|
||||
|
||||
// TestIsBootstrappedTracksRealConvergence is the regression guard for the
|
||||
// premature-true masking bug: manager.IsBootstrapped must report true ONLY once the
|
||||
// chain has ACTUALLY finished initial sync (its net marked it Bootstrapped), NOT the
|
||||
// instant it is merely tracked (added to m.chains with its sync goroutine launched).
|
||||
// Before the fix, a C-Chain stalled at genesis (head 0x0) reported
|
||||
// info.isBootstrapped(C)=true, masking the stall from any readiness gate.
|
||||
func TestIsBootstrappedTracksRealConvergence(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
chainConfigs := map[ids.ID]nets.Config{
|
||||
constants.PrimaryNetworkID: {},
|
||||
}
|
||||
netsTracker, err := NewNets(ids.GenerateTestNodeID(), chainConfigs)
|
||||
require.NoError(err)
|
||||
|
||||
config := &ManagerConfig{
|
||||
Log: log.NewNoOpLogger(),
|
||||
Metrics: metric.NewMultiGatherer(),
|
||||
VMManager: vms.NewManager(),
|
||||
ChainDataDir: t.TempDir(),
|
||||
Nets: netsTracker,
|
||||
}
|
||||
m, err := New(config)
|
||||
require.NoError(err)
|
||||
mImpl := m.(*manager)
|
||||
|
||||
// A native chain validated by the primary network — the C-Chain shape.
|
||||
chainID := ids.GenerateTestID()
|
||||
|
||||
// Simulate createChain's tracking: the chain EXISTS in m.chains and is registered
|
||||
// as bootstrapping in its validation net — but has NOT converged (initial sync is
|
||||
// still driving, e.g. stalled at genesis fetching ancestry).
|
||||
mImpl.chainsLock.Lock()
|
||||
mImpl.chains[chainID] = &chainInfo{Name: "C-Chain"}
|
||||
mImpl.chainsLock.Unlock()
|
||||
sb, _ := netsTracker.GetOrCreate(constants.PrimaryNetworkID)
|
||||
require.True(sb.AddChain(chainID))
|
||||
|
||||
// THE FIX: exists-but-not-converged must be FALSE (was true — the masking bug).
|
||||
require.False(m.IsBootstrapped(chainID),
|
||||
"a tracked-but-still-syncing chain must not report bootstrapped")
|
||||
for _, ci := range m.(*manager).GetChains() {
|
||||
if ci.ID == chainID {
|
||||
require.False(ci.Bootstrapped, "GetChains must not report a syncing chain bootstrapped")
|
||||
}
|
||||
}
|
||||
|
||||
// Initial sync reaches the frontier → monitorBootstrap calls sb.Bootstrapped.
|
||||
sb.Bootstrapped(chainID)
|
||||
|
||||
// Now — and only now — it reports bootstrapped (head advanced to frontier, VM live).
|
||||
require.True(m.IsBootstrapped(chainID),
|
||||
"a converged chain must report bootstrapped")
|
||||
found := false
|
||||
for _, ci := range m.(*manager).GetChains() {
|
||||
if ci.ID == chainID {
|
||||
found = true
|
||||
require.True(ci.Bootstrapped, "GetChains must report a converged chain bootstrapped")
|
||||
}
|
||||
}
|
||||
require.True(found)
|
||||
}
|
||||
|
||||
// TestToEngineChannelFlow verifies the toEngine channel notification flow
|
||||
// This tests the goroutine that reads from toEngine and triggers block building
|
||||
func TestToEngineChannelFlow(t *testing.T) {
|
||||
|
||||
+15
-1
@@ -89,7 +89,7 @@ func selectConsensusParams(sybilProtection bool, networkID uint32) consensusconf
|
||||
|
||||
// shouldWrapInProposerVM decides whether a linear chain.ChainVM is wrapped in
|
||||
// proposervm to enforce single-proposer-per-height block production (the
|
||||
// Snowman++ window). It is the SINGLE policy gate (the manager calls it once);
|
||||
// proposer window). It is the SINGLE policy gate (the manager calls it once);
|
||||
// keeping it a pure function makes the policy unit-testable without standing up
|
||||
// a whole chain. All three conditions must hold:
|
||||
//
|
||||
@@ -267,6 +267,16 @@ func (s *validatorStakeSource) TotalStake(height uint64) uint64 {
|
||||
return total
|
||||
}
|
||||
|
||||
// ValidatorCount implements consensuschain.StakeSource. The number of DISTINCT
|
||||
// validators in the set IN FORCE AT height — the round-scoped view-change's BFT
|
||||
// committee size (it sizes its POL/precommit quorum to bftAlpha over this count,
|
||||
// NOT the oversized sample K). Read from the SAME height-indexed set as
|
||||
// Weight/TotalStake so every node computes the identical committee and the
|
||||
// count-quorum matches the ⅔-by-stake set exactly.
|
||||
func (s *validatorStakeSource) ValidatorCount(height uint64) int {
|
||||
return len(validatorSetAtHeight(s.state, s.networkID, height))
|
||||
}
|
||||
|
||||
var _ consensuschain.StakeSource = (*validatorStakeSource)(nil)
|
||||
|
||||
// --- validator-set-root source (MEDIUM: epoch binding) -----------------------
|
||||
@@ -364,11 +374,15 @@ func hashValidatorSet(set map[ids.NodeID]*validators.GetValidatorOutput) ids.ID
|
||||
//
|
||||
// kind 1 = signed vote (payload = engine encodeSignedVote: nodeID+sig)
|
||||
// kind 2 = finality cert (payload = engine cert MarshalBinary)
|
||||
// (round-scoped view-change prevotes are engine-INTERNAL since consensus v1.36 —
|
||||
// the node no longer frames or routes a prevote kind)
|
||||
var quorumGossipMagic = [4]byte{'L', 'X', 'Q', 0x01}
|
||||
|
||||
const (
|
||||
quorumKindVote byte = 1
|
||||
quorumKindCert byte = 2
|
||||
// kind 3 (prevote) was DELETED with the v1.36 view-change rip-out (174af3c31); Nova sampling
|
||||
// decides and the ⅔ Quasar attestation rides quorumKindVote. Do not reuse 3 — keep the braid dead.
|
||||
)
|
||||
|
||||
// ErrNotQuorumGossip signals a payload is not a quorum envelope (so the caller
|
||||
|
||||
@@ -1,244 +0,0 @@
|
||||
# Robust RPC Handler Registration System
|
||||
|
||||
## Overview
|
||||
|
||||
This package provides a bulletproof RPC handler registration system for the Lux node, designed to handle the complexities of local development where nodes are frequently restarted. It replaces the fragile inline registration logic with a robust, maintainable solution.
|
||||
|
||||
## Key Features
|
||||
|
||||
### 🔄 Automatic Retry Logic
|
||||
- Exponential backoff for transient failures
|
||||
- Configurable retry count and wait times
|
||||
- Context-aware cancellation support
|
||||
|
||||
### ✅ Built-in Health Checks
|
||||
- Automatic validation after registration
|
||||
- Batch health checking for all chains
|
||||
- Detailed diagnostics for failures
|
||||
|
||||
### 🎯 Single Source of Truth
|
||||
- Centralized route construction logic
|
||||
- Consistent path formatting
|
||||
- No duplicate code or magic strings
|
||||
|
||||
### 🛡️ Defensive Programming
|
||||
- Nil checks on all inputs
|
||||
- Handler validation before registration
|
||||
- Graceful degradation on failures
|
||||
|
||||
### 📊 Developer-Friendly Debugging
|
||||
- Clear, actionable error messages
|
||||
- Comprehensive logging at appropriate levels
|
||||
- Built-in diagnostic tools
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ Chain Manager │
|
||||
└──────────┬──────────┘
|
||||
│ Creates Chain
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ ChainHandlerRegistrar│
|
||||
└──────────┬──────────┘
|
||||
│ Extracts Handlers
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ Handler Manager │
|
||||
└──────────┬──────────┘
|
||||
│ Registers with Retries
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ API Server │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Integration
|
||||
|
||||
Replace the handler registration code in `chains/manager.go` (lines 941-990) with:
|
||||
|
||||
```go
|
||||
// Create robust registrar
|
||||
registrar := rpc.NewChainHandlerRegistrar(
|
||||
m.Server,
|
||||
m.Log,
|
||||
m.CChainID,
|
||||
m.PChainID,
|
||||
)
|
||||
|
||||
// Register handlers
|
||||
if err := registrar.RegisterChainHandlers(ctx, chainParams.ID, chain.VM); err != nil {
|
||||
m.Log.Error("Failed to register handlers", log.Err(err))
|
||||
// Decide if this should be fatal or not
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
```go
|
||||
// Development environment - fail fast
|
||||
registrar.SetRetryConfig(2, 50*time.Millisecond)
|
||||
|
||||
// Production environment - more robust
|
||||
registrar.SetRetryConfig(5, 200*time.Millisecond)
|
||||
```
|
||||
|
||||
### Debugging
|
||||
|
||||
```go
|
||||
// Get route information
|
||||
info, exists := registrar.GetRouteInfo(chainID)
|
||||
if exists {
|
||||
fmt.Printf("Chain %s routes: %v\n", chainID, info.Endpoints)
|
||||
}
|
||||
|
||||
// Run health checks
|
||||
results := registrar.HealthCheckAll()
|
||||
for chainID, healthy := range results {
|
||||
fmt.Printf("Chain %s: %v\n", chainID, healthy)
|
||||
}
|
||||
|
||||
// Validate specific endpoint
|
||||
err := registrar.ValidateEndpoint(chainID, "/rpc")
|
||||
```
|
||||
|
||||
### Using the Debug Tool
|
||||
|
||||
```go
|
||||
// Quick diagnosis from CLI
|
||||
rpc.QuickDiagnose("localhost:9650", chainID, "C")
|
||||
|
||||
// Programmatic diagnosis
|
||||
tool := rpc.NewDebugTool("localhost:9650", logger)
|
||||
report := tool.DiagnoseEndpoint(chainID, "C")
|
||||
fmt.Println(report.String())
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
### HandlerManager (`handler_manager.go`)
|
||||
Core registration logic with retry mechanism and health checks.
|
||||
|
||||
**Key Methods:**
|
||||
- `RegisterChainHandlers()` - Main registration entry point
|
||||
- `HealthCheckRoute()` - Validates handler responsiveness
|
||||
- `GetRouteInfo()` - Retrieves registration details
|
||||
|
||||
### ChainHandlerRegistrar (`chain_integration.go`)
|
||||
Bridge between chain manager and handler manager.
|
||||
|
||||
**Key Methods:**
|
||||
- `RegisterChainHandlers()` - Extracts and registers handlers
|
||||
- `ValidateEndpoint()` - Tests specific endpoints
|
||||
- `GetAllRoutes()` - Returns all registered routes
|
||||
|
||||
### DebugTool (`debug_tool.go`)
|
||||
Comprehensive endpoint diagnostics for developers.
|
||||
|
||||
**Key Methods:**
|
||||
- `DiagnoseEndpoint()` - Full endpoint analysis
|
||||
- `QuickDiagnose()` - CLI-friendly diagnosis
|
||||
|
||||
## Error Handling
|
||||
|
||||
The system uses clear, actionable errors:
|
||||
|
||||
```go
|
||||
errNilHandler = errors.New("handler is nil")
|
||||
errNilServer = errors.New("server is nil")
|
||||
errEmptyEndpoint = errors.New("endpoint is empty")
|
||||
errRegistrationFailed = errors.New("handler registration failed")
|
||||
errHealthCheckFailed = errors.New("health check failed")
|
||||
```
|
||||
|
||||
Each error includes context about what failed and why.
|
||||
|
||||
## Testing
|
||||
|
||||
Comprehensive test coverage including:
|
||||
- Successful registration scenarios
|
||||
- Validation failure cases
|
||||
- Retry logic verification
|
||||
- Health check validation
|
||||
- Context cancellation
|
||||
- Performance benchmarks
|
||||
|
||||
Run tests:
|
||||
```bash
|
||||
go test ./chains/rpc/... -v
|
||||
```
|
||||
|
||||
## Common Issues and Solutions
|
||||
|
||||
### Issue: Handlers not accessible after registration
|
||||
**Solution:** Check health status with `HealthCheckAll()` and review debug output.
|
||||
|
||||
### Issue: Registration fails with "already exists"
|
||||
**Solution:** The retry logic handles this. If persistent, check for duplicate registration attempts.
|
||||
|
||||
### Issue: Slow registration during development
|
||||
**Solution:** Reduce retry count and wait time using `SetRetryConfig()`.
|
||||
|
||||
### Issue: Can't find the correct endpoint URL
|
||||
**Solution:** Use `DebugTool.DiagnoseEndpoint()` to test all URL patterns.
|
||||
|
||||
## Migration Guide
|
||||
|
||||
1. **Update imports:**
|
||||
```go
|
||||
import "github.com/luxfi/node/chains/rpc"
|
||||
```
|
||||
|
||||
2. **Replace inline registration (lines 941-990 in manager.go):**
|
||||
```go
|
||||
// Old code: complex type checking and manual registration
|
||||
// New code: single function call
|
||||
registrar := rpc.NewChainHandlerRegistrar(...)
|
||||
registrar.RegisterChainHandlers(...)
|
||||
```
|
||||
|
||||
3. **Add health monitoring (optional):**
|
||||
```go
|
||||
go func() {
|
||||
time.Sleep(5 * time.Second)
|
||||
registrar.HealthCheckAll()
|
||||
}()
|
||||
```
|
||||
|
||||
4. **Add debugging endpoints (optional):**
|
||||
```go
|
||||
http.HandleFunc("/debug/handlers", func(w http.ResponseWriter, r *http.Request) {
|
||||
routes := registrar.GetAllRoutes()
|
||||
json.NewEncoder(w).Encode(routes)
|
||||
})
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
- Registration: ~1ms per handler (without retries)
|
||||
- Health check: ~10ms per chain
|
||||
- Memory overhead: ~1KB per registered chain
|
||||
- No goroutine leaks or resource issues
|
||||
|
||||
## Future Improvements
|
||||
|
||||
Potential enhancements:
|
||||
- Metrics integration for registration success/failure rates
|
||||
- Automatic re-registration on failure
|
||||
- WebSocket-specific health checks
|
||||
- gRPC handler support
|
||||
- Handler versioning for upgrades
|
||||
|
||||
## Philosophy
|
||||
|
||||
This implementation follows core Go principles:
|
||||
- **Explicit over implicit** - Clear registration flow
|
||||
- **Errors are values** - Proper error handling throughout
|
||||
- **Simple over clever** - Straightforward retry logic
|
||||
- **Composition over inheritance** - Small, focused components
|
||||
- **Documentation is code** - Self-documenting with clear names
|
||||
|
||||
The system is designed to be bulletproof for development while remaining simple to understand and maintain.
|
||||
@@ -1,168 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/node/server/http"
|
||||
"github.com/luxfi/node/vms"
|
||||
)
|
||||
|
||||
// ChainHandlerRegistrar provides a clean interface for chain manager to register handlers.
|
||||
// This replaces the inline registration logic with a more robust, testable solution.
|
||||
type ChainHandlerRegistrar struct {
|
||||
manager *HandlerManager
|
||||
server server.Server
|
||||
log log.Logger
|
||||
cChainID ids.ID // Special handling for C-Chain
|
||||
pChainID ids.ID // Platform chain ID for validation
|
||||
}
|
||||
|
||||
// NewChainHandlerRegistrar creates a registrar for chain handler registration.
|
||||
// Encapsulates all the registration logic in one place.
|
||||
func NewChainHandlerRegistrar(
|
||||
server server.Server,
|
||||
logger log.Logger,
|
||||
cChainID ids.ID,
|
||||
pChainID ids.ID,
|
||||
) *ChainHandlerRegistrar {
|
||||
return &ChainHandlerRegistrar{
|
||||
manager: NewHandlerManager(server, logger),
|
||||
server: server,
|
||||
log: logger,
|
||||
cChainID: cChainID,
|
||||
pChainID: pChainID,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterChainHandlers is the main entry point from chain manager.
|
||||
// Handles all the complexity of VM type checking and handler extraction.
|
||||
func (r *ChainHandlerRegistrar) RegisterChainHandlers(
|
||||
ctx context.Context,
|
||||
chainID ids.ID,
|
||||
vm interface{},
|
||||
) error {
|
||||
r.log.Info("Attempting to register chain handlers",
|
||||
log.Stringer("chainID", chainID),
|
||||
log.String("vmType", fmt.Sprintf("%T", vm)))
|
||||
|
||||
// Don't register handlers for Platform VM
|
||||
if chainID == r.pChainID {
|
||||
r.log.Debug("Skipping handler registration for Platform VM")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Extract handlers from VM
|
||||
handlers, err := r.extractHandlers(ctx, vm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to extract handlers: %w", err)
|
||||
}
|
||||
|
||||
if len(handlers) == 0 {
|
||||
r.log.Info("VM does not provide any handlers",
|
||||
log.Stringer("chainID", chainID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Determine chain alias (special case for C-Chain)
|
||||
alias := r.getChainAlias(chainID)
|
||||
|
||||
// Register with robust handler manager
|
||||
return r.manager.RegisterChainHandlers(ctx, chainID, alias, handlers)
|
||||
}
|
||||
|
||||
// extractHandlers attempts to get handlers from the VM using multiple strategies.
|
||||
// Handles different VM wrapper types gracefully.
|
||||
func (r *ChainHandlerRegistrar) extractHandlers(
|
||||
ctx context.Context,
|
||||
vm interface{},
|
||||
) (map[string]http.Handler, error) {
|
||||
// First try direct interface check
|
||||
if provider, ok := vm.(vms.HandlerProvider); ok {
|
||||
r.log.Debug("VM directly implements HandlerProvider")
|
||||
return provider.CreateHandlers(ctx)
|
||||
}
|
||||
|
||||
// Try using the delegate helper (handles wrapped VMs)
|
||||
handlers, err := vms.DelegateHandlers(ctx, vm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("handler delegation failed: %w", err)
|
||||
}
|
||||
|
||||
if len(handlers) > 0 {
|
||||
r.log.Debug("Successfully extracted handlers via delegation",
|
||||
log.Int("count", len(handlers)))
|
||||
}
|
||||
|
||||
return handlers, nil
|
||||
}
|
||||
|
||||
// getChainAlias returns the appropriate alias for a chain.
|
||||
// C-Chain gets special treatment, others use their ID.
|
||||
func (r *ChainHandlerRegistrar) getChainAlias(chainID ids.ID) string {
|
||||
if chainID == r.cChainID {
|
||||
return "C"
|
||||
}
|
||||
// Could extend this for X-Chain and P-Chain if needed
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetRouteInfo returns information about a specific chain's registered routes.
|
||||
// Useful for debugging and operational visibility.
|
||||
func (r *ChainHandlerRegistrar) GetRouteInfo(chainID ids.ID) (*RouteInfo, bool) {
|
||||
return r.manager.GetRouteInfo(chainID)
|
||||
}
|
||||
|
||||
// GetAllRoutes returns all registered routes across all chains.
|
||||
// Complete visibility for monitoring and debugging.
|
||||
func (r *ChainHandlerRegistrar) GetAllRoutes() map[string]*RouteInfo {
|
||||
return r.manager.GetAllRoutes()
|
||||
}
|
||||
|
||||
// HealthCheckAll performs health checks on all registered routes.
|
||||
// Returns a map of chainID -> healthy status.
|
||||
func (r *ChainHandlerRegistrar) HealthCheckAll() map[string]bool {
|
||||
return r.manager.HealthCheckAll()
|
||||
}
|
||||
|
||||
// SetRetryConfig allows tuning of retry behavior for different environments.
|
||||
// Production might want more retries, dev might want faster failures.
|
||||
func (r *ChainHandlerRegistrar) SetRetryConfig(maxRetries int, initialWait time.Duration) {
|
||||
r.manager.SetRetryConfig(maxRetries, initialWait)
|
||||
}
|
||||
|
||||
// ValidateEndpoint performs a test request against a specific endpoint.
|
||||
// Useful for debugging specific handler issues.
|
||||
func (r *ChainHandlerRegistrar) ValidateEndpoint(
|
||||
chainID ids.ID,
|
||||
endpoint string,
|
||||
) error {
|
||||
info, exists := r.manager.GetRouteInfo(chainID)
|
||||
if !exists {
|
||||
return fmt.Errorf("no routes registered for chain %s", chainID)
|
||||
}
|
||||
|
||||
// Build the full URL
|
||||
fullURL := fmt.Sprintf("/ext/%s%s", info.Base, endpoint)
|
||||
|
||||
r.log.Info("Validating endpoint",
|
||||
log.Stringer("chainID", chainID),
|
||||
log.String("url", fullURL))
|
||||
|
||||
// Validates endpoint registration
|
||||
for _, registered := range info.Endpoints {
|
||||
if registered == endpoint {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("endpoint %s not found in registered endpoints: %v",
|
||||
endpoint, info.Endpoints)
|
||||
}
|
||||
@@ -1,302 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/go-json-experiment/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
)
|
||||
|
||||
// DebugTool provides utilities for debugging RPC handler issues.
|
||||
// Developer-friendly diagnostics with clear, actionable output.
|
||||
type DebugTool struct {
|
||||
baseURL string
|
||||
client *http.Client
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
// NewDebugTool creates a debug tool for RPC endpoint testing.
|
||||
func NewDebugTool(baseURL string, logger log.Logger) *DebugTool {
|
||||
if !strings.HasPrefix(baseURL, "http") {
|
||||
baseURL = "http://" + baseURL
|
||||
}
|
||||
|
||||
return &DebugTool{
|
||||
baseURL: strings.TrimSuffix(baseURL, "/"),
|
||||
client: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
log: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// DiagnoseEndpoint performs comprehensive diagnostics on an RPC endpoint.
|
||||
// Returns detailed information about what's working and what's not.
|
||||
func (d *DebugTool) DiagnoseEndpoint(chainID ids.ID, alias string) *DiagnosticReport {
|
||||
report := &DiagnosticReport{
|
||||
ChainID: chainID,
|
||||
Alias: alias,
|
||||
Timestamp: time.Now(),
|
||||
Tests: make([]TestResult, 0),
|
||||
}
|
||||
|
||||
// Test different URL patterns
|
||||
urlPatterns := d.getURLPatterns(chainID, alias)
|
||||
|
||||
for _, pattern := range urlPatterns {
|
||||
result := d.testEndpoint(pattern)
|
||||
report.Tests = append(report.Tests, result)
|
||||
}
|
||||
|
||||
// Test common RPC methods
|
||||
if bestURL := report.GetBestURL(); bestURL != "" {
|
||||
report.RPCTests = d.testRPCMethods(bestURL)
|
||||
}
|
||||
|
||||
return report
|
||||
}
|
||||
|
||||
// getURLPatterns returns all possible URL patterns to test.
|
||||
func (d *DebugTool) getURLPatterns(chainID ids.ID, alias string) []string {
|
||||
patterns := []string{
|
||||
fmt.Sprintf("%s/ext/bc/%s/rpc", d.baseURL, chainID.String()),
|
||||
fmt.Sprintf("%s/ext/bc/%s/ws", d.baseURL, chainID.String()),
|
||||
fmt.Sprintf("%s/ext/bc/%s", d.baseURL, chainID.String()),
|
||||
}
|
||||
|
||||
if alias != "" && alias != chainID.String() {
|
||||
patterns = append(patterns,
|
||||
fmt.Sprintf("%s/ext/bc/%s/rpc", d.baseURL, alias),
|
||||
fmt.Sprintf("%s/ext/bc/%s/ws", d.baseURL, alias),
|
||||
fmt.Sprintf("%s/ext/bc/%s", d.baseURL, alias),
|
||||
)
|
||||
}
|
||||
|
||||
// Also test without /ext prefix (some setups might differ)
|
||||
patterns = append(patterns,
|
||||
fmt.Sprintf("%s/bc/%s/rpc", d.baseURL, chainID.String()),
|
||||
)
|
||||
|
||||
return patterns
|
||||
}
|
||||
|
||||
// testEndpoint tests a single endpoint URL.
|
||||
func (d *DebugTool) testEndpoint(url string) TestResult {
|
||||
result := TestResult{
|
||||
URL: url,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
// First try a simple GET
|
||||
resp, err := d.client.Get(url)
|
||||
if err != nil {
|
||||
result.Error = fmt.Sprintf("GET failed: %v", err)
|
||||
result.Success = false
|
||||
return result
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
result.StatusCode = resp.StatusCode
|
||||
|
||||
// Read body for debugging
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
result.Response = string(bodyBytes)
|
||||
|
||||
// Now try a POST with JSON-RPC
|
||||
rpcReq := map[string]interface{}{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "web3_clientVersion",
|
||||
"params": []interface{}{},
|
||||
"id": 1,
|
||||
}
|
||||
|
||||
jsonBytes, _ := json.Marshal(rpcReq)
|
||||
postResp, err := d.client.Post(url, "application/json", bytes.NewReader(jsonBytes))
|
||||
if err != nil {
|
||||
result.Error = fmt.Sprintf("POST failed: %v", err)
|
||||
result.Success = false
|
||||
return result
|
||||
}
|
||||
defer postResp.Body.Close()
|
||||
|
||||
result.StatusCode = postResp.StatusCode
|
||||
|
||||
// Check if we got a valid JSON-RPC response
|
||||
var rpcResp map[string]interface{}
|
||||
if err := json.UnmarshalRead(postResp.Body, &rpcResp); err == nil {
|
||||
if _, hasResult := rpcResp["result"]; hasResult {
|
||||
result.Success = true
|
||||
result.Response = fmt.Sprintf("Valid JSON-RPC response: %v", rpcResp["result"])
|
||||
} else if errObj, hasError := rpcResp["error"]; hasError {
|
||||
result.Success = false
|
||||
result.Response = fmt.Sprintf("JSON-RPC error: %v", errObj)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// testRPCMethods tests common RPC methods against an endpoint.
|
||||
func (d *DebugTool) testRPCMethods(url string) []RPCTest {
|
||||
methods := []string{
|
||||
"web3_clientVersion",
|
||||
"eth_blockNumber",
|
||||
"eth_chainId",
|
||||
"net_version",
|
||||
"eth_syncing",
|
||||
}
|
||||
|
||||
tests := make([]RPCTest, 0, len(methods))
|
||||
|
||||
for _, method := range methods {
|
||||
test := RPCTest{
|
||||
Method: method,
|
||||
URL: url,
|
||||
}
|
||||
|
||||
req := map[string]interface{}{
|
||||
"jsonrpc": "2.0",
|
||||
"method": method,
|
||||
"params": []interface{}{},
|
||||
"id": 1,
|
||||
}
|
||||
|
||||
jsonBytes, _ := json.Marshal(req)
|
||||
resp, err := d.client.Post(url, "application/json", bytes.NewReader(jsonBytes))
|
||||
if err != nil {
|
||||
test.Error = err.Error()
|
||||
test.Success = false
|
||||
} else {
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.UnmarshalRead(resp.Body, &result); err == nil {
|
||||
if res, ok := result["result"]; ok {
|
||||
test.Success = true
|
||||
test.Result = fmt.Sprintf("%v", res)
|
||||
} else if errObj, ok := result["error"]; ok {
|
||||
test.Error = fmt.Sprintf("%v", errObj)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tests = append(tests, test)
|
||||
}
|
||||
|
||||
return tests
|
||||
}
|
||||
|
||||
// DiagnosticReport contains comprehensive endpoint diagnostic information.
|
||||
type DiagnosticReport struct {
|
||||
ChainID ids.ID
|
||||
Alias string
|
||||
Timestamp time.Time
|
||||
Tests []TestResult
|
||||
RPCTests []RPCTest
|
||||
}
|
||||
|
||||
// TestResult represents a single endpoint test result.
|
||||
type TestResult struct {
|
||||
URL string
|
||||
Success bool
|
||||
StatusCode int
|
||||
Response string
|
||||
Error string
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// RPCTest represents a test of a specific RPC method.
|
||||
type RPCTest struct {
|
||||
Method string
|
||||
URL string
|
||||
Success bool
|
||||
Result string
|
||||
Error string
|
||||
}
|
||||
|
||||
// GetBestURL returns the first working URL from the tests.
|
||||
func (r *DiagnosticReport) GetBestURL() string {
|
||||
for _, test := range r.Tests {
|
||||
if test.Success {
|
||||
return test.URL
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// String returns a human-readable report.
|
||||
func (r *DiagnosticReport) String() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(fmt.Sprintf("=== RPC Endpoint Diagnostic Report ===\n"))
|
||||
b.WriteString(fmt.Sprintf("Chain ID: %s\n", r.ChainID))
|
||||
if r.Alias != "" {
|
||||
b.WriteString(fmt.Sprintf("Alias: %s\n", r.Alias))
|
||||
}
|
||||
b.WriteString(fmt.Sprintf("Timestamp: %s\n\n", r.Timestamp.Format(time.RFC3339)))
|
||||
|
||||
b.WriteString("=== Endpoint Tests ===\n")
|
||||
for _, test := range r.Tests {
|
||||
status := "❌ FAILED"
|
||||
if test.Success {
|
||||
status = "✅ SUCCESS"
|
||||
}
|
||||
b.WriteString(fmt.Sprintf("\n%s %s\n", status, test.URL))
|
||||
b.WriteString(fmt.Sprintf(" Status Code: %d\n", test.StatusCode))
|
||||
if test.Error != "" {
|
||||
b.WriteString(fmt.Sprintf(" Error: %s\n", test.Error))
|
||||
}
|
||||
if test.Response != "" && len(test.Response) < 200 {
|
||||
b.WriteString(fmt.Sprintf(" Response: %s\n", test.Response))
|
||||
}
|
||||
}
|
||||
|
||||
if len(r.RPCTests) > 0 {
|
||||
b.WriteString("\n=== RPC Method Tests ===\n")
|
||||
for _, test := range r.RPCTests {
|
||||
status := "❌"
|
||||
if test.Success {
|
||||
status = "✅"
|
||||
}
|
||||
b.WriteString(fmt.Sprintf("%s %s: ", status, test.Method))
|
||||
if test.Success {
|
||||
b.WriteString(test.Result)
|
||||
} else {
|
||||
b.WriteString(test.Error)
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("\n=== Recommendations ===\n")
|
||||
if bestURL := r.GetBestURL(); bestURL != "" {
|
||||
b.WriteString(fmt.Sprintf("✅ Use this endpoint: %s\n", bestURL))
|
||||
} else {
|
||||
b.WriteString("❌ No working endpoints found. Check:\n")
|
||||
b.WriteString(" 1. Is the node running?\n")
|
||||
b.WriteString(" 2. Is the chain bootstrapped?\n")
|
||||
b.WriteString(" 3. Are handlers properly registered?\n")
|
||||
b.WriteString(" 4. Check node logs for handler registration errors\n")
|
||||
b.WriteString(" 5. Try restarting the node\n")
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// QuickDiagnose performs a quick endpoint check and prints results.
|
||||
// Convenience function for CLI tools.
|
||||
func QuickDiagnose(nodeURL string, chainID ids.ID, alias string) {
|
||||
logger := log.NewNoOpLogger()
|
||||
tool := NewDebugTool(nodeURL, logger)
|
||||
report := tool.DiagnoseEndpoint(chainID, alias)
|
||||
fmt.Println(report.String())
|
||||
}
|
||||
@@ -1,324 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Package rpc provides robust RPC handler registration with retries, health checks, and clear debugging.
|
||||
// Follows Go principles: fail fast with clear errors, single responsibility, minimal dependencies.
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/node/server/http"
|
||||
)
|
||||
|
||||
var (
|
||||
// Errors follow Go convention: lowercase, descriptive, actionable
|
||||
errNilHandler = errors.New("handler is nil")
|
||||
errNilServer = errors.New("server is nil")
|
||||
errEmptyEndpoint = errors.New("endpoint is empty")
|
||||
errRegistrationFailed = errors.New("handler registration failed")
|
||||
errHealthCheckFailed = errors.New("health check failed")
|
||||
)
|
||||
|
||||
// HandlerManager manages RPC handler registration with robust error handling and health checks.
|
||||
// Single responsibility: reliable handler registration with observability.
|
||||
type HandlerManager struct {
|
||||
server server.Server
|
||||
log log.Logger
|
||||
mu sync.RWMutex
|
||||
routes map[string]*RouteInfo // chainID -> route info
|
||||
retries int // max registration retries
|
||||
retryWait time.Duration // initial retry wait time
|
||||
}
|
||||
|
||||
// RouteInfo contains complete information about a registered route.
|
||||
// Everything needed for debugging in one place.
|
||||
type RouteInfo struct {
|
||||
ChainID ids.ID
|
||||
ChainAlias string
|
||||
Base string // e.g., "bc/C" or "bc/<chainID>"
|
||||
Endpoints []string // e.g., ["/rpc", "/ws"]
|
||||
Handler http.Handler
|
||||
Healthy bool
|
||||
LastCheck time.Time
|
||||
}
|
||||
|
||||
// NewHandlerManager creates a handler manager with sensible defaults.
|
||||
// Simple factory, no magic.
|
||||
func NewHandlerManager(server server.Server, logger log.Logger) *HandlerManager {
|
||||
return &HandlerManager{
|
||||
server: server,
|
||||
log: logger,
|
||||
routes: make(map[string]*RouteInfo),
|
||||
retries: 3,
|
||||
retryWait: 100 * time.Millisecond,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterChainHandlers registers all handlers for a chain with retry logic and health checks.
|
||||
// This is the main entry point - handles everything needed for robust registration.
|
||||
func (m *HandlerManager) RegisterChainHandlers(
|
||||
ctx context.Context,
|
||||
chainID ids.ID,
|
||||
chainAlias string,
|
||||
handlers map[string]http.Handler,
|
||||
) error {
|
||||
if m.server == nil {
|
||||
return errNilServer
|
||||
}
|
||||
|
||||
m.log.Info("Starting chain handler registration",
|
||||
log.Stringer("chainID", chainID),
|
||||
log.String("alias", chainAlias),
|
||||
log.Int("handlerCount", len(handlers)))
|
||||
|
||||
// Validate handlers first - fail fast
|
||||
if err := m.validateHandlers(handlers); err != nil {
|
||||
return fmt.Errorf("handler validation failed: %w", err)
|
||||
}
|
||||
|
||||
// Build route info
|
||||
info := &RouteInfo{
|
||||
ChainID: chainID,
|
||||
ChainAlias: chainAlias,
|
||||
Endpoints: make([]string, 0, len(handlers)),
|
||||
}
|
||||
|
||||
// Determine base paths
|
||||
bases := m.getBasePaths(chainID, chainAlias)
|
||||
|
||||
// Register each handler with retries
|
||||
var registrationErrors []error
|
||||
for endpoint, handler := range handlers {
|
||||
info.Endpoints = append(info.Endpoints, endpoint)
|
||||
|
||||
for _, base := range bases {
|
||||
if err := m.registerWithRetry(ctx, base, endpoint, handler); err != nil {
|
||||
registrationErrors = append(registrationErrors,
|
||||
fmt.Errorf("failed to register %s%s: %w", base, endpoint, err))
|
||||
m.log.Error("Handler registration failed",
|
||||
log.String("base", base),
|
||||
log.String("endpoint", endpoint),
|
||||
log.Err(err))
|
||||
} else {
|
||||
m.log.Info("Handler registered successfully",
|
||||
log.String("route", fmt.Sprintf("/ext/%s%s", base, endpoint)),
|
||||
log.Stringer("chainID", chainID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store route info for monitoring
|
||||
m.mu.Lock()
|
||||
info.Base = bases[0] // Primary base
|
||||
info.Handler = handlers["/rpc"] // Store primary handler for health checks
|
||||
m.routes[chainID.String()] = info
|
||||
m.mu.Unlock()
|
||||
|
||||
// Run health checks
|
||||
if err := m.healthCheckRoute(info); err != nil {
|
||||
m.log.Warn("Health check failed for newly registered chain",
|
||||
log.Stringer("chainID", chainID),
|
||||
log.Err(err))
|
||||
}
|
||||
|
||||
// Return aggregate error if any registrations failed
|
||||
if len(registrationErrors) > 0 {
|
||||
return fmt.Errorf("%w: %v", errRegistrationFailed, registrationErrors)
|
||||
}
|
||||
|
||||
m.log.Info("Chain handler registration completed",
|
||||
log.Stringer("chainID", chainID),
|
||||
log.String("routes", strings.Join(m.getFullRoutes(bases, info.Endpoints), ", ")))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateHandlers ensures all handlers are valid before attempting registration.
|
||||
// Fail fast with clear errors - no silent failures.
|
||||
func (m *HandlerManager) validateHandlers(handlers map[string]http.Handler) error {
|
||||
if len(handlers) == 0 {
|
||||
return errors.New("no handlers provided")
|
||||
}
|
||||
|
||||
for endpoint, handler := range handlers {
|
||||
if handler == nil {
|
||||
return fmt.Errorf("%w for endpoint %s", errNilHandler, endpoint)
|
||||
}
|
||||
if endpoint == "" {
|
||||
return errEmptyEndpoint
|
||||
}
|
||||
// Ensure endpoint starts with /
|
||||
if !strings.HasPrefix(endpoint, "/") {
|
||||
return fmt.Errorf("endpoint %s must start with /", endpoint)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getBasePaths returns all base paths for a chain (with and without alias).
|
||||
// Single source of truth for path construction.
|
||||
func (m *HandlerManager) getBasePaths(chainID ids.ID, chainAlias string) []string {
|
||||
bases := []string{}
|
||||
|
||||
// If we have an alias (like "C" for C-Chain), use it as primary
|
||||
if chainAlias != "" && chainAlias != chainID.String() {
|
||||
bases = append(bases, fmt.Sprintf("bc/%s", chainAlias))
|
||||
}
|
||||
|
||||
// Always include the full chain ID path
|
||||
bases = append(bases, fmt.Sprintf("bc/%s", chainID.String()))
|
||||
|
||||
return bases
|
||||
}
|
||||
|
||||
// getFullRoutes constructs full route paths for logging.
|
||||
// Clear, complete information for operators.
|
||||
func (m *HandlerManager) getFullRoutes(bases []string, endpoints []string) []string {
|
||||
routes := []string{}
|
||||
for _, base := range bases {
|
||||
for _, endpoint := range endpoints {
|
||||
routes = append(routes, fmt.Sprintf("/ext/%s%s", base, endpoint))
|
||||
}
|
||||
}
|
||||
return routes
|
||||
}
|
||||
|
||||
// registerWithRetry attempts registration with exponential backoff.
|
||||
// Handles transient failures gracefully.
|
||||
func (m *HandlerManager) registerWithRetry(
|
||||
ctx context.Context,
|
||||
base string,
|
||||
endpoint string,
|
||||
handler http.Handler,
|
||||
) error {
|
||||
wait := m.retryWait
|
||||
var lastErr error
|
||||
|
||||
for attempt := 0; attempt < m.retries; attempt++ {
|
||||
// Check context cancellation
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
// Try registration
|
||||
if err := m.server.AddRoute(handler, base, endpoint); err == nil {
|
||||
return nil // Success!
|
||||
} else {
|
||||
lastErr = err
|
||||
m.log.Debug("Registration attempt failed, retrying",
|
||||
log.Int("attempt", attempt+1),
|
||||
log.String("base", base),
|
||||
log.String("endpoint", endpoint),
|
||||
log.Err(err))
|
||||
}
|
||||
|
||||
// Don't wait after last attempt
|
||||
if attempt < m.retries-1 {
|
||||
select {
|
||||
case <-time.After(wait):
|
||||
wait *= 2 // Exponential backoff
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed after %d attempts: %w", m.retries, lastErr)
|
||||
}
|
||||
|
||||
// healthCheckRoute performs a basic health check on a registered route.
|
||||
// Validates that handlers are actually responding.
|
||||
func (m *HandlerManager) healthCheckRoute(info *RouteInfo) error {
|
||||
if info.Handler == nil {
|
||||
return fmt.Errorf("no handler to check for chain %s", info.ChainID)
|
||||
}
|
||||
|
||||
// Create a test request
|
||||
req := httptest.NewRequest("POST", "/", strings.NewReader(`{"jsonrpc":"2.0","method":"web3_clientVersion","params":[],"id":1}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Record the response
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
// Call the handler
|
||||
info.Handler.ServeHTTP(recorder, req)
|
||||
|
||||
// Check response
|
||||
info.LastCheck = time.Now()
|
||||
if recorder.Code == http.StatusOK || recorder.Code == http.StatusMethodNotAllowed {
|
||||
info.Healthy = true
|
||||
m.log.Debug("Health check passed",
|
||||
log.Stringer("chainID", info.ChainID),
|
||||
log.Int("status", recorder.Code))
|
||||
return nil
|
||||
}
|
||||
|
||||
info.Healthy = false
|
||||
return fmt.Errorf("%w: status %d", errHealthCheckFailed, recorder.Code)
|
||||
}
|
||||
|
||||
// GetRouteInfo returns information about a registered chain's routes.
|
||||
// Useful for debugging and monitoring.
|
||||
func (m *HandlerManager) GetRouteInfo(chainID ids.ID) (*RouteInfo, bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
info, exists := m.routes[chainID.String()]
|
||||
return info, exists
|
||||
}
|
||||
|
||||
// GetAllRoutes returns all registered route information.
|
||||
// Complete visibility for operators.
|
||||
func (m *HandlerManager) GetAllRoutes() map[string]*RouteInfo {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
// Return a copy to prevent external modification
|
||||
routes := make(map[string]*RouteInfo, len(m.routes))
|
||||
for k, v := range m.routes {
|
||||
routes[k] = v
|
||||
}
|
||||
return routes
|
||||
}
|
||||
|
||||
// HealthCheckAll performs health checks on all registered routes.
|
||||
// Batch operation for monitoring systems.
|
||||
func (m *HandlerManager) HealthCheckAll() map[string]bool {
|
||||
m.mu.RLock()
|
||||
routes := make([]*RouteInfo, 0, len(m.routes))
|
||||
for _, info := range m.routes {
|
||||
routes = append(routes, info)
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
|
||||
results := make(map[string]bool)
|
||||
for _, info := range routes {
|
||||
err := m.healthCheckRoute(info)
|
||||
results[info.ChainID.String()] = err == nil
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// SetRetryConfig allows customization of retry behavior.
|
||||
// Flexibility for different deployment scenarios.
|
||||
func (m *HandlerManager) SetRetryConfig(maxRetries int, initialWait time.Duration) {
|
||||
if maxRetries > 0 {
|
||||
m.retries = maxRetries
|
||||
}
|
||||
if initialWait > 0 {
|
||||
m.retryWait = initialWait
|
||||
}
|
||||
}
|
||||
@@ -1,291 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/node/server/http"
|
||||
"github.com/luxfi/runtime"
|
||||
"github.com/luxfi/vm"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// mockServer implements a test server for handler registration
|
||||
type mockServer struct {
|
||||
routes map[string]http.Handler
|
||||
failCount int
|
||||
maxFailures int
|
||||
returnError error
|
||||
aliases map[string][]string
|
||||
}
|
||||
|
||||
func newMockServer() *mockServer {
|
||||
return &mockServer{
|
||||
routes: make(map[string]http.Handler),
|
||||
maxFailures: 0,
|
||||
aliases: make(map[string][]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *mockServer) AddRoute(handler http.Handler, base, endpoint string) error {
|
||||
// Simulate transient failures for retry testing
|
||||
if s.failCount < s.maxFailures {
|
||||
s.failCount++
|
||||
return errors.New("transient failure")
|
||||
}
|
||||
|
||||
// Return configured error if any
|
||||
if s.returnError != nil {
|
||||
return s.returnError
|
||||
}
|
||||
|
||||
// Store the route
|
||||
key := base + endpoint
|
||||
s.routes[key] = handler
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *mockServer) AddAliases(endpoint string, aliases ...string) error {
|
||||
s.aliases[endpoint] = aliases
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *mockServer) AddRouteWithReadLock(handler http.Handler, base, endpoint string) error {
|
||||
return s.AddRoute(handler, base, endpoint)
|
||||
}
|
||||
|
||||
func (s *mockServer) AddAliasesWithReadLock(endpoint string, aliases ...string) error {
|
||||
return s.AddAliases(endpoint, aliases...)
|
||||
}
|
||||
|
||||
func (s *mockServer) Dispatch() error { return nil }
|
||||
func (s *mockServer) RegisterChain(chainName string, rt *runtime.Runtime, vm vm.VM) {
|
||||
}
|
||||
func (s *mockServer) Shutdown() error { return nil }
|
||||
func (s *mockServer) SetRootInfoProvider(_ server.RootInfoProvider) {}
|
||||
|
||||
func TestHandlerManager_RegisterChainHandlers(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
chainID ids.ID
|
||||
chainAlias string
|
||||
handlers map[string]http.Handler
|
||||
serverError error
|
||||
expectError bool
|
||||
expectRoutes int
|
||||
}{
|
||||
{
|
||||
name: "successful registration with alias",
|
||||
chainID: ids.GenerateTestID(),
|
||||
chainAlias: "C",
|
||||
handlers: map[string]http.Handler{
|
||||
"/rpc": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}),
|
||||
"/ws": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}),
|
||||
},
|
||||
expectError: false,
|
||||
expectRoutes: 4, // 2 endpoints × 2 bases (alias + ID)
|
||||
},
|
||||
{
|
||||
name: "successful registration without alias",
|
||||
chainID: ids.GenerateTestID(),
|
||||
handlers: map[string]http.Handler{
|
||||
"/rpc": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}),
|
||||
},
|
||||
expectError: false,
|
||||
expectRoutes: 1,
|
||||
},
|
||||
{
|
||||
name: "nil handler validation",
|
||||
chainID: ids.GenerateTestID(),
|
||||
chainAlias: "X",
|
||||
handlers: map[string]http.Handler{"/rpc": nil},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "empty endpoint validation",
|
||||
chainID: ids.GenerateTestID(),
|
||||
handlers: map[string]http.Handler{"": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "no handlers provided",
|
||||
chainID: ids.GenerateTestID(),
|
||||
handlers: map[string]http.Handler{},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "invalid endpoint format",
|
||||
chainID: ids.GenerateTestID(),
|
||||
handlers: map[string]http.Handler{"rpc": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})},
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Setup
|
||||
server := newMockServer()
|
||||
server.returnError = tt.serverError
|
||||
logger := log.NewNoOpLogger()
|
||||
manager := NewHandlerManager(server, logger)
|
||||
|
||||
// Execute
|
||||
ctx := context.Background()
|
||||
err := manager.RegisterChainHandlers(ctx, tt.chainID, tt.chainAlias, tt.handlers)
|
||||
|
||||
// Verify
|
||||
if tt.expectError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, server.routes, tt.expectRoutes)
|
||||
|
||||
// Verify route info was stored
|
||||
info, exists := manager.GetRouteInfo(tt.chainID)
|
||||
require.True(t, exists)
|
||||
require.Equal(t, tt.chainID, info.ChainID)
|
||||
require.Equal(t, tt.chainAlias, info.ChainAlias)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerManager_RetryLogic(t *testing.T) {
|
||||
// Setup server that fails twice then succeeds
|
||||
server := newMockServer()
|
||||
server.maxFailures = 2
|
||||
|
||||
logger := log.NewNoOpLogger()
|
||||
manager := NewHandlerManager(server, logger)
|
||||
manager.SetRetryConfig(3, 10*time.Millisecond)
|
||||
|
||||
// Create test handler
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
// Register with retries
|
||||
ctx := context.Background()
|
||||
chainID := ids.GenerateTestID()
|
||||
require.NoError(t, manager.RegisterChainHandlers(ctx, chainID, "TEST", map[string]http.Handler{
|
||||
"/rpc": handler,
|
||||
}))
|
||||
require.Equal(t, 2, server.failCount) // Failed twice, succeeded on third try
|
||||
require.Len(t, server.routes, 2) // Both alias and ID routes
|
||||
}
|
||||
|
||||
func TestHandlerManager_HealthCheck(t *testing.T) {
|
||||
server := newMockServer()
|
||||
logger := log.NewNoOpLogger()
|
||||
manager := NewHandlerManager(server, logger)
|
||||
|
||||
// Register a healthy handler
|
||||
healthyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"jsonrpc":"2.0","result":"test","id":1}`))
|
||||
})
|
||||
|
||||
// Register an unhealthy handler
|
||||
unhealthyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
chainID1 := ids.GenerateTestID()
|
||||
chainID2 := ids.GenerateTestID()
|
||||
|
||||
// Register healthy chain
|
||||
require.NoError(t, manager.RegisterChainHandlers(ctx, chainID1, "", map[string]http.Handler{
|
||||
"/rpc": healthyHandler,
|
||||
}))
|
||||
|
||||
// Register unhealthy chain
|
||||
// Registration succeeds even if health check fails
|
||||
require.NoError(t, manager.RegisterChainHandlers(ctx, chainID2, "", map[string]http.Handler{
|
||||
"/rpc": unhealthyHandler,
|
||||
})) // Registration should succeed regardless of handler health
|
||||
|
||||
// Check health status
|
||||
results := manager.HealthCheckAll()
|
||||
require.True(t, results[chainID1.String()])
|
||||
require.False(t, results[chainID2.String()])
|
||||
}
|
||||
|
||||
func TestHandlerManager_GetBasePaths(t *testing.T) {
|
||||
manager := &HandlerManager{}
|
||||
chainID := ids.GenerateTestID()
|
||||
|
||||
// Test with alias
|
||||
bases := manager.getBasePaths(chainID, "C")
|
||||
require.Equal(t, []string{"bc/C", "bc/" + chainID.String()}, bases)
|
||||
|
||||
// Test without alias
|
||||
bases = manager.getBasePaths(chainID, "")
|
||||
require.Equal(t, []string{"bc/" + chainID.String()}, bases)
|
||||
|
||||
// Test when alias equals chain ID (shouldn't duplicate)
|
||||
bases = manager.getBasePaths(chainID, chainID.String())
|
||||
require.Equal(t, []string{"bc/" + chainID.String()}, bases)
|
||||
}
|
||||
|
||||
func TestHandlerManager_ContextCancellation(t *testing.T) {
|
||||
// Create a server that delays to test cancellation
|
||||
server := &mockServer{
|
||||
routes: make(map[string]http.Handler),
|
||||
returnError: errors.New("slow server"),
|
||||
}
|
||||
|
||||
logger := log.NewNoOpLogger()
|
||||
manager := NewHandlerManager(server, logger)
|
||||
manager.SetRetryConfig(10, 100*time.Millisecond) // Many retries with delays
|
||||
|
||||
// Create cancelled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // Cancel immediately
|
||||
|
||||
// Try to register - should fail with context error
|
||||
chainID := ids.GenerateTestID()
|
||||
err := manager.RegisterChainHandlers(ctx, chainID, "", map[string]http.Handler{
|
||||
"/rpc": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}),
|
||||
})
|
||||
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "context canceled")
|
||||
}
|
||||
|
||||
// Benchmark to ensure performance doesn't degrade
|
||||
func BenchmarkHandlerRegistration(b *testing.B) {
|
||||
server := newMockServer()
|
||||
logger := log.NewNoOpLogger()
|
||||
manager := NewHandlerManager(server, logger)
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
chainID := ids.GenerateTestID()
|
||||
manager.RegisterChainHandlers(ctx, chainID, "TEST", map[string]http.Handler{
|
||||
"/rpc": handler,
|
||||
"/ws": handler,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/node/server/http"
|
||||
)
|
||||
|
||||
// IntegrationExample shows how to modify the existing createChain function in manager.go.
|
||||
// This replaces lines 941-990 with cleaner, more robust code.
|
||||
func IntegrationExample(
|
||||
ctx context.Context,
|
||||
chainID ids.ID,
|
||||
vm interface{},
|
||||
server server.Server,
|
||||
logger log.Logger,
|
||||
cChainID ids.ID,
|
||||
pChainID ids.ID,
|
||||
isDevMode bool,
|
||||
) error {
|
||||
// BEFORE: 50+ lines of complex type checking and error-prone registration
|
||||
// AFTER: Clean, robust registration with proper error handling
|
||||
|
||||
// Step 1: Create the registrar
|
||||
registrar := NewChainHandlerRegistrar(server, logger, cChainID, pChainID)
|
||||
|
||||
// Step 2: Configure based on environment
|
||||
if isDevMode {
|
||||
// Development: Fast failures for quick iteration
|
||||
registrar.SetRetryConfig(2, 50*time.Millisecond)
|
||||
logger.Info("Using development handler registration settings")
|
||||
} else {
|
||||
// Production: More robust with retries
|
||||
registrar.SetRetryConfig(5, 200*time.Millisecond)
|
||||
logger.Info("Using production handler registration settings")
|
||||
}
|
||||
|
||||
// Step 3: Register handlers (replaces all the complex VM type checking)
|
||||
startTime := time.Now()
|
||||
err := registrar.RegisterChainHandlers(ctx, chainID, vm)
|
||||
duration := time.Since(startTime)
|
||||
|
||||
// Step 4: Handle registration result
|
||||
if err != nil {
|
||||
// Log error but don't fail chain creation
|
||||
// Handlers are not critical for chain operation
|
||||
logger.Error("RPC handler registration failed",
|
||||
log.Stringer("chainID", chainID),
|
||||
log.Err(err),
|
||||
log.Duration("duration", duration),
|
||||
log.String("action", "Chain will operate without HTTP/RPC access"))
|
||||
|
||||
// Could emit metrics here if available
|
||||
// metric.HandlerRegistrationFailed.Inc()
|
||||
|
||||
// Non-fatal: return nil to allow chain to continue
|
||||
// Change to 'return err' if you want this to be fatal
|
||||
return nil
|
||||
}
|
||||
|
||||
// Step 5: Log success with useful information
|
||||
if info, exists := registrar.GetRouteInfo(chainID); exists {
|
||||
logger.Info("RPC handlers registered successfully",
|
||||
log.Stringer("chainID", chainID),
|
||||
log.String("alias", info.ChainAlias),
|
||||
log.Strings("endpoints", info.Endpoints),
|
||||
log.Duration("duration", duration),
|
||||
log.Bool("healthCheckPassed", info.Healthy))
|
||||
|
||||
// Print developer-friendly message
|
||||
if isDevMode && len(info.Endpoints) > 0 {
|
||||
baseURL := "http://localhost:9630"
|
||||
fmt.Printf("\n✅ Chain %s RPC endpoints ready:\n", chainID)
|
||||
for _, endpoint := range info.Endpoints {
|
||||
if info.ChainAlias != "" {
|
||||
fmt.Printf(" %s/ext/bc/%s%s\n", baseURL, info.ChainAlias, endpoint)
|
||||
}
|
||||
fmt.Printf(" %s/ext/bc/%s%s\n", baseURL, chainID, endpoint)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
// Step 6: Schedule async health monitoring (optional)
|
||||
if !isDevMode {
|
||||
go monitorHandlerHealth(ctx, registrar, chainID, logger)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// monitorHandlerHealth runs periodic health checks in the background.
|
||||
// This helps detect and log handler issues early.
|
||||
func monitorHandlerHealth(
|
||||
ctx context.Context,
|
||||
registrar *ChainHandlerRegistrar,
|
||||
chainID ids.ID,
|
||||
logger log.Logger,
|
||||
) {
|
||||
// Initial delay to let chain fully initialize
|
||||
select {
|
||||
case <-time.After(10 * time.Second):
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
|
||||
// Run initial health check
|
||||
checkHealth(registrar, chainID, logger)
|
||||
|
||||
// Periodic health checks
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
checkHealth(registrar, chainID, logger)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkHealth performs a health check and logs results.
|
||||
func checkHealth(registrar *ChainHandlerRegistrar, chainID ids.ID, logger log.Logger) {
|
||||
results := registrar.HealthCheckAll()
|
||||
|
||||
for chainIDStr, healthy := range results {
|
||||
if chainIDStr == chainID.String() {
|
||||
if healthy {
|
||||
logger.Debug("Handler health check passed",
|
||||
log.String("chainID", chainIDStr))
|
||||
} else {
|
||||
logger.Warn("Handler health check failed",
|
||||
log.String("chainID", chainIDStr),
|
||||
log.String("action", "Will continue monitoring"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MinimalIntegration shows the absolute minimum code needed.
|
||||
// This is what you'd actually put in manager.go.
|
||||
func MinimalIntegration(
|
||||
ctx context.Context,
|
||||
chainID ids.ID,
|
||||
vm interface{},
|
||||
server server.Server,
|
||||
logger log.Logger,
|
||||
cChainID ids.ID,
|
||||
) error {
|
||||
// Just three lines to replace 50+ lines of complex code!
|
||||
registrar := NewChainHandlerRegistrar(server, logger, cChainID, ids.Empty)
|
||||
if err := registrar.RegisterChainHandlers(ctx, chainID, vm); err != nil {
|
||||
logger.Error("Handler registration failed", log.Err(err))
|
||||
}
|
||||
return nil // Non-fatal
|
||||
}
|
||||
+1
-1
@@ -19,7 +19,7 @@ services:
|
||||
LUXD_CONSENSUS_QUORUM_SIZE: "1"
|
||||
LUXD_LOG_LEVEL: "info"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-sf", "http://localhost:9630/ext/health"]
|
||||
test: ["CMD", "curl", "-sf", "http://localhost:9630/v1/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
|
||||
@@ -1762,6 +1762,14 @@ func GetNodeConfig(v *viper.Viper) (node.Config, error) {
|
||||
if nodeConfig.HealthCheckFreq < 0 {
|
||||
return node.Config{}, fmt.Errorf("%s must be positive", HealthCheckFreqKey)
|
||||
}
|
||||
nodeConfig.ProposerWindowDuration = v.GetDuration(ProposerVMWindowDurationKey)
|
||||
if nodeConfig.ProposerWindowDuration < 0 {
|
||||
return node.Config{}, fmt.Errorf("%s must not be negative", ProposerVMWindowDurationKey)
|
||||
}
|
||||
nodeConfig.ProposerMinBlockDelay = v.GetDuration(ProposerVMMinBlockDelayKey)
|
||||
if nodeConfig.ProposerMinBlockDelay < 0 {
|
||||
return node.Config{}, fmt.Errorf("%s must not be negative", ProposerVMMinBlockDelayKey)
|
||||
}
|
||||
// Halflife of continuous averager used in health checks
|
||||
healthCheckAveragerHalflife := v.GetDuration(HealthCheckAveragerHalflifeKey)
|
||||
if healthCheckAveragerHalflife <= 0 {
|
||||
|
||||
@@ -24,7 +24,6 @@ import (
|
||||
"github.com/luxfi/node/genesis/builder"
|
||||
"github.com/luxfi/node/nets"
|
||||
pchaingenesis "github.com/luxfi/node/vms/platformvm/genesis"
|
||||
pchaintxs "github.com/luxfi/node/vms/platformvm/txs"
|
||||
)
|
||||
|
||||
const chainConfigFilenameExtension = ".ex"
|
||||
@@ -536,8 +535,8 @@ func TestGetNetConfigsFromFlags(t *testing.T) {
|
||||
"2Ctt6eGAeo4MLqTmGa7AdRecuVMPGWEX9wSsCLBYrLhX4a394i": {
|
||||
"consensusParameters": {
|
||||
"k": 30,
|
||||
"alphaPreference": 16,
|
||||
"alphaConfidence": 20
|
||||
"alphaPreference": 20,
|
||||
"alphaConfidence": 25
|
||||
},
|
||||
"validatorOnly": true
|
||||
}
|
||||
@@ -547,8 +546,8 @@ func TestGetNetConfigsFromFlags(t *testing.T) {
|
||||
config, ok := given[id]
|
||||
require.True(ok)
|
||||
require.True(config.ValidatorOnly)
|
||||
require.Equal(16, config.ConsensusParameters.AlphaPreference)
|
||||
require.Equal(20, config.ConsensusParameters.AlphaConfidence)
|
||||
require.Equal(20, config.ConsensusParameters.AlphaPreference)
|
||||
require.Equal(25, config.ConsensusParameters.AlphaConfidence)
|
||||
require.Equal(30, config.ConsensusParameters.K)
|
||||
// must still respect defaults (MainnetParameters.MaxOutstandingItems = 1024)
|
||||
require.Equal(1024, config.ConsensusParameters.MaxOutstandingItems)
|
||||
@@ -733,7 +732,7 @@ func TestResolveUTXOAssetID_POnlyFallback(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
pOnly := &pchaingenesis.Genesis{Chains: nil}
|
||||
pOnlyBytes, err := pchaingenesis.Codec.Marshal(pchaintxs.CodecVersion, pOnly)
|
||||
pOnlyBytes, err := pOnly.Bytes()
|
||||
require.NoError(err)
|
||||
|
||||
gotID, err := resolveUTXOAssetID(42, pOnlyBytes)
|
||||
|
||||
@@ -369,6 +369,7 @@ func addNodeFlags(fs *pflag.FlagSet) {
|
||||
// ProposerVM
|
||||
fs.Bool(ProposerVMUseCurrentHeightKey, false, "Have the ProposerVM always report the last accepted P-chain block height")
|
||||
fs.Duration(ProposerVMMinBlockDelayKey, proposervm.DefaultMinBlockDelay, "Minimum delay to enforce when building a chain++ block for the primary network chains and the default minimum delay for chains")
|
||||
fs.Duration(ProposerVMWindowDurationKey, 0, "Proposer-slot spacing for block production (0 uses the 5s mainnet default); shrink for fast cadence on small/local networks")
|
||||
|
||||
// Metrics
|
||||
fs.Bool(MeterVMsEnabledKey, true, "Enable Meter VMs to track VM performance with more granularity")
|
||||
|
||||
@@ -187,6 +187,7 @@ const (
|
||||
ConsensusFrontierPollFrequencyKey = "consensus-frontier-poll-frequency"
|
||||
ProposerVMUseCurrentHeightKey = "proposervm-use-current-height"
|
||||
ProposerVMMinBlockDelayKey = "proposervm-min-block-delay"
|
||||
ProposerVMWindowDurationKey = "proposervm-window-duration"
|
||||
FdLimitKey = "fd-limit"
|
||||
IndexEnabledKey = "index-enabled"
|
||||
IndexAllowIncompleteKey = "index-allow-incomplete"
|
||||
|
||||
@@ -177,6 +177,16 @@ type Config struct {
|
||||
// Health
|
||||
HealthCheckFreq time.Duration `json:"healthCheckFreq"`
|
||||
|
||||
// ProposerWindowDuration overrides the proposervm proposer-slot spacing.
|
||||
// Zero keeps the 5s mainnet default; small local/dev nets set it low (e.g.
|
||||
// 1s) so block cadence is not floored at 5s per proposer slot.
|
||||
ProposerWindowDuration time.Duration `json:"proposerWindowDuration"`
|
||||
|
||||
// ProposerMinBlockDelay is the proposervm minimum delay between consecutive
|
||||
// blocks (the hard cadence floor). Zero keeps the 1s default; high-throughput
|
||||
// / DEX nets set it low (e.g. 1ms) to approach the consensus-finality floor.
|
||||
ProposerMinBlockDelay time.Duration `json:"proposerMinBlockDelay"`
|
||||
|
||||
// Network configuration
|
||||
NetworkConfig network.Config `json:"networkConfig"`
|
||||
|
||||
|
||||
+5
-5
@@ -133,12 +133,12 @@ deploy:
|
||||
|
||||
### Prometheus Metrics
|
||||
|
||||
The node exposes metrics at `http://localhost:9630/ext/metrics`
|
||||
The node exposes metrics at `http://localhost:9630/v1/metrics`
|
||||
|
||||
### Health Checks
|
||||
|
||||
- Liveness: `http://localhost:9630/ext/health`
|
||||
- Readiness: `http://localhost:9630/ext/info`
|
||||
- Liveness: `http://localhost:9630/v1/health`
|
||||
- Readiness: `http://localhost:9630/v1/info`
|
||||
|
||||
### Grafana Dashboard
|
||||
|
||||
@@ -178,12 +178,12 @@ docker exec -it luxd /bin/bash
|
||||
# Check if bootstrapped
|
||||
curl -X POST -H "Content-Type: application/json" \
|
||||
-d '{"jsonrpc":"2.0","id":1,"method":"info.isBootstrapped","params":{"chain":"C"}}' \
|
||||
http://localhost:9630/ext/info
|
||||
http://localhost:9630/v1/info
|
||||
|
||||
# Get block number
|
||||
curl -X POST -H "Content-Type: application/json" \
|
||||
-d '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}' \
|
||||
http://localhost:9630/ext/bc/C/rpc
|
||||
http://localhost:9630/v1/bc/C/rpc
|
||||
```
|
||||
|
||||
## CI/CD
|
||||
|
||||
@@ -287,10 +287,10 @@ echo "📝 Starting with command:"
|
||||
echo " $CMD"
|
||||
echo ""
|
||||
echo "📡 API Endpoints:"
|
||||
echo " - JSON-RPC: http://$HTTP_HOST:$HTTP_PORT/ext/bc/C/rpc"
|
||||
echo " - WebSocket: ws://$HTTP_HOST:$HTTP_PORT/ext/bc/C/ws"
|
||||
echo " - Health: http://$HTTP_HOST:$HTTP_PORT/ext/health"
|
||||
echo " - Info: http://$HTTP_HOST:$HTTP_PORT/ext/info"
|
||||
echo " - JSON-RPC: http://$HTTP_HOST:$HTTP_PORT/v1/bc/C/rpc"
|
||||
echo " - WebSocket: ws://$HTTP_HOST:$HTTP_PORT/v1/bc/C/ws"
|
||||
echo " - Health: http://$HTTP_HOST:$HTTP_PORT/v1/health"
|
||||
echo " - Info: http://$HTTP_HOST:$HTTP_PORT/v1/info"
|
||||
echo ""
|
||||
|
||||
# Execute
|
||||
|
||||
+1
-1
@@ -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 '{"jsonrpc":"2.0","method":"health.health","id":1}' \{"\n"} -H 'content-type:application/json' \{"\n"} 127.0.0.1:9650/ext/health
|
||||
curl -X POST --data '{"jsonrpc":"2.0","method":"health.health","id":1}' \{"\n"} -H 'content-type:application/json' \{"\n"} 127.0.0.1:9650/v1/health
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
@@ -31,7 +31,7 @@ Or in configuration:
|
||||
## Endpoint
|
||||
|
||||
```
|
||||
http://localhost:9630/ext/admin
|
||||
http://localhost:9630/v1/admin
|
||||
```
|
||||
|
||||
## Methods
|
||||
@@ -54,7 +54,7 @@ curl -X POST --data '{
|
||||
"chain":"2S53R2ub94CV5vmSRAjqPYmRxvuiFunCb1gN2CAw3DQBfPWghX"
|
||||
},
|
||||
"id":1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/admin
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/admin
|
||||
```
|
||||
|
||||
---
|
||||
@@ -85,7 +85,7 @@ curl -X POST --data '{
|
||||
"chain":"2S53R2ub94CV5vmSRAjqPYmRxvuiFunCb1gN2CAw3DQBfPWghX"
|
||||
},
|
||||
"id":1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/admin
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/admin
|
||||
```
|
||||
|
||||
**Response:**
|
||||
@@ -114,7 +114,7 @@ curl -X POST --data '{
|
||||
"method":"admin.getLogLevel",
|
||||
"params":{},
|
||||
"id":1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/admin
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/admin
|
||||
```
|
||||
|
||||
**Response:**
|
||||
@@ -146,7 +146,7 @@ curl -X POST --data '{
|
||||
"logLevel":"debug"
|
||||
},
|
||||
"id":1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/admin
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/admin
|
||||
```
|
||||
|
||||
---
|
||||
@@ -181,7 +181,7 @@ curl -X POST --data '{
|
||||
"method":"admin.loadVMs",
|
||||
"params":{},
|
||||
"id":1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/admin
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/admin
|
||||
```
|
||||
|
||||
**Response:**
|
||||
@@ -214,7 +214,7 @@ curl -X POST --data '{
|
||||
"method":"admin.startCPUProfiler",
|
||||
"params":{},
|
||||
"id":1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/admin
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/admin
|
||||
```
|
||||
|
||||
---
|
||||
@@ -232,7 +232,7 @@ curl -X POST --data '{
|
||||
"method":"admin.stopCPUProfiler",
|
||||
"params":{},
|
||||
"id":1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/admin
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/admin
|
||||
```
|
||||
|
||||
---
|
||||
@@ -250,7 +250,7 @@ curl -X POST --data '{
|
||||
"method":"admin.memoryProfile",
|
||||
"params":{},
|
||||
"id":1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/admin
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/admin
|
||||
```
|
||||
|
||||
---
|
||||
@@ -276,7 +276,7 @@ curl -X POST --data '{
|
||||
"method":"admin.getConfig",
|
||||
"params":{},
|
||||
"id":1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/admin
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/admin
|
||||
```
|
||||
|
||||
**Response:**
|
||||
@@ -311,7 +311,7 @@ curl -X POST --data '{
|
||||
"method":"admin.shutdown",
|
||||
"params":{},
|
||||
"id":1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/admin
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/admin
|
||||
```
|
||||
|
||||
---
|
||||
@@ -330,7 +330,7 @@ curl -X POST --data '{
|
||||
"method":"admin.db.commit",
|
||||
"params":{},
|
||||
"id":1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/admin
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/admin
|
||||
```
|
||||
|
||||
## Profiling and Debugging
|
||||
@@ -344,7 +344,7 @@ curl -X POST --data '{
|
||||
"method":"admin.startCPUProfiler",
|
||||
"params":{},
|
||||
"id":1
|
||||
}' http://localhost:9630/ext/admin
|
||||
}' http://localhost:9630/v1/admin
|
||||
|
||||
# Let it run for 30 seconds
|
||||
sleep 30
|
||||
@@ -355,7 +355,7 @@ curl -X POST --data '{
|
||||
"method":"admin.stopCPUProfiler",
|
||||
"params":{},
|
||||
"id":2
|
||||
}' http://localhost:9630/ext/admin
|
||||
}' http://localhost:9630/v1/admin
|
||||
|
||||
# Analyze profile
|
||||
go tool pprof cpu.prof
|
||||
@@ -370,7 +370,7 @@ curl -X POST --data '{
|
||||
"method":"admin.memoryProfile",
|
||||
"params":{},
|
||||
"id":1
|
||||
}' http://localhost:9630/ext/admin
|
||||
}' http://localhost:9630/v1/admin
|
||||
|
||||
# Analyze
|
||||
go tool pprof mem.prof
|
||||
@@ -401,7 +401,7 @@ set_log_level() {
|
||||
\"method\":\"admin.setLogLevel\",
|
||||
\"params\":{\"logLevel\":\"$LEVEL\"},
|
||||
\"id\":1
|
||||
}" http://localhost:9630/ext/admin
|
||||
}" http://localhost:9630/v1/admin
|
||||
}
|
||||
|
||||
# Increase verbosity for debugging
|
||||
@@ -476,7 +476,7 @@ Enable audit logging for admin operations:
|
||||
# auto_restart.sh
|
||||
|
||||
check_health() {
|
||||
curl -s http://localhost:9630/ext/health | jq -r '.healthy'
|
||||
curl -s http://localhost:9630/v1/health | jq -r '.healthy'
|
||||
}
|
||||
|
||||
restart_node() {
|
||||
@@ -488,7 +488,7 @@ restart_node() {
|
||||
"method":"admin.shutdown",
|
||||
"params":{},
|
||||
"id":1
|
||||
}' http://localhost:9630/ext/admin
|
||||
}' http://localhost:9630/v1/admin
|
||||
|
||||
sleep 10
|
||||
|
||||
@@ -514,7 +514,7 @@ CURRENT=$(curl -s -X POST --data '{
|
||||
"method":"admin.getConfig",
|
||||
"params":{},
|
||||
"id":1
|
||||
}' http://localhost:9630/ext/admin)
|
||||
}' http://localhost:9630/v1/admin)
|
||||
|
||||
echo "Current configuration:"
|
||||
echo $CURRENT | jq .
|
||||
@@ -525,7 +525,7 @@ curl -X POST --data '{
|
||||
"method":"admin.setLogLevel",
|
||||
"params":{"logLevel":"debug"},
|
||||
"id":2
|
||||
}' http://localhost:9630/ext/admin
|
||||
}' http://localhost:9630/v1/admin
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -10,7 +10,7 @@ The Health API provides endpoints to monitor the health and readiness of your Lu
|
||||
## Endpoint
|
||||
|
||||
```
|
||||
http://localhost:9630/ext/health
|
||||
http://localhost:9630/v1/health
|
||||
```
|
||||
|
||||
## Health Check Types
|
||||
@@ -20,7 +20,7 @@ http://localhost:9630/ext/health
|
||||
Get the overall health status of the node:
|
||||
|
||||
```bash
|
||||
curl http://localhost:9630/ext/health
|
||||
curl http://localhost:9630/v1/health
|
||||
```
|
||||
|
||||
**Response:**
|
||||
@@ -61,7 +61,7 @@ curl http://localhost:9630/ext/health
|
||||
Check if the node is ready to serve requests:
|
||||
|
||||
```bash
|
||||
curl http://localhost:9630/ext/health/readiness
|
||||
curl http://localhost:9630/v1/health/readiness
|
||||
```
|
||||
|
||||
Returns:
|
||||
@@ -73,7 +73,7 @@ Returns:
|
||||
Check if the node is alive and running:
|
||||
|
||||
```bash
|
||||
curl http://localhost:9630/ext/health/liveness
|
||||
curl http://localhost:9630/v1/health/liveness
|
||||
```
|
||||
|
||||
Returns:
|
||||
@@ -92,7 +92,7 @@ curl -X POST --data '{
|
||||
"id": 1,
|
||||
"method": "health.health",
|
||||
"params": {}
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/health
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/health
|
||||
```
|
||||
|
||||
**Response:**
|
||||
@@ -175,7 +175,7 @@ Configure per-chain health parameters:
|
||||
Export health metrics in Prometheus format:
|
||||
|
||||
```bash
|
||||
curl http://localhost:9630/ext/metrics | grep health
|
||||
curl http://localhost:9630/v1/metrics | grep health
|
||||
```
|
||||
|
||||
Metrics:
|
||||
@@ -208,13 +208,13 @@ spec:
|
||||
image: luxfi/node:latest
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /ext/health/liveness
|
||||
path: /v1/health/liveness
|
||||
port: 9630
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /ext/health/readiness
|
||||
path: /v1/health/readiness
|
||||
port: 9630
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 5
|
||||
@@ -229,7 +229,7 @@ spec:
|
||||
# health_monitor.sh
|
||||
|
||||
while true; do
|
||||
HEALTH=$(curl -s http://localhost:9630/ext/health | jq -r '.healthy')
|
||||
HEALTH=$(curl -s http://localhost:9630/v1/health | jq -r '.healthy')
|
||||
|
||||
if [ "$HEALTH" != "true" ]; then
|
||||
echo "ALERT: Node unhealthy at $(date)"
|
||||
@@ -248,7 +248,7 @@ done
|
||||
# health_analysis.sh
|
||||
|
||||
# Get detailed health
|
||||
RESPONSE=$(curl -s http://localhost:9630/ext/health)
|
||||
RESPONSE=$(curl -s http://localhost:9630/v1/health)
|
||||
|
||||
# Parse each chain
|
||||
for CHAIN in P X C Q; do
|
||||
@@ -271,7 +271,7 @@ done
|
||||
|
||||
```
|
||||
backend lux_nodes
|
||||
option httpchk GET /ext/health
|
||||
option httpchk GET /v1/health
|
||||
http-check expect status 200
|
||||
|
||||
server node1 192.168.1.10:9630 check
|
||||
@@ -289,7 +289,7 @@ upstream lux_nodes {
|
||||
}
|
||||
|
||||
location /health_check {
|
||||
proxy_pass http://lux_nodes/ext/health;
|
||||
proxy_pass http://lux_nodes/v1/health;
|
||||
proxy_connect_timeout 1s;
|
||||
proxy_read_timeout 1s;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ The Info API provides general information about the node and network status. Thi
|
||||
## Endpoint
|
||||
|
||||
```
|
||||
http://localhost:9630/ext/info
|
||||
http://localhost:9630/v1/info
|
||||
```
|
||||
|
||||
## Format
|
||||
@@ -23,7 +23,7 @@ curl -X POST --data '{
|
||||
"method": "info.<method>",
|
||||
"params": {...},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
|
||||
```
|
||||
|
||||
## Methods
|
||||
@@ -41,7 +41,7 @@ curl -X POST --data '{
|
||||
"method": "info.getNodeVersion",
|
||||
"params": {},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
@@ -78,7 +78,7 @@ curl -X POST --data '{
|
||||
"method": "info.getNodeID",
|
||||
"params": {},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
@@ -111,7 +111,7 @@ curl -X POST --data '{
|
||||
"method": "info.getNodeIP",
|
||||
"params": {},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
@@ -140,7 +140,7 @@ curl -X POST --data '{
|
||||
"method": "info.getNetworkID",
|
||||
"params": {},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
@@ -175,7 +175,7 @@ curl -X POST --data '{
|
||||
"method": "info.getNetworkName",
|
||||
"params": {},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
@@ -207,7 +207,7 @@ curl -X POST --data '{
|
||||
"alias": "P"
|
||||
},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
@@ -239,7 +239,7 @@ curl -X POST --data '{
|
||||
"chain": "P"
|
||||
},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
@@ -269,7 +269,7 @@ curl -X POST --data '{
|
||||
"method": "info.peers",
|
||||
"params": {},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
@@ -313,7 +313,7 @@ curl -X POST --data '{
|
||||
"method": "info.getTxFee",
|
||||
"params": {},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
@@ -348,7 +348,7 @@ curl -X POST --data '{
|
||||
"method": "info.uptime",
|
||||
"params": {},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
|
||||
```
|
||||
|
||||
**Example Response (Validator):**
|
||||
@@ -390,7 +390,7 @@ curl -X POST --data '{
|
||||
"method": "info.getVMs",
|
||||
"params": {},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
@@ -424,7 +424,7 @@ curl -X POST --data '{
|
||||
"method": "info.getUpgrades",
|
||||
"params": {},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
@@ -468,7 +468,7 @@ VERSION=$(curl -s -X POST --data '{
|
||||
"method": "info.getNodeVersion",
|
||||
"params": {},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' $NODE_URL/ext/info | jq -r '.result.version')
|
||||
}' -H 'content-type:application/json;' $NODE_URL/v1/info | jq -r '.result.version')
|
||||
|
||||
echo "Version: $VERSION"
|
||||
|
||||
@@ -478,7 +478,7 @@ NODE_ID=$(curl -s -X POST --data '{
|
||||
"method": "info.getNodeID",
|
||||
"params": {},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' $NODE_URL/ext/info | jq -r '.result.nodeID')
|
||||
}' -H 'content-type:application/json;' $NODE_URL/v1/info | jq -r '.result.nodeID')
|
||||
|
||||
echo "Node ID: $NODE_ID"
|
||||
|
||||
@@ -488,7 +488,7 @@ PEERS=$(curl -s -X POST --data '{
|
||||
"method": "info.peers",
|
||||
"params": {},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' $NODE_URL/ext/info | jq -r '.result.numPeers')
|
||||
}' -H 'content-type:application/json;' $NODE_URL/v1/info | jq -r '.result.numPeers')
|
||||
|
||||
echo "Connected Peers: $PEERS"
|
||||
|
||||
@@ -499,7 +499,7 @@ for CHAIN in P X C Q; do
|
||||
\"method\": \"info.isBootstrapped\",
|
||||
\"params\": {\"chain\": \"$CHAIN\"},
|
||||
\"id\": 1
|
||||
}" -H 'content-type:application/json;' $NODE_URL/ext/info | jq -r '.result.isBootstrapped')
|
||||
}" -H 'content-type:application/json;' $NODE_URL/v1/info | jq -r '.result.isBootstrapped')
|
||||
|
||||
echo "$CHAIN-Chain Bootstrapped: $STATUS"
|
||||
done
|
||||
@@ -510,7 +510,7 @@ UPTIME=$(curl -s -X POST --data '{
|
||||
"method": "info.uptime",
|
||||
"params": {},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' $NODE_URL/ext/info 2>/dev/null | jq -r '.result.rewardingStakePercentage' 2>/dev/null)
|
||||
}' -H 'content-type:application/json;' $NODE_URL/v1/info 2>/dev/null | jq -r '.result.rewardingStakePercentage' 2>/dev/null)
|
||||
|
||||
if [ "$UPTIME" != "null" ] && [ -n "$UPTIME" ]; then
|
||||
echo "Validator Uptime: $UPTIME%"
|
||||
@@ -531,7 +531,7 @@ PEERS=$(curl -s -X POST --data '{
|
||||
"method": "info.peers",
|
||||
"params": {},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' $NODE_URL/ext/info | jq -r '.result.peers[]')
|
||||
}' -H 'content-type:application/json;' $NODE_URL/v1/info | jq -r '.result.peers[]')
|
||||
|
||||
echo "=== Peer Connection Quality ==="
|
||||
echo "Node ID | Latency (ms) | Uptime % | Version"
|
||||
@@ -560,7 +560,7 @@ while true; do
|
||||
\"method\": \"info.isBootstrapped\",
|
||||
\"params\": {\"chain\": \"$CHAIN\"},
|
||||
\"id\": 1
|
||||
}" -H 'content-type:application/json;' $NODE_URL/ext/info | jq -r '.result.isBootstrapped')
|
||||
}" -H 'content-type:application/json;' $NODE_URL/v1/info | jq -r '.result.isBootstrapped')
|
||||
|
||||
if [ "$STATUS" == "true" ]; then
|
||||
echo "✅ $CHAIN-Chain: Bootstrapped"
|
||||
@@ -575,7 +575,7 @@ while true; do
|
||||
"method": "info.peers",
|
||||
"params": {},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' $NODE_URL/ext/info | jq -r '.result.numPeers')
|
||||
}' -H 'content-type:application/json;' $NODE_URL/v1/info | jq -r '.result.numPeers')
|
||||
|
||||
echo ""
|
||||
echo "Connected Peers: $PEERS"
|
||||
@@ -588,7 +588,7 @@ while true; do
|
||||
\"method\": \"info.isBootstrapped\",
|
||||
\"params\": {\"chain\": \"$CHAIN\"},
|
||||
\"id\": 1
|
||||
}" -H 'content-type:application/json;' $NODE_URL/ext/info | jq -r '.result.isBootstrapped')
|
||||
}" -H 'content-type:application/json;' $NODE_URL/v1/info | jq -r '.result.isBootstrapped')
|
||||
|
||||
if [ "$STATUS" != "true" ]; then
|
||||
ALL_BOOTSTRAPPED=false
|
||||
|
||||
@@ -10,7 +10,7 @@ The Platform Chain (P-Chain) is responsible for staking, validators, and chain m
|
||||
## Endpoint
|
||||
|
||||
```
|
||||
http://localhost:9630/ext/bc/P
|
||||
http://localhost:9630/v1/bc/P
|
||||
```
|
||||
|
||||
## Format
|
||||
@@ -23,7 +23,7 @@ curl -X POST --data '{
|
||||
"method": "platform.<method>",
|
||||
"params": {...},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
```
|
||||
|
||||
## Methods
|
||||
@@ -41,7 +41,7 @@ curl -X POST --data '{
|
||||
"method": "platform.getHeight",
|
||||
"params": {},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
@@ -73,7 +73,7 @@ curl -X POST --data '{
|
||||
"addresses": ["P-lux1q8tgunsf7sxpl2qp9fz0xjs36kstm7vdjvpzcc"]
|
||||
},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
@@ -114,7 +114,7 @@ curl -X POST --data '{
|
||||
"limit": 100
|
||||
},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
```
|
||||
|
||||
---
|
||||
@@ -134,7 +134,7 @@ curl -X POST --data '{
|
||||
"method": "platform.getCurrentValidators",
|
||||
"params": {},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
@@ -190,7 +190,7 @@ curl -X POST --data '{
|
||||
"method": "platform.getPendingValidators",
|
||||
"params": {},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
```
|
||||
|
||||
---
|
||||
@@ -212,7 +212,7 @@ curl -X POST --data '{
|
||||
"height": 365000
|
||||
},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
```
|
||||
|
||||
---
|
||||
@@ -233,7 +233,7 @@ curl -X POST --data '{
|
||||
"height": 365000
|
||||
},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
```
|
||||
|
||||
---
|
||||
@@ -255,7 +255,7 @@ curl -X POST --data '{
|
||||
"addresses": ["P-lux1q8tgunsf7sxpl2qp9fz0xjs36kstm7vdjvpzcc"]
|
||||
},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
```
|
||||
|
||||
---
|
||||
@@ -274,7 +274,7 @@ curl -X POST --data '{
|
||||
"method": "platform.getMinStake",
|
||||
"params": {},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
@@ -305,7 +305,7 @@ curl -X POST --data '{
|
||||
"method": "platform.getTotalStake",
|
||||
"params": {},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
```
|
||||
|
||||
---
|
||||
@@ -327,7 +327,7 @@ curl -X POST --data '{
|
||||
"txID": "2nmH8LithVbdjaXsxVQCQfXtzN9hBbmebrsaEYnLM9T32Uy3Y5"
|
||||
},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
```
|
||||
|
||||
---
|
||||
@@ -345,7 +345,7 @@ curl -X POST --data '{
|
||||
"method": "platform.getTimestamp",
|
||||
"params": {},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
```
|
||||
|
||||
---
|
||||
@@ -363,7 +363,7 @@ curl -X POST --data '{
|
||||
"method": "platform.getBlockchains",
|
||||
"params": {},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
@@ -409,7 +409,7 @@ curl -X POST --data '{
|
||||
"blockID": "vXSY7FK7NR65Y8BrJDKQH4a6vBdJuqAMvVzWj3Zxcap5J4ZE3"
|
||||
},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
```
|
||||
|
||||
---
|
||||
@@ -431,7 +431,7 @@ curl -X POST --data '{
|
||||
"txID": "2nmH8LithVbdjaXsxVQCQfXtzN9hBbmebrsaEYnLM9T32Uy3Y5"
|
||||
},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
```
|
||||
|
||||
---
|
||||
@@ -452,7 +452,7 @@ curl -X POST --data '{
|
||||
"txID": "2nmH8LithVbdjaXsxVQCQfXtzN9hBbmebrsaEYnLM9T32Uy3Y5"
|
||||
},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
@@ -488,7 +488,7 @@ curl -X POST --data '{
|
||||
"method": "platform.getCurrentSupply",
|
||||
"params": {},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
```
|
||||
|
||||
---
|
||||
@@ -507,7 +507,7 @@ curl -X POST --data '{
|
||||
"method": "platform.getChains",
|
||||
"params": {},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
```
|
||||
|
||||
---
|
||||
@@ -544,7 +544,7 @@ curl -X POST --data '{
|
||||
"password": "mypassword"
|
||||
},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
```
|
||||
|
||||
---
|
||||
@@ -694,7 +694,7 @@ curl -s -X POST --data "{
|
||||
\"nodeIDs\": [\"$NODE_ID\"]
|
||||
},
|
||||
\"id\": 1
|
||||
}" -H 'content-type:application/json;' http://localhost:9630/ext/bc/P | jq '.result.validators[0]'
|
||||
}" -H 'content-type:application/json;' http://localhost:9630/v1/bc/P | jq '.result.validators[0]'
|
||||
```
|
||||
|
||||
### Monitor Staking Rewards
|
||||
@@ -711,7 +711,7 @@ STAKE=$(curl -s -X POST --data "{
|
||||
\"addresses\": [\"$ADDRESS\"]
|
||||
},
|
||||
\"id\": 1
|
||||
}" -H 'content-type:application/json;' http://localhost:9630/ext/bc/P)
|
||||
}" -H 'content-type:application/json;' http://localhost:9630/v1/bc/P)
|
||||
|
||||
echo "Current stake: $(echo $STAKE | jq -r '.result.staked')"
|
||||
echo "Stakeable: $(echo $STAKE | jq -r '.result.stakeable')"
|
||||
|
||||
@@ -210,14 +210,14 @@ curl -X POST --data '{
|
||||
"jsonrpc":"2.0",
|
||||
"id": 1,
|
||||
"method": "health.health"
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/health
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/health
|
||||
|
||||
# Get node info
|
||||
curl -X POST --data '{
|
||||
"jsonrpc":"2.0",
|
||||
"id": 1,
|
||||
"method": "info.getNodeVersion"
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
|
||||
|
||||
# Check bootstrap status
|
||||
curl -X POST --data '{
|
||||
@@ -227,7 +227,7 @@ curl -X POST --data '{
|
||||
"params": {
|
||||
"chain": "P"
|
||||
}
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -308,7 +308,7 @@ curl -X POST --data '{
|
||||
"jsonrpc":"2.0",
|
||||
"id": 1,
|
||||
"method": "health.health"
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/health
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/health
|
||||
```
|
||||
|
||||
### Bootstrap Status
|
||||
@@ -320,7 +320,7 @@ curl -X POST --data '{
|
||||
"id": 1,
|
||||
"method": "info.isBootstrapped",
|
||||
"params": {"chain": "P"}
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
|
||||
```
|
||||
|
||||
### Metrics
|
||||
@@ -328,7 +328,7 @@ curl -X POST --data '{
|
||||
Access Prometheus metrics:
|
||||
|
||||
```bash
|
||||
curl http://localhost:9630/ext/metrics
|
||||
curl http://localhost:9630/v1/metrics
|
||||
```
|
||||
|
||||
Key metrics to monitor:
|
||||
|
||||
@@ -58,7 +58,7 @@ curl -X POST --data '{
|
||||
"jsonrpc":"2.0",
|
||||
"id": 1,
|
||||
"method": "platform.getHeight"
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
|
||||
# Check bootstrap status for all chains
|
||||
curl -X POST --data '{
|
||||
@@ -66,7 +66,7 @@ curl -X POST --data '{
|
||||
"id": 1,
|
||||
"method": "info.isBootstrapped",
|
||||
"params": {"chain": "P"}
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
|
||||
```
|
||||
|
||||
### Configure Public IP
|
||||
@@ -90,7 +90,7 @@ curl -X POST --data '{
|
||||
"jsonrpc":"2.0",
|
||||
"id": 1,
|
||||
"method": "info.getNodeID"
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/info
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/info
|
||||
```
|
||||
|
||||
Response:
|
||||
@@ -155,7 +155,7 @@ curl -X POST --data '{
|
||||
"password": "mypassword"
|
||||
},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/keystore
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/keystore
|
||||
|
||||
# Create P-Chain address
|
||||
curl -X POST --data '{
|
||||
@@ -166,7 +166,7 @@ curl -X POST --data '{
|
||||
"password": "mypassword"
|
||||
},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
```
|
||||
|
||||
### Transfer LUX to P-Chain
|
||||
@@ -185,7 +185,7 @@ curl -X POST --data '{
|
||||
"amount": 2000000000000
|
||||
},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/X
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/X
|
||||
|
||||
# Import to P-Chain
|
||||
curl -X POST --data '{
|
||||
@@ -197,7 +197,7 @@ curl -X POST --data '{
|
||||
"sourceChain": "X"
|
||||
},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
```
|
||||
|
||||
## Step 4: Add as Validator
|
||||
@@ -221,7 +221,7 @@ curl -X POST --data '{
|
||||
"delegationFeeRate": 10
|
||||
},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
```
|
||||
|
||||
Parameters:
|
||||
@@ -243,7 +243,7 @@ curl -X POST --data '{
|
||||
"method": "platform.getPendingValidators",
|
||||
"params": {},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
|
||||
# Get current validators
|
||||
curl -X POST --data '{
|
||||
@@ -251,7 +251,7 @@ curl -X POST --data '{
|
||||
"method": "platform.getCurrentValidators",
|
||||
"params": {},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
```
|
||||
|
||||
## Step 5: Monitor Your Validator
|
||||
@@ -268,7 +268,7 @@ curl -X POST --data '{
|
||||
"nodeID": "NodeID-5KqnQfaFQxY9rsFBAa68377qWSherYLQ7"
|
||||
},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
```
|
||||
|
||||
### Monitor Performance Metrics
|
||||
@@ -277,10 +277,10 @@ Key metrics to track:
|
||||
|
||||
```bash
|
||||
# Node health
|
||||
curl http://localhost:9630/ext/health
|
||||
curl http://localhost:9630/v1/health
|
||||
|
||||
# Prometheus metrics
|
||||
curl http://localhost:9630/ext/metrics | grep -E "uptime|stake|validator"
|
||||
curl http://localhost:9630/v1/metrics | grep -E "uptime|stake|validator"
|
||||
```
|
||||
|
||||
Important metrics:
|
||||
@@ -308,7 +308,7 @@ NODE_URL="http://localhost:9630"
|
||||
WEBHOOK_URL="your-webhook-url"
|
||||
|
||||
# Check if node is responsive
|
||||
if ! curl -s "$NODE_URL/ext/health" > /dev/null; then
|
||||
if ! curl -s "$NODE_URL/v1/health" > /dev/null; then
|
||||
curl -X POST "$WEBHOOK_URL" -d '{"text":"ALERT: Node is not responding!"}'
|
||||
fi
|
||||
|
||||
@@ -318,7 +318,7 @@ UPTIME=$(curl -s -X POST --data '{
|
||||
"method":"platform.getValidator",
|
||||
"params":{"nodeID":"NodeID-xxx"},
|
||||
"id":1
|
||||
}' "$NODE_URL/ext/bc/P" | jq -r '.result.uptime')
|
||||
}' "$NODE_URL/v1/bc/P" | jq -r '.result.uptime')
|
||||
|
||||
if [ "$UPTIME" -lt "80" ]; then
|
||||
curl -X POST "$WEBHOOK_URL" -d "{\"text\":\"WARNING: Uptime is $UPTIME%\"}"
|
||||
@@ -349,7 +349,7 @@ curl -X POST --data '{
|
||||
"nodeID": "NodeID-5KqnQfaFQxY9rsFBAa68377qWSherYLQ7"
|
||||
},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
```
|
||||
|
||||
## Chain Validation
|
||||
@@ -373,7 +373,7 @@ curl -X POST --data '{
|
||||
"weight": 1000
|
||||
},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
```
|
||||
|
||||
### Chain Requirements
|
||||
|
||||
@@ -104,14 +104,14 @@ curl -X POST --data '{
|
||||
"jsonrpc":"2.0",
|
||||
"id": 1,
|
||||
"method": "info.getNetworkInfo"
|
||||
}' -H 'content-type:application/json;' http://localhost:9650/ext/info
|
||||
}' -H 'content-type:application/json;' http://localhost:9650/v1/info
|
||||
|
||||
# Get node ID
|
||||
curl -X POST --data '{
|
||||
"jsonrpc":"2.0",
|
||||
"id": 1,
|
||||
"method": "info.getNodeID"
|
||||
}' -H 'content-type:application/json;' http://localhost:9650/ext/info
|
||||
}' -H 'content-type:application/json;' http://localhost:9650/v1/info
|
||||
```
|
||||
|
||||
## Chain Management
|
||||
@@ -130,7 +130,7 @@ curl -X POST --data '{
|
||||
"genesisData": "...",
|
||||
"netID": "network-id"
|
||||
}
|
||||
}' -H 'content-type:application/json;' http://localhost:9650/ext/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9650/v1/P
|
||||
```
|
||||
|
||||
### Validator Management
|
||||
@@ -147,7 +147,7 @@ curl -X POST --data '{
|
||||
"endTime": ...,
|
||||
"stakeAmount": ...
|
||||
}
|
||||
}' -H 'content-type:application/json;' http://localhost:9650/ext/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9650/v1/P
|
||||
```
|
||||
|
||||
## Configuration Reference
|
||||
@@ -175,10 +175,10 @@ curl -X POST --data '{
|
||||
|
||||
```bash
|
||||
# Prometheus metrics endpoint
|
||||
curl http://localhost:9650/ext/metrics
|
||||
curl http://localhost:9650/v1/metrics
|
||||
|
||||
# Health check
|
||||
curl http://localhost:9650/ext/health
|
||||
curl http://localhost:9650/v1/health
|
||||
```
|
||||
|
||||
### Logs
|
||||
@@ -193,7 +193,7 @@ curl -X POST --data '{
|
||||
"id": 1,
|
||||
"method": "admin.setLogLevel",
|
||||
"params": {"logLevel": "debug"}
|
||||
}' http://localhost:9650/ext/admin
|
||||
}' http://localhost:9650/v1/admin
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
@@ -595,10 +595,10 @@ curl -X POST --data '{
|
||||
"jsonrpc":"2.0",
|
||||
"method":"info.peers",
|
||||
"id":1
|
||||
}' http://localhost:9630/ext/info
|
||||
}' http://localhost:9630/v1/info
|
||||
|
||||
# Check network metrics
|
||||
curl http://localhost:9630/ext/metrics | grep network
|
||||
curl http://localhost:9630/v1/metrics | grep network
|
||||
|
||||
# Monitor connections
|
||||
netstat -an | grep 9631
|
||||
|
||||
@@ -39,7 +39,7 @@ scrape_configs:
|
||||
- job_name: 'lux-node'
|
||||
static_configs:
|
||||
- targets: ['localhost:9630']
|
||||
metrics_path: '/ext/metrics'
|
||||
metrics_path: '/v1/metrics'
|
||||
|
||||
- job_name: 'node-exporter'
|
||||
static_configs:
|
||||
@@ -56,7 +56,7 @@ alerting:
|
||||
|
||||
### Available Metrics
|
||||
|
||||
The Lux node exposes 400+ Prometheus metrics at `http://localhost:9630/ext/metrics`.
|
||||
The Lux node exposes 400+ Prometheus metrics at `http://localhost:9630/v1/metrics`.
|
||||
|
||||
#### Key Metric Categories
|
||||
|
||||
@@ -419,11 +419,11 @@ groups:
|
||||
|
||||
```bash
|
||||
# Basic health check
|
||||
curl http://localhost:9630/ext/health
|
||||
curl http://localhost:9630/v1/health
|
||||
|
||||
# Detailed health with readiness/liveness
|
||||
curl http://localhost:9630/ext/health/readiness
|
||||
curl http://localhost:9630/ext/health/liveness
|
||||
curl http://localhost:9630/v1/health/readiness
|
||||
curl http://localhost:9630/v1/health/liveness
|
||||
```
|
||||
|
||||
### Custom Health Script
|
||||
@@ -443,7 +443,7 @@ send_alert() {
|
||||
}
|
||||
|
||||
# Check if node is responsive
|
||||
if ! curl -s "$NODE_URL/ext/health" > /dev/null; then
|
||||
if ! curl -s "$NODE_URL/v1/health" > /dev/null; then
|
||||
send_alert "Node is not responding!"
|
||||
exit 1
|
||||
fi
|
||||
@@ -455,7 +455,7 @@ for CHAIN in P X C Q; do
|
||||
\"method\": \"info.isBootstrapped\",
|
||||
\"params\": {\"chain\": \"$CHAIN\"},
|
||||
\"id\": 1
|
||||
}" -H 'content-type:application/json;' $NODE_URL/ext/info | jq -r '.result.isBootstrapped')
|
||||
}" -H 'content-type:application/json;' $NODE_URL/v1/info | jq -r '.result.isBootstrapped')
|
||||
|
||||
if [ "$STATUS" != "true" ]; then
|
||||
send_alert "$CHAIN-Chain is not bootstrapped!"
|
||||
@@ -468,7 +468,7 @@ PEERS=$(curl -s -X POST --data '{
|
||||
"method": "info.peers",
|
||||
"params": {},
|
||||
"id": 1
|
||||
}' -H 'content-type:application/json;' $NODE_URL/ext/info | jq -r '.result.numPeers')
|
||||
}' -H 'content-type:application/json;' $NODE_URL/v1/info | jq -r '.result.numPeers')
|
||||
|
||||
if [ "$PEERS" -lt 4 ]; then
|
||||
send_alert "Low peer count: $PEERS"
|
||||
@@ -619,7 +619,7 @@ Create runbooks for common issues:
|
||||
|
||||
```bash
|
||||
# Real-time metrics
|
||||
watch -n 1 'curl -s http://localhost:9630/ext/metrics | grep -E "chain_height|network_peers"'
|
||||
watch -n 1 'curl -s http://localhost:9630/v1/metrics | grep -E "chain_height|network_peers"'
|
||||
|
||||
# Log streaming
|
||||
tail -f ~/.luxd/logs/*.log | grep --line-buffered ERROR
|
||||
|
||||
@@ -26,7 +26,7 @@ else
|
||||
fi
|
||||
|
||||
# Check API responsiveness
|
||||
if curl -s http://localhost:9630/ext/health > /dev/null 2>&1; then
|
||||
if curl -s http://localhost:9630/v1/health > /dev/null 2>&1; then
|
||||
echo "✅ API is responsive"
|
||||
else
|
||||
echo "❌ API is NOT responsive"
|
||||
@@ -53,7 +53,7 @@ PEERS=$(curl -s -X POST --data '{
|
||||
"jsonrpc":"2.0",
|
||||
"method":"info.peers",
|
||||
"id":1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/info 2>/dev/null | jq -r '.result.numPeers')
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/info 2>/dev/null | jq -r '.result.numPeers')
|
||||
|
||||
if [ -n "$PEERS" ] && [ "$PEERS" -gt 0 ]; then
|
||||
echo "✅ Connected peers: $PEERS"
|
||||
@@ -225,7 +225,7 @@ curl -X POST --data '{
|
||||
"method":"platform.getCurrentValidators",
|
||||
"params":{"nodeIDs":["<your-node-id>"]},
|
||||
"id":1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
|
||||
# Verify staking transaction
|
||||
curl -X POST --data '{
|
||||
@@ -233,7 +233,7 @@ curl -X POST --data '{
|
||||
"method":"platform.getTx",
|
||||
"params":{"txID":"<staking-tx-id>"},
|
||||
"id":1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
```
|
||||
|
||||
#### Issue: Low uptime percentage
|
||||
@@ -275,7 +275,7 @@ curl -X POST --data '{
|
||||
"jsonrpc":"2.0",
|
||||
"method":"platform.getHeight",
|
||||
"id":1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
|
||||
# Reduce consensus participation
|
||||
./build/node --consensus-gossip-concurrent=2
|
||||
@@ -334,7 +334,7 @@ grep "api" ~/.luxd/configs/node-config.json
|
||||
./build/node --http-host=0.0.0.0
|
||||
|
||||
# Check for rate limiting
|
||||
curl -I http://localhost:9630/ext/info
|
||||
curl -I http://localhost:9630/v1/info
|
||||
```
|
||||
|
||||
### Staking Issues
|
||||
@@ -369,7 +369,7 @@ curl -X POST --data '{
|
||||
"method":"platform.getBalance",
|
||||
"params":{"addresses":["P-lux1..."]},
|
||||
"id":1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
|
||||
# Import funds from X-Chain
|
||||
curl -X POST --data '{
|
||||
@@ -381,7 +381,7 @@ curl -X POST --data '{
|
||||
"sourceChain":"X"
|
||||
},
|
||||
"id":1
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/ext/bc/P
|
||||
}' -H 'content-type:application/json;' http://localhost:9630/v1/bc/P
|
||||
```
|
||||
|
||||
## Log Analysis
|
||||
@@ -518,7 +518,7 @@ curl -X POST --data '{
|
||||
"jsonrpc":"2.0",
|
||||
"method":"info.getNodeID",
|
||||
"id":1
|
||||
}' http://localhost:9630/ext/info
|
||||
}' http://localhost:9630/v1/info
|
||||
```
|
||||
|
||||
### Support Channels
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
# Postmortem: C-Chain accepted-head state GC eviction (mainnet freeze)
|
||||
|
||||
**Status:** Resolved. Mainnet recovered and accepted at 5/5 validators, C-Chain
|
||||
height 1085412, hash `0xd957eae6cb0bbef37174…`, all validators in agreement,
|
||||
explorer at tip, treasury and Genesis NFT state verified.
|
||||
|
||||
**Severity:** Critical (mainnet C-Chain unable to build blocks; no state loss).
|
||||
|
||||
## Accepted permanent invariant
|
||||
|
||||
> The accepted C-Chain head's state root must never be GC/pruning eligible —
|
||||
> across idle windows, duplicate empty-block state roots, cold snapshot/cache
|
||||
> layers, small state history, and restarts.
|
||||
>
|
||||
> accepted head ⇒ accepted head state root is pinned ⇒ GC/pruning cannot evict
|
||||
> the execution base for H+1.
|
||||
|
||||
The specific accepted-head GC eviction failure is **structurally prevented** by
|
||||
the head-state pin, and production evidence confirms the fleet no longer
|
||||
exhibits the prior failure signature. (A formal long-idle ritual was not
|
||||
completed to termination during the incident window; the sign-off rests on the
|
||||
structural invariant plus production evidence: a head idle for 3h04m was built
|
||||
on cleanly, multiple 5–15 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.
|
||||
@@ -75,9 +75,9 @@ func NewMultiNetworkNode() *MultiNetworkNode {
|
||||
|
||||
// StartRPCServer starts the unified RPC server
|
||||
func (n *MultiNetworkNode) StartRPCServer(port int) {
|
||||
http.HandleFunc("/ext/crossnet/status", n.handleCrossNetStatus)
|
||||
http.HandleFunc("/ext/crossnet/validators", n.handleCrossNetValidators)
|
||||
http.HandleFunc("/ext/network/", n.handleNetworkSpecific)
|
||||
http.HandleFunc("/v1/crossnet/status", n.handleCrossNetStatus)
|
||||
http.HandleFunc("/v1/crossnet/validators", n.handleCrossNetValidators)
|
||||
http.HandleFunc("/v1/network/", n.handleNetworkSpecific)
|
||||
|
||||
fmt.Printf("🌐 Multi-Network RPC Server starting on port %d\n", port)
|
||||
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil))
|
||||
@@ -172,7 +172,7 @@ func (n *MultiNetworkNode) handleCrossNetValidators(w http.ResponseWriter, r *ht
|
||||
|
||||
// handleNetworkSpecific routes to network-specific handlers
|
||||
func (n *MultiNetworkNode) handleNetworkSpecific(w http.ResponseWriter, r *http.Request) {
|
||||
// Parse network ID from path: /ext/network/{networkID}/...
|
||||
// Parse network ID from path: /v1/network/{networkID}/...
|
||||
// This would route to the appropriate network's chain manager
|
||||
|
||||
response := fmt.Sprintf(`{
|
||||
@@ -251,9 +251,9 @@ func main() {
|
||||
}
|
||||
|
||||
fmt.Println("\n🌐 Starting Multi-Network RPC Server...")
|
||||
fmt.Println(" • Cross-network status: http://localhost:9650/ext/crossnet/status")
|
||||
fmt.Println(" • Cross-network validators: http://localhost:9650/ext/crossnet/validators")
|
||||
fmt.Println(" • Network-specific: http://localhost:9650/ext/network/{networkID}/...")
|
||||
fmt.Println(" • Cross-network status: http://localhost:9650/v1/crossnet/status")
|
||||
fmt.Println(" • Cross-network validators: http://localhost:9650/v1/crossnet/validators")
|
||||
fmt.Println(" • Network-specific: http://localhost:9650/v1/network/{networkID}/...")
|
||||
|
||||
// This would be replaced with actual RPC server
|
||||
node.StartRPCServer(9650)
|
||||
|
||||
Binary file not shown.
+23
-13
@@ -54,7 +54,8 @@ var (
|
||||
QChainAliases = AliasesFor("Q")
|
||||
AChainAliases = AliasesFor("A")
|
||||
BChainAliases = AliasesFor("B")
|
||||
TChainAliases = AliasesFor("T")
|
||||
MChainAliases = AliasesFor("M")
|
||||
FChainAliases = AliasesFor("F")
|
||||
ZChainAliases = AliasesFor("Z")
|
||||
GChainAliases = AliasesFor("G")
|
||||
KChainAliases = AliasesFor("K")
|
||||
@@ -604,7 +605,8 @@ func FromConfig(config *genesiscfg.Config) ([]byte, ids.ID, error) {
|
||||
{[]byte(config.CChainGenesis), constants.EVMID, "C-Chain", nil},
|
||||
{[]byte(config.DChainGenesis), constants.DexVMID, "D-Chain", nil},
|
||||
{[]byte(config.BChainGenesis), constants.BridgeVMID, "B-Chain", nil},
|
||||
{[]byte(config.TChainGenesis), constants.ThresholdVMID, "T-Chain", nil},
|
||||
{[]byte(config.MChainGenesis), constants.MPCVMID, "M-Chain", nil},
|
||||
{[]byte(config.FChainGenesis), constants.FHEVMID, "F-Chain", nil},
|
||||
{[]byte(config.QChainGenesis), constants.QuantumVMID, "Q-Chain", nil},
|
||||
{[]byte(config.ZChainGenesis), constants.ZKVMID, "Z-Chain", nil},
|
||||
// A/G/K carry genesis blobs and are deterministic genesis chains
|
||||
@@ -726,10 +728,10 @@ func UTXOAssetIDFromGenesisBytes(genesisBytes []byte) (ids.ID, bool, error) {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if uChain.VMID != constants.XVMID {
|
||||
if uChain.VMID() != constants.XVMID {
|
||||
continue
|
||||
}
|
||||
id, err := xvmgenesis.AssetIDFromBytes(uChain.GenesisData)
|
||||
id, err := xvmgenesis.AssetIDFromBytes(uChain.GenesisData())
|
||||
if err != nil {
|
||||
return ids.Empty, false, fmt.Errorf("derive X-Chain asset ID from genesis data: %w", err)
|
||||
}
|
||||
@@ -747,7 +749,7 @@ func VMGenesis(genesisBytes []byte, vmID ids.ID) (*pchaintxs.Tx, error) {
|
||||
}
|
||||
for _, chain := range gen.Chains {
|
||||
uChain := chain.Unsigned.(*pchaintxs.CreateChainTx)
|
||||
if uChain.VMID == vmID {
|
||||
if uChain.VMID() == vmID {
|
||||
return chain, nil
|
||||
}
|
||||
}
|
||||
@@ -776,7 +778,7 @@ func Aliases(genesisBytes []byte) (map[string][]string, map[ids.ID][]string, err
|
||||
uChain := chain.Unsigned.(*pchaintxs.CreateChainTx)
|
||||
chainID := chain.ID()
|
||||
endpoint := path.Join(constants.ChainAliasPrefix, chainID.String())
|
||||
switch uChain.VMID {
|
||||
switch uChain.VMID() {
|
||||
case constants.XVMID:
|
||||
apiAliases[endpoint] = []string{
|
||||
"X",
|
||||
@@ -832,16 +834,24 @@ func Aliases(genesisBytes []byte) (map[string][]string, map[ids.ID][]string, err
|
||||
path.Join(constants.ChainAliasPrefix, "bridge"),
|
||||
}
|
||||
chainAliases[chainID] = BChainAliases
|
||||
case constants.ThresholdVMID:
|
||||
case constants.MPCVMID:
|
||||
apiAliases[endpoint] = []string{
|
||||
"T",
|
||||
"threshold",
|
||||
"thresholdvm",
|
||||
"M",
|
||||
"mpc",
|
||||
path.Join(constants.ChainAliasPrefix, "T"),
|
||||
path.Join(constants.ChainAliasPrefix, "threshold"),
|
||||
"mpcvm",
|
||||
path.Join(constants.ChainAliasPrefix, "M"),
|
||||
path.Join(constants.ChainAliasPrefix, "mpc"),
|
||||
}
|
||||
chainAliases[chainID] = TChainAliases
|
||||
chainAliases[chainID] = MChainAliases
|
||||
case constants.FHEVMID:
|
||||
apiAliases[endpoint] = []string{
|
||||
"F",
|
||||
"fhe",
|
||||
"fhevm",
|
||||
path.Join(constants.ChainAliasPrefix, "F"),
|
||||
path.Join(constants.ChainAliasPrefix, "fhe"),
|
||||
}
|
||||
chainAliases[chainID] = FChainAliases
|
||||
case constants.ZKVMID:
|
||||
apiAliases[endpoint] = []string{
|
||||
"Z",
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/luxfi/node/vms/platformvm/genesis"
|
||||
pchaintxs "github.com/luxfi/node/vms/platformvm/txs"
|
||||
)
|
||||
|
||||
// TestUTXOAssetIDFromGenesisBytes_Sovereign asserts the canonical
|
||||
@@ -63,7 +62,7 @@ func TestUTXOAssetIDFromGenesisBytes_POnly(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
pOnly := &genesis.Genesis{Chains: nil}
|
||||
pOnlyBytes, err := genesis.Codec.Marshal(pchaintxs.CodecVersion, pOnly)
|
||||
pOnlyBytes, err := pOnly.Bytes()
|
||||
require.NoError(err)
|
||||
|
||||
id, ok, err := UTXOAssetIDFromGenesisBytes(pOnlyBytes)
|
||||
@@ -93,7 +92,7 @@ func TestVMGenesisOptInChains(t *testing.T) {
|
||||
pOnly := &genesis.Genesis{
|
||||
Chains: nil,
|
||||
}
|
||||
pOnlyBytes, err := genesis.Codec.Marshal(pchaintxs.CodecVersion, pOnly)
|
||||
pOnlyBytes, err := pOnly.Bytes()
|
||||
require.NoError(err)
|
||||
|
||||
for name, vmID := range map[string]ids.ID{
|
||||
|
||||
@@ -314,15 +314,15 @@ func TestFromConfigExplicitStakers(t *testing.T) {
|
||||
for i, vdrTx := range parsed.Validators {
|
||||
switch ut := vdrTx.Unsigned.(type) {
|
||||
case *txs.AddValidatorTx:
|
||||
require.Equal(stakers[i].Weight, ut.Wght,
|
||||
require.Equal(stakers[i].Weight, ut.Weight(),
|
||||
"validator %d weight mismatch", i)
|
||||
t.Logf("Validator %d: NodeID=%s Weight=%d StakeOuts=%d",
|
||||
i, ut.Validator.NodeID, ut.Wght, len(ut.StakeOuts))
|
||||
i, ut.Validator().NodeID, ut.Weight(), len(ut.StakeOuts()))
|
||||
case *txs.AddPermissionlessValidatorTx:
|
||||
require.Equal(stakers[i].Weight, ut.Wght,
|
||||
require.Equal(stakers[i].Weight, ut.Weight(),
|
||||
"validator %d weight mismatch", i)
|
||||
t.Logf("Validator %d: NodeID=%s Weight=%d StakeOuts=%d",
|
||||
i, ut.Validator.NodeID, ut.Wght, len(ut.StakeOuts))
|
||||
i, ut.Validator().NodeID, ut.Weight(), len(ut.StakeOuts()))
|
||||
default:
|
||||
t.Fatalf("unexpected validator tx type: %T", ut)
|
||||
}
|
||||
@@ -396,15 +396,15 @@ func TestFromConfigExplicitStakersNoStakedFunds(t *testing.T) {
|
||||
for i, vdrTx := range parsed.Validators {
|
||||
switch ut := vdrTx.Unsigned.(type) {
|
||||
case *txs.AddValidatorTx:
|
||||
require.Greater(ut.Wght, uint64(0), "validator %d weight must be non-zero", i)
|
||||
require.Greater(len(ut.StakeOuts), 0, "validator %d must have stake outputs", i)
|
||||
require.Greater(ut.Weight(), uint64(0), "validator %d weight must be non-zero", i)
|
||||
require.Greater(len(ut.StakeOuts()), 0, "validator %d must have stake outputs", i)
|
||||
t.Logf("Validator %d: NodeID=%s Weight=%d StakeOuts=%d",
|
||||
i, ut.Validator.NodeID, ut.Wght, len(ut.StakeOuts))
|
||||
i, ut.Validator().NodeID, ut.Weight(), len(ut.StakeOuts()))
|
||||
case *txs.AddPermissionlessValidatorTx:
|
||||
require.Greater(ut.Wght, uint64(0), "validator %d weight must be non-zero", i)
|
||||
require.Greater(len(ut.StakeOuts), 0, "validator %d must have stake outputs", i)
|
||||
require.Greater(ut.Weight(), uint64(0), "validator %d weight must be non-zero", i)
|
||||
require.Greater(len(ut.StakeOuts()), 0, "validator %d must have stake outputs", i)
|
||||
t.Logf("Validator %d: NodeID=%s Weight=%d StakeOuts=%d",
|
||||
i, ut.Validator.NodeID, ut.Wght, len(ut.StakeOuts))
|
||||
i, ut.Validator().NodeID, ut.Weight(), len(ut.StakeOuts()))
|
||||
default:
|
||||
t.Fatalf("unexpected validator tx type: %T", ut)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/luxfi/constants"
|
||||
"github.com/luxfi/node/vms/platformvm/genesis"
|
||||
pchaintxs "github.com/luxfi/node/vms/platformvm/txs"
|
||||
)
|
||||
|
||||
// parsePolicy reads the operator form "3-of-5" into (K, N).
|
||||
//
|
||||
// The canonical parser is luxfi/threshold/pkg/quorum, but the node's genesis
|
||||
// builder is a boot-critical path and does not take a dependency on a
|
||||
// threshold-crypto library to read three integers. What this test pins is the
|
||||
// FORM — that the genesis blob states a quorum in a spelling that cannot be
|
||||
// confused with a polynomial degree — and the form is checkable here.
|
||||
func parsePolicy(t *testing.T, s string) (k, n int) {
|
||||
t.Helper()
|
||||
_, err := fmt.Sscanf(s, "%d-of-%d", &k, &n)
|
||||
require.NoErrorf(t, err, "policy %q is not in the operator form \"3-of-5\"", s)
|
||||
require.Greaterf(t, k, 1, "policy %q: k=1 is not a threshold policy", s)
|
||||
require.LessOrEqualf(t, k, n, "policy %q can never be satisfied", s)
|
||||
return k, n
|
||||
}
|
||||
|
||||
// canonicalMPCVMID is M-Chain's vmID in the encoding the node's plugin registry
|
||||
// resolves against. Pinned literally: the plugin binary's FILENAME must equal
|
||||
// it, and a change that leaves the two out of step produces a chain that is
|
||||
// declared in genesis but that no node can start.
|
||||
//
|
||||
// The predecessor value, tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t, is
|
||||
// the retired `thresholdvm` identifier. It was what the Dockerfile installed
|
||||
// the plugin under while genesis declared MPCVMID, so the two never met.
|
||||
const canonicalMPCVMID = "qCURact1n41FcoNBch8iMVBwc9AWie48D118ZNJ5tBdWrvryS"
|
||||
|
||||
// M-Chain must be a GENESIS chain — in the P-Chain's chain set at height 0,
|
||||
// tracked by every validator from boot — not a chain created later by a
|
||||
// CreateChainTx someone has to remember to submit.
|
||||
//
|
||||
// The difference matters for custody specifically. A post-genesis chain exists
|
||||
// only from the height its tx landed, so a node syncing from genesis has a
|
||||
// window with no custody state, and the chain's very existence depends on an
|
||||
// operator action that can be forgotten or fumbled. Bridged funds should not
|
||||
// depend on that.
|
||||
func TestMChainIsAGenesisChain(t *testing.T) {
|
||||
for _, networkID := range []uint32{
|
||||
constants.MainnetID,
|
||||
constants.TestnetID,
|
||||
constants.LocalID,
|
||||
} {
|
||||
cfg := GetConfig(networkID)
|
||||
require.NotNilf(t, cfg, "network %d has no genesis config", networkID)
|
||||
require.NotEmptyf(t, cfg.MChainGenesis,
|
||||
"network %d carries no mchain.json, so the builder skips M-Chain entirely", networkID)
|
||||
|
||||
raw, _, err := FromConfig(cfg)
|
||||
require.NoErrorf(t, err, "network %d genesis build", networkID)
|
||||
|
||||
parsed, err := genesis.Parse(raw)
|
||||
require.NoErrorf(t, err, "network %d genesis parse", networkID)
|
||||
|
||||
// Genesis chains are CreateChainTx entries executed at height 0 — the
|
||||
// chain exists from the first block, without anyone submitting a tx.
|
||||
var found *pchaintxs.CreateChainTx
|
||||
for _, c := range parsed.Chains {
|
||||
u, ok := c.Unsigned.(*pchaintxs.CreateChainTx)
|
||||
require.True(t, ok, "a genesis chain entry must be a CreateChainTx")
|
||||
if u.VMID() == constants.MPCVMID {
|
||||
found = u
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNilf(t, found,
|
||||
"network %d: no genesis chain with vmID %s (M-Chain)", networkID, constants.MPCVMID)
|
||||
require.Equal(t, "M-Chain", found.BlockchainName())
|
||||
require.Equalf(t, []byte(cfg.MChainGenesis), found.GenesisData(),
|
||||
"network %d: the VM must receive exactly the mchain.json bytes", networkID)
|
||||
require.Equalf(t, canonicalMPCVMID, found.VMID().String(),
|
||||
"network %d: genesis declares a vmID the plugin registry will look up by filename", networkID)
|
||||
}
|
||||
}
|
||||
|
||||
// The genesis blob decides how many custodians must cooperate to move bridged
|
||||
// funds, so it must state that in a form nothing can misread as a polynomial
|
||||
// degree. A bare `"threshold": 3` reads as the signer count to an operator and
|
||||
// as the degree to every threshold library, and those differ by one.
|
||||
func TestMChainGenesisPolicyIsUnambiguousAndDeployable(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
networkID uint32
|
||||
want string
|
||||
}{
|
||||
{constants.MainnetID, "3-of-5"},
|
||||
{constants.TestnetID, "3-of-5"},
|
||||
{constants.LocalID, "2-of-3"},
|
||||
} {
|
||||
cfg := GetConfig(tc.networkID)
|
||||
require.NotNil(t, cfg)
|
||||
|
||||
var blob struct {
|
||||
Policy string `json:"policy"`
|
||||
VM string `json:"vm"`
|
||||
}
|
||||
require.NoErrorf(t, json.Unmarshal([]byte(cfg.MChainGenesis), &blob),
|
||||
"network %d mchain.json must decode, policy included", tc.networkID)
|
||||
|
||||
require.Equalf(t, tc.want, blob.Policy, "network %d policy", tc.networkID)
|
||||
k, n := parsePolicy(t, blob.Policy)
|
||||
require.Greaterf(t, 2*k, n,
|
||||
"network %d: %s admits two disjoint quorums, so two halves of the committee could authorise contradictory releases",
|
||||
tc.networkID, blob.Policy)
|
||||
require.Equalf(t, "mpcvm", blob.VM,
|
||||
"network %d must name the current VM, not the retired ThresholdVM", tc.networkID)
|
||||
}
|
||||
}
|
||||
|
||||
// The ambiguous fields must be gone, not merely ignored. Leaving them in place
|
||||
// means the next reader has two numbers to choose between, and the whole point
|
||||
// of the policy field is that there is exactly one.
|
||||
func TestMChainGenesisHasNoAmbiguousThresholdFields(t *testing.T) {
|
||||
for _, networkID := range []uint32{constants.MainnetID, constants.TestnetID, constants.LocalID} {
|
||||
cfg := GetConfig(networkID)
|
||||
require.NotNil(t, cfg)
|
||||
|
||||
var blob map[string]any
|
||||
require.NoError(t, json.Unmarshal([]byte(cfg.MChainGenesis), &blob))
|
||||
for _, dead := range []string{"mpcThreshold", "mpcParties", "threshold", "totalParties"} {
|
||||
require.NotContainsf(t, blob, dead,
|
||||
"network %d mchain.json still carries %q; the policy field is the only quorum statement",
|
||||
networkID, dead)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A policy is only real if the network has enough validators to hold its
|
||||
// shares. M-Chain draws its committee from the validator set, and RunKeygen
|
||||
// refuses a policy needing more parties than the committee has — so a genesis
|
||||
// declaring 7-of-10 on a five-validator network fails closed forever: no
|
||||
// custody key can ever be generated, and the failure only shows up the first
|
||||
// time someone tries to bridge.
|
||||
//
|
||||
// mainnet's mchain.json shipped exactly that mismatch for as long as the field
|
||||
// existed, harmlessly, because nothing read it. This test is what keeps it
|
||||
// harmless now that the policy is authoritative.
|
||||
func TestMChainPolicyIsSatisfiableByTheGenesisValidatorSet(t *testing.T) {
|
||||
for _, networkID := range []uint32{constants.MainnetID, constants.TestnetID, constants.LocalID} {
|
||||
cfg := GetConfig(networkID)
|
||||
require.NotNil(t, cfg)
|
||||
|
||||
raw, _, err := FromConfig(cfg)
|
||||
require.NoError(t, err)
|
||||
parsed, err := genesis.Parse(raw)
|
||||
require.NoError(t, err)
|
||||
|
||||
var blob struct {
|
||||
Policy string `json:"policy"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal([]byte(cfg.MChainGenesis), &blob))
|
||||
_, n := parsePolicy(t, blob.Policy)
|
||||
|
||||
require.LessOrEqualf(t, n, len(parsed.Validators),
|
||||
"network %d: M-Chain policy %s needs %d parties but genesis declares %d validators; "+
|
||||
"keygen would fail closed and no custody key could ever exist",
|
||||
networkID, blob.Policy, n, len(parsed.Validators))
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,8 @@ var Registry = []ChainSpec{
|
||||
{Letter: "Q", VMID: constants.QuantumVMID, Aliases: []string{"quantum", "quantumvm", "pq"}, Name: "Q-Chain"},
|
||||
{Letter: "A", VMID: constants.AIVMID, Aliases: []string{"attest", "ai", "aivm"}, Name: "A-Chain"},
|
||||
{Letter: "B", VMID: constants.BridgeVMID, Aliases: []string{"bridge", "bridgevm"}, Name: "B-Chain"},
|
||||
{Letter: "T", VMID: constants.ThresholdVMID, Aliases: []string{"threshold", "thresholdvm", "mpc"}, Name: "T-Chain"},
|
||||
{Letter: "M", VMID: constants.MPCVMID, Aliases: []string{"mpc", "mpcvm"}, Name: "M-Chain"},
|
||||
{Letter: "F", VMID: constants.FHEVMID, Aliases: []string{"fhe", "fhevm"}, Name: "F-Chain"},
|
||||
{Letter: "Z", VMID: constants.ZKVMID, Aliases: []string{"zk", "zkvm"}, Name: "Z-Chain"},
|
||||
{Letter: "G", VMID: constants.GraphVMID, Aliases: []string{"graph", "graphvm", "dgraph"}, Name: "G-Chain"},
|
||||
{Letter: "K", VMID: constants.KeyVMID, Aliases: []string{"key", "keyvm"}, Name: "K-Chain"},
|
||||
|
||||
@@ -41,7 +41,8 @@ func TestChainAliasesRegistryParity(t *testing.T) {
|
||||
{"Q", QChainAliases, []string{"Q", "quantum", "quantumvm", "pq"}},
|
||||
{"A", AChainAliases, []string{"A", "attest", "ai", "aivm"}},
|
||||
{"B", BChainAliases, []string{"B", "bridge", "bridgevm"}},
|
||||
{"T", TChainAliases, []string{"T", "threshold", "thresholdvm", "mpc"}},
|
||||
{"M", MChainAliases, []string{"M", "mpc", "mpcvm"}},
|
||||
{"F", FChainAliases, []string{"F", "fhe", "fhevm"}},
|
||||
{"Z", ZChainAliases, []string{"Z", "zk", "zkvm"}},
|
||||
{"G", GChainAliases, []string{"G", "graph", "graphvm", "dgraph"}},
|
||||
{"K", KChainAliases, []string{"K", "key", "keyvm"}},
|
||||
@@ -72,7 +73,8 @@ func TestVMAliasesRegistryParity(t *testing.T) {
|
||||
constants.QuantumVMID: {"quantumvm", "quantum", "pq"},
|
||||
constants.AIVMID: {"aivm", "attest", "ai"},
|
||||
constants.BridgeVMID: {"bridgevm", "bridge"},
|
||||
constants.ThresholdVMID: {"thresholdvm", "threshold", "mpc"},
|
||||
constants.MPCVMID: {"mpc", "mpcvm"},
|
||||
constants.FHEVMID: {"fhe", "fhevm"},
|
||||
constants.ZKVMID: {"zkvm", "zk"},
|
||||
constants.GraphVMID: {"graphvm", "graph", "dgraph"},
|
||||
constants.KeyVMID: {"keyvm", "key"},
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// zzz_gateprobe_test.go is a BLUE-team gate instrument for the
|
||||
// mpcvm-decomplect surgery (LP-134 / LP-7050). NOT a behavioral
|
||||
// assertion — it emits a deterministic per-chain digest of the fully
|
||||
// built P-Chain genesis for every network so before/after can be diffed
|
||||
// to PROVE which chains changed and which stayed byte-identical.
|
||||
// zzz_ prefix keeps it last. Delete after RED sign-off.
|
||||
package builder
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/node/vms/platformvm/genesis"
|
||||
pchaintxs "github.com/luxfi/node/vms/platformvm/txs"
|
||||
)
|
||||
|
||||
func TestZZZ_GateProbe_PerChainDigest(t *testing.T) {
|
||||
nets := []struct {
|
||||
name string
|
||||
id uint32
|
||||
}{
|
||||
{"mainnet", 1},
|
||||
{"testnet", 2},
|
||||
{"devnet", 3},
|
||||
{"localnet", 1337},
|
||||
}
|
||||
|
||||
for _, n := range nets {
|
||||
cfg := GetConfig(n.id)
|
||||
if cfg == nil {
|
||||
t.Fatalf("%s GetConfig: nil", n.name)
|
||||
}
|
||||
gb, rootID, err := FromConfig(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("%s FromConfig: %v", n.name, err)
|
||||
}
|
||||
gen, err := genesis.Parse(gb)
|
||||
if err != nil {
|
||||
t.Fatalf("%s Parse: %v", n.name, err)
|
||||
}
|
||||
|
||||
type row struct {
|
||||
name string
|
||||
vmid string
|
||||
dataSum string
|
||||
chainID string
|
||||
}
|
||||
rows := make([]row, 0, len(gen.Chains))
|
||||
for _, c := range gen.Chains {
|
||||
u := c.Unsigned.(*pchaintxs.CreateChainTx)
|
||||
sum := sha256.Sum256(u.GenesisData())
|
||||
rows = append(rows, row{
|
||||
name: u.BlockchainName(),
|
||||
vmid: u.VMID().String(),
|
||||
dataSum: hex.EncodeToString(sum[:]),
|
||||
chainID: c.ID().String(),
|
||||
})
|
||||
}
|
||||
sort.Slice(rows, func(i, j int) bool { return rows[i].name < rows[j].name })
|
||||
|
||||
fmt.Printf("=== NET %s (id=%d) P-ROOT=%s ===\n", n.name, n.id, rootID.String())
|
||||
for _, r := range rows {
|
||||
fmt.Printf(" CHAIN name=%-10s vmid=%-52s dataSHA256=%s chainTxID=%s\n",
|
||||
r.name, r.vmid, r.dataSum, r.chainID)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,21 +26,21 @@ require (
|
||||
github.com/huin/goupnp v1.3.0
|
||||
github.com/jackpal/gateway v1.1.1
|
||||
github.com/jackpal/go-nat-pmp v1.0.2
|
||||
github.com/luxfi/consensus v1.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/consensus v1.36.12
|
||||
github.com/luxfi/crypto v1.20.2
|
||||
github.com/luxfi/database v1.21.1
|
||||
github.com/luxfi/ids v1.3.2
|
||||
github.com/luxfi/keychain v1.1.1
|
||||
github.com/luxfi/log v1.4.3
|
||||
github.com/luxfi/math v1.4.1
|
||||
github.com/luxfi/metric v1.5.9
|
||||
github.com/luxfi/math v1.5.1
|
||||
github.com/luxfi/metric v1.8.1
|
||||
github.com/luxfi/mock v0.1.1
|
||||
github.com/mr-tron/base58 v1.3.0
|
||||
github.com/onsi/ginkgo/v2 v2.28.1
|
||||
github.com/pires/go-proxyproto v0.11.0
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/common v0.68.0 // indirect
|
||||
github.com/rs/cors v1.11.1
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
@@ -56,13 +56,13 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.44.0 // indirect
|
||||
go.uber.org/goleak v1.3.0
|
||||
go.uber.org/mock v0.6.0
|
||||
golang.org/x/crypto v0.52.0
|
||||
golang.org/x/crypto v0.54.0
|
||||
golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9 // indirect
|
||||
golang.org/x/mod v0.36.0
|
||||
golang.org/x/net v0.55.0
|
||||
golang.org/x/sync v0.20.0
|
||||
golang.org/x/mod v0.37.0
|
||||
golang.org/x/net v0.57.0
|
||||
golang.org/x/sync v0.22.0
|
||||
golang.org/x/time v0.15.0
|
||||
golang.org/x/tools v0.45.0
|
||||
golang.org/x/tools v0.47.0
|
||||
gonum.org/v1/gonum v0.17.0
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
@@ -73,7 +73,7 @@ require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0
|
||||
github.com/cockroachdb/errors v1.12.0 // indirect
|
||||
github.com/cockroachdb/errors v1.13.0 // indirect
|
||||
github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 // indirect
|
||||
github.com/cockroachdb/redact v1.1.8 // indirect
|
||||
github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb // indirect
|
||||
@@ -81,7 +81,7 @@ require (
|
||||
github.com/deckarep/golang-set/v2 v2.9.0 // indirect
|
||||
github.com/fatih/structtag v1.2.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/getsentry/sentry-go v0.44.1 // indirect
|
||||
github.com/getsentry/sentry-go v0.46.2 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
@@ -90,7 +90,7 @@ require (
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
|
||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/klauspost/compress v1.18.6
|
||||
github.com/klauspost/compress v1.19.1
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
@@ -98,7 +98,7 @@ require (
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/procfs v0.20.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.15.0 // indirect
|
||||
github.com/sanity-io/litter v1.5.5 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
@@ -107,8 +107,8 @@ require (
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/text v0.40.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -118,68 +118,69 @@ require (
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||
github.com/golang/mock v1.7.0-rc.1
|
||||
github.com/luxfi/accel v1.2.4
|
||||
github.com/luxfi/api v1.0.15
|
||||
github.com/luxfi/api v1.1.1
|
||||
github.com/luxfi/atomic v1.0.0
|
||||
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/chains v1.7.9
|
||||
github.com/luxfi/codec v1.2.1
|
||||
github.com/luxfi/compress v0.1.1
|
||||
github.com/luxfi/constants v1.6.2
|
||||
github.com/luxfi/container v0.2.1
|
||||
github.com/luxfi/filesystem v0.0.1
|
||||
github.com/luxfi/genesis v1.13.16
|
||||
github.com/luxfi/genesis v1.16.4
|
||||
github.com/luxfi/genesis/pkg/genesis/security v1.13.8
|
||||
github.com/luxfi/geth v1.17.12
|
||||
github.com/luxfi/go-bip39 v1.1.2
|
||||
github.com/luxfi/keys v1.2.0
|
||||
github.com/luxfi/geth v1.20.1
|
||||
github.com/luxfi/go-bip39 v1.2.0
|
||||
github.com/luxfi/kms v1.12.10
|
||||
github.com/luxfi/math/safe v0.0.1
|
||||
github.com/luxfi/net v0.0.5
|
||||
github.com/luxfi/p2p v1.21.1
|
||||
github.com/luxfi/resource v0.0.1
|
||||
github.com/luxfi/net v0.1.1
|
||||
github.com/luxfi/p2p v1.22.1
|
||||
github.com/luxfi/resource v0.1.1
|
||||
github.com/luxfi/rpc v1.1.0
|
||||
github.com/luxfi/runtime v1.1.3
|
||||
github.com/luxfi/sdk v1.17.9
|
||||
github.com/luxfi/runtime v1.3.1
|
||||
github.com/luxfi/sdk v1.18.1
|
||||
github.com/luxfi/sys v0.1.0
|
||||
github.com/luxfi/timer v1.0.2
|
||||
github.com/luxfi/threshold v1.12.3
|
||||
github.com/luxfi/timer v1.1.1
|
||||
github.com/luxfi/units v1.0.0
|
||||
github.com/luxfi/utils v1.2.0
|
||||
github.com/luxfi/utxo v0.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/luxfi/utils v1.3.1
|
||||
github.com/luxfi/utxo v0.5.8
|
||||
github.com/luxfi/validators v1.3.1
|
||||
github.com/luxfi/vm v1.3.3
|
||||
github.com/luxfi/warp v1.24.1
|
||||
github.com/luxfi/zap v1.2.6
|
||||
github.com/luxfi/zwing v0.6.1
|
||||
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354
|
||||
github.com/zap-proto/http v0.0.0-20260506200741-fd6047874433
|
||||
github.com/valyala/fasthttp v1.73.0
|
||||
github.com/zap-proto/http v0.3.0
|
||||
go.uber.org/zap v1.27.1
|
||||
)
|
||||
|
||||
require (
|
||||
capnproto.org/go/capnp/v3 v3.0.1-alpha.2 // indirect
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
filippo.io/hpke v0.4.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect
|
||||
github.com/aws/smithy-go v1.24.2 // indirect
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.6 // indirect
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.5.0 // indirect
|
||||
github.com/btcsuite/btcd/chainhash/v2 v2.0.0 // indirect
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
|
||||
github.com/colega/zeropool v0.0.0-20230505084239-6fb4a4f75381 // indirect
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
@@ -189,24 +190,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.5.0 // indirect
|
||||
github.com/luxfi/age v1.6.0 // indirect
|
||||
github.com/luxfi/bft v0.1.5 // indirect
|
||||
github.com/luxfi/corona v0.10.3 // indirect
|
||||
github.com/luxfi/corona v0.10.4 // indirect
|
||||
github.com/luxfi/crypto/ipa v1.2.4 // indirect
|
||||
github.com/luxfi/dkg v0.3.5 // indirect
|
||||
github.com/luxfi/kms v1.11.7 // indirect
|
||||
github.com/luxfi/keys v1.4.1 // indirect
|
||||
github.com/luxfi/lattice/v7 v7.1.4 // indirect
|
||||
github.com/luxfi/lens v0.1.4 // indirect
|
||||
github.com/luxfi/lens v0.2.1 // indirect
|
||||
github.com/luxfi/light v1.0.0 // indirect
|
||||
github.com/luxfi/magnetar v1.2.3 // indirect
|
||||
github.com/luxfi/mdns v0.1.1 // indirect
|
||||
github.com/luxfi/mlwe v0.2.1 // indirect
|
||||
github.com/luxfi/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/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/zapdb v1.10.1 // indirect
|
||||
github.com/miekg/dns v1.1.72 // indirect
|
||||
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b // indirect
|
||||
@@ -215,13 +216,16 @@ require (
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/tinylib/msgp v1.6.4 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/zap-proto/go v1.1.0 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.9 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/luxfi/concurrent v0.0.3
|
||||
github.com/luxfi/proto v1.3.5
|
||||
github.com/luxfi/upgrade v1.0.1 // indirect
|
||||
github.com/luxfi/concurrent v0.1.1
|
||||
github.com/luxfi/proto v1.4.2
|
||||
github.com/luxfi/upgrade v1.0.3 // indirect
|
||||
github.com/luxfi/version v1.0.1
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
)
|
||||
@@ -242,13 +246,13 @@ require (
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/luxfi/address v1.0.1
|
||||
github.com/luxfi/cache v1.2.1 // indirect
|
||||
github.com/luxfi/formatting v1.0.1
|
||||
github.com/luxfi/go-bip32 v1.0.2
|
||||
github.com/luxfi/address v1.1.1
|
||||
github.com/luxfi/cache v1.3.1 // indirect
|
||||
github.com/luxfi/formatting v1.1.1
|
||||
github.com/luxfi/go-bip32 v1.1.0
|
||||
github.com/luxfi/math/big v0.1.0 // indirect
|
||||
github.com/luxfi/sampler v1.1.0 // indirect
|
||||
github.com/luxfi/tls v1.0.3 // indirect
|
||||
github.com/luxfi/tls v1.1.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.15 // indirect
|
||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||
github.com/montanaflynn/stats v0.9.0 // indirect
|
||||
@@ -257,15 +261,7 @@ require (
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
)
|
||||
|
||||
exclude github.com/ethereum/go-ethereum v1.10.26
|
||||
|
||||
// TEMPORARY — local-dev build aid for the bootstrap frozen-cache convergence fix.
|
||||
// FINAL CASCADE (publish step, NOT done here): tag consensus v1.25.36 (the uncommitted
|
||||
// engine/chain/integration.go FinalizedLedger + FinalizedBlockAtHeight accessors and the
|
||||
// engine/chain/bootstrap Has→Accepted 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.
|
||||
|
||||
@@ -1,28 +1,50 @@
|
||||
bazil.org/fuse v0.0.0-20230120002735-62a210ff1fd5/go.mod h1:gG3RZAMXCa/OTes6rr9EwusmR1OH1tDDy+cg9c5YliY=
|
||||
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd h1:ZLsPO6WdZ5zatV4UfVpr7oAwLGRZ+sebTUruuM4Ra3M=
|
||||
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo=
|
||||
capnproto.org/go/capnp/v3 v3.0.1-alpha.2 h1:W/cf+XEArUSwcBBE/9wS2NpWDkM5NLQOjmzEiHZpYi0=
|
||||
capnproto.org/go/capnp/v3 v3.0.1-alpha.2/go.mod h1:2vT5D2dtG8sJGEoEKU17e+j7shdaYp1Myl8X03B3hmc=
|
||||
codeberg.org/go-fonts/liberation v0.5.0/go.mod h1:zS/2e1354/mJ4pGzIIaEtm/59VFCFnYC7YV6YdGl5GU=
|
||||
codeberg.org/go-latex/latex v0.1.0/go.mod h1:LA0q/AyWIYrqVd+A9Upkgsb+IqPcmSTKc9Dny04MHMw=
|
||||
codeberg.org/go-pdf/fpdf v0.10.0/go.mod h1:Y0DGRAdZ0OmnZPvjbMp/1bYxmIPxm0ws4tfoPOc4LjU=
|
||||
connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
|
||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A=
|
||||
filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY=
|
||||
filippo.io/nistec v0.0.4/go.mod h1:PK/lw8I1gQT4hUML4QGaqljwdDaFcMyFKSXN7kjrtKI=
|
||||
git.sr.ht/~sbinet/gg v0.6.0/go.mod h1:uucygbfC9wVPQIfrmwM2et0imr8L7KQWywX0xpFMm94=
|
||||
github.com/ALTree/bigfloat v0.2.0 h1:AwNzawrpFuw55/YDVlcPw0F0cmmXrmngBHhVrvdXPvM=
|
||||
github.com/ALTree/bigfloat v0.2.0/go.mod h1:+NaH2gLeY6RPBPPQf4aRotPPStg+eXc8f9ZaE4vRfD4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4/go.mod h1:8mwH4klAm9DUgR2EEHyEEAQlRDvLPyg5fQry3y+cDew=
|
||||
github.com/ChainSafe/go-schnorrkel v1.1.0 h1:rZ6EU+CZFCjB4sHUE1jIu8VDoB/wRKZxoe1tkcO71Wk=
|
||||
github.com/ChainSafe/go-schnorrkel v1.1.0/go.mod h1:ABkENxiP+cvjFiByMIZ9LYbRoNNLeBLiakC1XeTFxfE=
|
||||
github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE=
|
||||
github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
|
||||
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260311194731-d5b7577c683d/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
|
||||
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||
github.com/StephenButtolph/canoto v0.17.3 h1:lvsnYD4b96vD1knnmp1xCmZqfYpY/jSeRozGdOfdvGI=
|
||||
github.com/StephenButtolph/canoto v0.17.3/go.mod h1:IcnAHC6nJUfQFVR9y60ko2ecUqqHHSB6UwI9NnBFZnE=
|
||||
github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794/go.mod h1:7e+I0LQFUI9AXWxOfsQROs9xPhoJtbsyWcjJqDd4KPY=
|
||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
|
||||
github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
|
||||
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=
|
||||
github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM=
|
||||
github.com/andybalholm/brotli v1.2.2/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.13 h1:5KgbxMaS2coSWRrx9TX/QtWbqzgQkOdEa3sZPhBhCSg=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.13/go.mod h1:8zz7wedqtCbw5e9Mi2doEwDyEgHcEE9YOJp6a8jdSMY=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.13 h1:mA59E3fokBvyEGHKFdnpNNrvaR351cqiHgRg+JzOSRI=
|
||||
@@ -37,16 +59,25 @@ github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 h1:eZioDaZGJ0tMM4gzmkNIO2aAoQd+je7Ug7TkvAzlmkU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18/go.mod h1:CCXwUKAJdoWr6/NcxZ+zsiPr6oH/Q5aTooRGYieAyj4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 h1:fJvQ5mIBVfKtiyx0AHY6HeWcRX5LGANLpq8SVR+Uazs=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10/go.mod h1:Kzm5e6OmNH8VMkgK9t+ry5jEih4Y8whqs+1hrkxim1I=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 h1:/A/xDuZAVD2BpsS2fftFRo/NoEKQJ8YTnJDEHBy2Gtg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18/go.mod h1:hWe9b4f+djUQGmyiGEeOnZv69dtMSgpDRIvNMvuvzvY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.5/go.mod h1:TmxGowuBYwjmHFOsEDxaZdsQE62JJzOmtiWafTi/czg=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 h1:M1A9AjcFwlxTLuf0Faj88L8Iqw0n/AJHjpZTQzMMsSc=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2/go.mod h1:KsdTV6Q9WKUZm2mNJnUFmIoXfZux91M3sr/a4REX8e0=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 h1:HwxWTbTrIHm5qY+CAEur0s/figc3qwvLWsNkF4RPToo=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 h1:QKZH0S178gCmFEgst8hN0mCX1KxLgHBKKY/CLqwP8lg=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9/go.mod h1:7yuQJoT+OoH8aqIxw9vwF+8KpvLZ8AWmvmUWHsGQZvI=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.14 h1:GcLE9ba5ehAQma6wlopUesYg/hbcOhFNWTjELkiWkh4=
|
||||
@@ -67,8 +98,8 @@ github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6
|
||||
github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.6 h1:IzlsEr9olcSRKB/n7c4351F3xHKxS2lma+1UFGCYd4E=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.6/go.mod h1:m22FrOAiuxl/tht9wIqAoGHcbnCCaPWyauO8y2LGGtQ=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.5.0 h1:KioMXOWa76b86sTZZOmbzv/ldaQCmB8KFAyn5PbB8E8=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.5.0/go.mod h1:+K/MYXcLBtHEQjRbjHuJChuybk4LCgjdjgRwil+e+Kk=
|
||||
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00=
|
||||
@@ -76,10 +107,12 @@ 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/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts=
|
||||
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
|
||||
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
|
||||
github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
|
||||
@@ -87,19 +120,28 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku
|
||||
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
||||
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
||||
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8=
|
||||
github.com/cavaliergopher/grab/v3 v3.0.1/go.mod h1:1U/KNnD+Ft6JJiYoYBAimKH2XrYptb8Kl3DFGmsjpq4=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chelnak/ysmrr v0.6.0/go.mod h1:56JSrmQgb7/7xoMvuD87h3PE/qW6K1+BQcrgWtVLTUo=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
github.com/cloudflare/cloudflare-go v0.116.0/go.mod h1:Ds6urDwn/TF2uIU24mu7H91xkKP8gSAHxQ44DSZgVmU=
|
||||
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4=
|
||||
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU=
|
||||
github.com/cockroachdb/errors v1.12.0 h1:d7oCs6vuIMUQRVbi6jWWWEJZahLCfJpnJSVobd1/sUo=
|
||||
github.com/cockroachdb/errors v1.12.0/go.mod h1:SvzfYNNBshAVbZ8wzNc/UPK3w1vf0dKDUP41ucAIf7g=
|
||||
github.com/cockroachdb/errors v1.13.0 h1:BoCcJeiP9hpBJDETkX19qi8Tb8So37srSsp3stTaDMQ=
|
||||
github.com/cockroachdb/errors v1.13.0/go.mod h1:bjxt/4E5+OyuAnacpTIU9rn2mzPu1VlthvHP+xpROq0=
|
||||
github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0 h1:pU88SPhIFid6/k0egdR5V6eALQYq2qbSmukrkgIh/0A=
|
||||
github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M=
|
||||
github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 h1:ASDL+UJcILMqgNeV5jiqR4j+sTuvQNHdf2chuKj1M5k=
|
||||
@@ -110,8 +152,9 @@ github.com/cockroachdb/redact v1.1.8 h1:8eVLLj6juKxiKrAEw2b8cJvNqWq++U8WOfQFuL7K
|
||||
github.com/cockroachdb/redact v1.1.8/go.mod h1:GceHHpJ0rMDpYARL5In88Alq/xMBUtVlz7Qxix6ZVkw=
|
||||
github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb h1:3bCgBvB8PbJVMX1ouCcSIxvsqKPYM7gs72o0zC76n9g=
|
||||
github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
|
||||
github.com/colega/zeropool v0.0.0-20230505084239-6fb4a4f75381 h1:d5EKgQfRQvO97jnISfR89AiCCCJMwMFoSxUiU0OGCRU=
|
||||
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||
github.com/colega/zeropool v0.0.0-20230505084239-6fb4a4f75381/go.mod h1:OU76gHeRo8xrzGJU3F3I1CqX1ekM8dfJw0+wPeMwnp0=
|
||||
github.com/consensys/bavard v0.2.2-0.20260118153501-cba9f5475432/go.mod h1:k/zVjHHC4B+PQy1Pg7fgvG3ALicQw540Crag8qx+dZs=
|
||||
github.com/consensys/gnark-crypto v0.20.1 h1:PXDUBvk8AzhvWowHLWBEAfUQcV1/aZgWIqD6eMpXmDg=
|
||||
github.com/consensys/gnark-crypto v0.20.1/go.mod h1:RBWrSgy+IDbGR69RRV313th3M/aZU1ubk2om+qHuTSc=
|
||||
github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY=
|
||||
@@ -121,9 +164,11 @@ github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc=
|
||||
github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI=
|
||||
github.com/crate-crypto/go-kzg-4844 v1.1.0/go.mod h1:JolLjpSff1tCCJKaJx4psrlEdlXuJEC996PL3tTAFks=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/cronokirby/saferith v0.33.0 h1:TgoQlfsD4LIwx71+ChfRcIpjkw+RPOapDEVxa+LhwLo=
|
||||
github.com/cronokirby/saferith v0.33.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -141,22 +186,30 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeC
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
|
||||
github.com/dgraph-io/badger/v4 v4.9.0/go.mod h1:5/MEx97uzdPUHR4KtkNt8asfI2T4JiEiQlV7kWUo8c0=
|
||||
github.com/dgraph-io/ristretto/v2 v2.4.0 h1:I/w09yLjhdcVD2QV192UJcq8dPBaAJb9pOuMyNy0XlU=
|
||||
github.com/dgraph-io/ristretto/v2 v2.4.0/go.mod h1:0KsrXtXvnv0EqnzyowllbVJB8yBonswa2lTCK2gGo9E=
|
||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
|
||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0/go.mod h1:56wL82FO0bfMU5RvfXoIwSOP2ggqqxT+tAfNEIyxuHw=
|
||||
github.com/dop251/goja v0.0.0-20260311135729-065cd970411c/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/emicklei/dot v1.11.0 h1:zsrhCuFHAJge/aZIC4N4LdHy5tqYu4tWEaUzIwdYj4Y=
|
||||
github.com/emicklei/dot v1.11.0/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/ethereum/c-kzg-4844/v2 v2.1.7 h1:aat3CuITdDbPC6pmEGRT0zJ5eOxzrZj8TJT5z7Xk//M=
|
||||
github.com/ethereum/c-kzg-4844/v2 v2.1.7/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw=
|
||||
github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk=
|
||||
github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8=
|
||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
|
||||
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
|
||||
github.com/ferranbt/fastssz v1.0.0 h1:9EXXYsracSqQRBQiHeaVsG/KQeYblPf40hsQPb9Dzk8=
|
||||
github.com/ferranbt/fastssz v1.0.0/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg=
|
||||
github.com/fjl/gencodec v0.1.1/go.mod h1:chDHL3wKXuBgauP8x3XNZkl5EIAR5SoCTmmmDTZRzmw=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
@@ -166,8 +219,11 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=
|
||||
github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/getsentry/sentry-go v0.44.1 h1:/cPtrA5qB7uMRrhgSn9TYtcEF36auGP3Y6+ThvD/yaI=
|
||||
github.com/getsentry/sentry-go v0.44.1/go.mod h1:XDotiNZbgf5U8bPDUAfvcFmOnMQQceESxyKaObSssW0=
|
||||
github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61/go.mod h1:Q0X6pkwTILDlzrGEckF6HKjXe48EgsY/l7K7vhY4MW8=
|
||||
github.com/gballet/go-libpcsclite v0.0.0-20250918194357-1ec6f2e601c6/go.mod h1:3IVE7v4II2gS2V5amIH7F7NeYQtbbORtQtjdflgS1vk=
|
||||
github.com/getsentry/sentry-go v0.46.2 h1:1jhYwrKGa3sIpo/y5iDNXS5wDoT7I1KNzMHrnK6ojns=
|
||||
github.com/getsentry/sentry-go v0.46.2/go.mod h1:evVbw2qotNUdYG8KxXbAdjOQWWvWIwKxpjdZZIvcIPw=
|
||||
github.com/ghemawat/stream v0.0.0-20171120220530-696b145b53b9/go.mod h1:106OIgooyS7OzLDOpUGgm9fA3bQENb/cFSyyBmMoJDs=
|
||||
github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs=
|
||||
github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
|
||||
github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M=
|
||||
@@ -176,8 +232,12 @@ github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01
|
||||
github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
|
||||
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-json-experiment/json v0.0.0-20260601182631-00ed12fed2a6 h1:nxP4pPoyqOAgX8lYDFCfl3DyKeXErCvSvhcyzwGV9CE=
|
||||
github.com/go-json-experiment/json v0.0.0-20260601182631-00ed12fed2a6/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -188,19 +248,30 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccmack/gocc v1.0.2/go.mod h1:LXX2tFVUggS/Zgx/ICPOr3MLyusuM7EcbfkPvNsjdO8=
|
||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||
github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
|
||||
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
@@ -212,6 +283,7 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
@@ -219,12 +291,14 @@ github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs=
|
||||
github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
@@ -243,10 +317,13 @@ github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5T
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
|
||||
github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE=
|
||||
github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs=
|
||||
github.com/graph-gophers/graphql-go v1.9.0/go.mod h1:23olKZ7duEvHlF/2ELEoSZaY1aNPfShjP782SOoNTyM=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||
github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is=
|
||||
github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s=
|
||||
github.com/gtank/ristretto255 v0.2.0 h1:LeOuWr6giplWkkMizx2emfG03SRPJqKt1nfIHLVHQ/0=
|
||||
github.com/gtank/ristretto255 v0.2.0/go.mod h1:OJ1ox/dWcp7sJ5grYDcZ+kkHYuj5nelW5aaL7ESVXBw=
|
||||
github.com/guptarohit/asciigraph v0.5.5/go.mod h1:dYl5wwK4gNsnFf9Zp+l06rFiDZ5YtXM6x7SRWZ3KGag=
|
||||
github.com/hanzoai/vfs v0.4.3 h1:QN9SemEQBq9x1l/toi51/TZWctbt3i3mgUJfz5RPALY=
|
||||
github.com/hanzoai/vfs v0.4.3/go.mod h1:wTHfTpJ/165yz0qfPBNFcYRg+tGw8YDwPu+xgps88zU=
|
||||
github.com/hanzos3/go-sdk v1.0.2 h1:EOJQGVnwclkzIyRJyWqtqmA2muyaSsF4y+7KYC4Vhdw=
|
||||
@@ -255,6 +332,7 @@ github.com/hashicorp/go-bexpr v0.1.16 h1:D+fKoGyUzXVS0FdjOX1ws3vIck8DVtBqQ0tsusm
|
||||
github.com/hashicorp/go-bexpr v0.1.16/go.mod h1:HGKbAByHn2aJWUV47gL7+IjLK79iU3EZIbOwCXJZLoE=
|
||||
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
|
||||
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330=
|
||||
github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg=
|
||||
github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao=
|
||||
@@ -264,28 +342,46 @@ github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXei
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
|
||||
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
|
||||
github.com/hydrogen18/memlistener v1.0.0/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.14.0/go.mod h1:Ahpm3QXKMJslpXl3IftVLVezreAUtBOTZssDrjZEFHI=
|
||||
github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
|
||||
github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
|
||||
github.com/jackpal/gateway v1.1.1 h1:UXXXkJGIHFsStms9ZBgGpoaFEJP7oJtFn5vplIT68E8=
|
||||
github.com/jackpal/gateway v1.1.1/go.mod h1:Tl1vZVtUaXx5j6P5HFmv45alhEi4yHHLfT4PRbB7eyw=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
|
||||
github.com/jacobsa/fuse v0.0.0-20260302145937-f1ba38d60fdf/go.mod h1:fcpw1yk/suvFhB8rT9P+pst+NLboWsBLky9csooKjPc=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7/go.mod h1:BMxO138bOokdgt4UaxZiEfypcSHX0t6SIFimVP1oRfk=
|
||||
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE=
|
||||
github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/juju/fslock v0.0.0-20160525022230-4d5c94c67b4b/go.mod h1:HMcgvsgd0Fjj4XXDkbjdmlbI505rUPBs6WBMYg2pXks=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
|
||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/kilic/bls12-381 v0.1.0/go.mod h1:vDTTHJONJ6G+P2R74EhnyotQDTliQDnFEwhdmfzw1ig=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE=
|
||||
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
|
||||
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
|
||||
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
@@ -296,152 +392,180 @@ github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzW
|
||||
github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
|
||||
github.com/luxfi/accel v1.2.4 h1:5VbIHyEvvfobn2zBiTFODxDw1CeqxCepZOLlvkuf9yQ=
|
||||
github.com/luxfi/accel v1.2.4/go.mod h1:ISIwAX+ZfsL/S5nsP2JvfldXN6Nc+QzoWf6Jtaq+xsQ=
|
||||
github.com/luxfi/address v1.0.1 h1:Sc4keyuVzBIvHr7uVeYZf2/WY9YDGUgDi/iiWenj49g=
|
||||
github.com/luxfi/address v1.0.1/go.mod h1:5j3Eh66v9zvv1GbNdZwt+23krV8JlSDaRzmWZU8ZRM0=
|
||||
github.com/luxfi/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/address v1.1.1 h1:4afWzyBWzTiZN7RenBtdMC9LIvP9L4CSBzSquwKEAgI=
|
||||
github.com/luxfi/address v1.1.1/go.mod h1:KG0jUBcgoJYeieKP5jboCq9UewwDBIOus8ZCqUMVlw8=
|
||||
github.com/luxfi/age v1.6.0 h1:KMD8gSOP4NVCb7NWSlRcgBZNV2xm2a+qQWPyPmiX6f4=
|
||||
github.com/luxfi/age v1.6.0/go.mod h1:7cu9CIyikgyAvr5MlXFapEDQ15yBaHOSdKkK5lG04WE=
|
||||
github.com/luxfi/ai v0.1.0/go.mod h1:lTuY32dGjEJUgVheYuanlt1O0jHj0AZ09sUSpRsmH6M=
|
||||
github.com/luxfi/api v1.1.1 h1:CXD7m0quPmUm+Qw35TrF+E7b0Fq4qz9gHfrZ5gyrjHU=
|
||||
github.com/luxfi/api v1.1.1/go.mod h1:g6J0iohVqaIj2aO1u/ZJPqjiX2tog0NM3/SBf7wJ4cA=
|
||||
github.com/luxfi/atomic v1.0.0 h1:xUV60MuzRvXngaQ1sM0yVC2v4TRoLlUGkkH7M9PS4yw=
|
||||
github.com/luxfi/atomic v1.0.0/go.mod h1:0G2mTlQ6TXWHICUHrUUPu1/qAiIyR4gSZ2tva9ci/bI=
|
||||
github.com/luxfi/bft v0.1.5 h1:5xVLPkog4e5LTgaVlb9pgxA0EWE6tkrKwHPZVRz+RZw=
|
||||
github.com/luxfi/bft v0.1.5/go.mod h1:5I8Ft8yA69xZlDe3RB0i4MgbqFKLZe65o/sha8JuKvU=
|
||||
github.com/luxfi/cache v1.2.1 h1:kAzOS55/hmYeNKR+0HAKv4ma48Y6JjkI8UQeqdZ8bfI=
|
||||
github.com/luxfi/cache v1.2.1/go.mod h1:co7JTxZZHpKT31Yh01LFp5aZOxmoUg157FhBLQdQHVU=
|
||||
github.com/luxfi/chains v1.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/cache v1.3.1 h1:grQhi/B5GKypG7avDMeY143QTgFbfEvQICKNIh1Cw6U=
|
||||
github.com/luxfi/cache v1.3.1/go.mod h1:2MokdbeNUy/9O3mdREWkE6BiN7tRvePkXiKkcb+4M7g=
|
||||
github.com/luxfi/chains v1.7.9 h1:sDF/WQn56o/4i1/uCS8i5ENDMuyWi8+iVcnYGAstMDM=
|
||||
github.com/luxfi/chains v1.7.9/go.mod h1:Qtx9JNLOWYyhnvq434YSjPINT6we9nfViUIIuGD/2bc=
|
||||
github.com/luxfi/codec v1.2.1 h1:NA/O3dWm9QejQPdjLEIVx42ddVqAXsy6Y6igL/V1+aU=
|
||||
github.com/luxfi/codec v1.2.1/go.mod h1:xjWOTEbw9gxY/N8nZwQPvRCfPnK/ugJHBWsb3BZ0HHs=
|
||||
github.com/luxfi/compress v0.1.1 h1:cQjRYQFRrw8HinjW1T8FmKgqdRwrFYDCdecc1t0i+uQ=
|
||||
github.com/luxfi/compress v0.1.1/go.mod h1:d4CkRRmwPzafIp57Jxra3RmAmNwNwU8Oc0QBBRlTqGo=
|
||||
github.com/luxfi/concurrent v0.1.1 h1:LkAmNXybOsAm97nFILqMDBp/YLgleuLmyGomFT7YNxU=
|
||||
github.com/luxfi/concurrent v0.1.1/go.mod h1:GlkfiJtPBpatNxvROf7hkPi4gsnmdHGmqhDnVWFrkZE=
|
||||
github.com/luxfi/config v1.1.2/go.mod h1:z6t0a5pGpQz2uDW2qJPLX5fZ/eWbpiNa51gBc63ebFk=
|
||||
github.com/luxfi/consensus v1.36.10 h1:LJPvHc2zL7VFhbqfYu11rA50yvxzTK17Z4mRN04HQhU=
|
||||
github.com/luxfi/consensus v1.36.10/go.mod h1:iwlx11D7BRdEaUqyUvueAe2Fit1K1k26avBK2eLYdnc=
|
||||
github.com/luxfi/consensus v1.36.12 h1:MJKq6j9+04Y2NC+NArGQgv5QNcjprYCk+gppCz1PdSg=
|
||||
github.com/luxfi/consensus v1.36.12/go.mod h1:mCtA6k31FVW/H6rsYQ7V8VvS198WsFS8oFo/z3IMI+A=
|
||||
github.com/luxfi/constants v1.6.2 h1:pXHdKIFbfE9qX4xOjq2LxYvagNhhNvspUVEbPcIEKfA=
|
||||
github.com/luxfi/constants v1.6.2/go.mod h1:r0oH8C/+r/XFYBq1AJxt6zWRKKRKgDzrEMop/CCs9rI=
|
||||
github.com/luxfi/container v0.2.1 h1:MTnfKXzS5+oxV5jKZerdOxSA6iMPaQI9/FWGufizzaw=
|
||||
github.com/luxfi/container v0.2.1/go.mod h1:B+uM0wP0lGvt/SSK7QOEn/qBcsHzILVHlKikdCyzSgM=
|
||||
github.com/luxfi/corona v0.10.4 h1:/+Uy5iOWBMkr+XACnRRiyrbb5ebsZLsUXjHfJW3sFyw=
|
||||
github.com/luxfi/corona v0.10.4/go.mod h1:44Tjnm2uRG22kmmLfCzR8QEO8OXZ3jR/OnUKLsnjVJ8=
|
||||
github.com/luxfi/crypto v1.20.2 h1:L81WEsU/hs2A76F5PWBusG0yU74QqkDdUqqgexWUxh4=
|
||||
github.com/luxfi/crypto v1.20.2/go.mod h1:qYHOM0lO4PRh7LEaObxFQUIMjmT1/paVm/WgZkobT1k=
|
||||
github.com/luxfi/crypto/ipa v1.2.4 h1:6xfwhI9/HrcDkF3Ti5/NxsNQIWbwYDJmRSNIHRQ/xfU=
|
||||
github.com/luxfi/crypto/ipa v1.2.4/go.mod h1:43J6f6rcfUMrZt4cQectMOZb6Ps+fAEj8ZTPC3Kk+gE=
|
||||
github.com/luxfi/database v1.20.4 h1:WOt2GIGJxf8AFpg49odMz8DZ8RFSLDrozGhZtmorN70=
|
||||
github.com/luxfi/database v1.20.4/go.mod h1:S/LvmfzNYWVNslcEcZwDrntqUO2ksaL8ql1nRmLUA/Q=
|
||||
github.com/luxfi/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/go.mod h1:wYWQmwospvdKBWQ6OJMXb8I+kfgEtE46R1rD8H7vHCQ=
|
||||
github.com/luxfi/dkg v0.3.5 h1:s2L2mMQaz+n9m0b0ghvoV5VZNxiwb2z4WrGugvK0udY=
|
||||
github.com/luxfi/dkg v0.3.5/go.mod h1:M+WH7GFRN+YUD851Rlnumdp0Md98kplNN8pVx65U8I8=
|
||||
github.com/luxfi/evm v1.104.10/go.mod h1:4xBXSPz0d6cRa+XSN65zL5yeJDuASlgCjg8qt86EZLM=
|
||||
github.com/luxfi/fhe v1.11.1/go.mod h1:QeNytuStXaY+HWvUN4+ULZBRMzvnrzjA/AV6FWksBro=
|
||||
github.com/luxfi/filesystem v0.0.1 h1:VZ6xMFKaAPBW/ddlMsDnI2G0VU1lV5rYaVcW5d+KwEY=
|
||||
github.com/luxfi/filesystem v0.0.1/go.mod h1:OQVSU6XNwqrr1AI+MqkID2taHUclx7NYmmr3svgttec=
|
||||
github.com/luxfi/formatting v1.0.1 h1:ZnE1rAdEUds9yAegdVdGDOBGN6hLMPOv6E03Fp8IEYo=
|
||||
github.com/luxfi/formatting v1.0.1/go.mod h1:mYzNf5DJOiqSSKUPzNj5dKy4tstFbN3pZlkI5716eKc=
|
||||
github.com/luxfi/genesis v1.13.16 h1:suwWPwUu2nv1fxvx9vwHgcgJCzkCpiVMxBrJIh4S3BQ=
|
||||
github.com/luxfi/genesis v1.13.16/go.mod h1:qUa+AcTWwxv0x+CJochBsRNOMbmEBjw07HJKZLhs5c0=
|
||||
github.com/luxfi/formatting v1.1.1 h1:MJhVXIPh1dbysvYEjtaEA/Z0FUTiI7n0DwOF54FS08c=
|
||||
github.com/luxfi/formatting v1.1.1/go.mod h1:zhBWp6fLZduhpiAdPgVDdPVOyhw4FvwRUksF6+xKQCE=
|
||||
github.com/luxfi/genesis v1.16.4 h1:8ZtqvgPnICgGpjBzAjYAPrI3qwr1g1DPbum+hgjc4bg=
|
||||
github.com/luxfi/genesis v1.16.4/go.mod h1:0F6hV6GfwDSmIWsueB7/vqPZFxyS7A4Jo3p1QU87fFc=
|
||||
github.com/luxfi/genesis/builder v0.1.0/go.mod h1:p0vgZzt/aOTSlpw7VSvhh4Wfdp99FErW37CDlCAp88o=
|
||||
github.com/luxfi/genesis/pkg/genesis/security v1.13.8 h1:9ier0p55ErSpoXHRJpZ04WV5HwwMB1uDrU7PHGBKG2U=
|
||||
github.com/luxfi/genesis/pkg/genesis/security v1.13.8/go.mod h1:DzU+GYUFv12ja4Vc46bWKNBBmNYbcow3u/DASx4wpfI=
|
||||
github.com/luxfi/geth v1.17.12 h1:UP/fhpcfbGPTrkOCwX3d88Oc3jVm5gTOgfjgq+lek6s=
|
||||
github.com/luxfi/geth v1.17.12/go.mod h1:3vQfQJd9JC+AVBjxNXa9PYQOqpbE2dKu8E3jqhPZ3LU=
|
||||
github.com/luxfi/go-bip32 v1.0.2 h1:7vFbb+Wr4Z499q2tuCLdd7wWjtn8sH+HWBlx76mhH9Y=
|
||||
github.com/luxfi/go-bip32 v1.0.2/go.mod h1:bc7/LXDKAJQZ/F0Xjf5yXaTZxY9/ssLb4FC+Hxn/cDk=
|
||||
github.com/luxfi/go-bip39 v1.1.2 h1:p+wLMPGs6MLQh7q0YIsmy2EhHL7LHiELEGTJko6t/Jg=
|
||||
github.com/luxfi/go-bip39 v1.1.2/go.mod h1:96de9VkR2kY/ASAnhMtvt3TSh+PZkAFAngNj0GjRGDo=
|
||||
github.com/luxfi/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/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/gpu v1.0.1/go.mod h1:8r1ReBPsLx1zmcA3GksKD4J7azP52FSTU/LpT9TyYHE=
|
||||
github.com/luxfi/hid v0.9.3/go.mod h1:XJ/7DZAHf5dggm3zWNbitKuFGB7J96b4iX+8NO3BsnY=
|
||||
github.com/luxfi/ids v1.3.2 h1:c6Rft5kZB4XqiCtWaGH47bfhaNFm3FGRfhEzI01GVeI=
|
||||
github.com/luxfi/ids v1.3.2/go.mod h1:+5l8cYMbKpORJbQ2r98CYJo9TQATgUdnmzpYFZWMwwc=
|
||||
github.com/luxfi/keychain v1.1.1 h1:dTYEPy6CGVC1sogMci4iJogUvW6VdTmemplQdzRqnAs=
|
||||
github.com/luxfi/keychain v1.1.1/go.mod h1:hAzBcwxGumtoYrM5hfhwdt8wE0p7r2JCd5AxswqfkoY=
|
||||
github.com/luxfi/keys v1.4.1 h1:2Zcoovaz9OLPz7m7VGXfRrGnrlqt0GeUpJclsPBi4EU=
|
||||
github.com/luxfi/keys v1.4.1/go.mod h1:P8EUP5DKrR1SUZBGZjDT3rWcp2P1miUlVh7IBRNBphU=
|
||||
github.com/luxfi/kms v1.12.10 h1:fXgisnBkixFSrqhOFbZVRQkf8cX4q4vS3Gix1qjL3PQ=
|
||||
github.com/luxfi/kms v1.12.10/go.mod h1:CCcWDXIlDT2TwfYAUxedLyzShFRJAlVlp15zQ2L3CrU=
|
||||
github.com/luxfi/lattice/v7 v7.1.4 h1:hQR02M6cHTAV5+joOPi9gb9Gm+z/hKJnhJF4IlciIJs=
|
||||
github.com/luxfi/lattice/v7 v7.1.4/go.mod h1:DmIQFi3mJiehVsR235l1NKYEU0JhU649OX5p7gMEW2c=
|
||||
github.com/luxfi/lens v0.1.4 h1:goGjGDXx2BNdjzXDunL5QT8elK2ZyCcc0z8TAbtWYrg=
|
||||
github.com/luxfi/lens v0.1.4/go.mod h1:mL+G8IK+9L41d78/2FYRgfhEzAjcr5+VEXB8SGuHbus=
|
||||
github.com/luxfi/ledger v1.2.1/go.mod h1:sthw0IUSsdO/eKvzAQBZsEZEVn8jDE7vDo95NYmlLJs=
|
||||
github.com/luxfi/lens v0.2.1 h1:5Qd0GdjbM+XUVgwDbZ452tKkR7yeE8QnBTHHaH8fJNY=
|
||||
github.com/luxfi/lens v0.2.1/go.mod h1:6FIhC8weEE5RbNMF3SaE+XPSB9cr6FmjypYBoHkz4JQ=
|
||||
github.com/luxfi/light v1.0.0 h1:zTJgp5M0xX8rIwRjYDQgOGhLas3rgXyhkszrTX+ne3s=
|
||||
github.com/luxfi/light v1.0.0/go.mod h1:1G0kgjEe/srlBMCIrFq4IvhnrMuHKFG18CRUWfw1z30=
|
||||
github.com/luxfi/log v1.4.3 h1:xkUKRWvQ4ZwvlUC2e0/RTtHYZOYSMvSQ9W9lbjwBmiI=
|
||||
github.com/luxfi/log v1.4.3/go.mod h1:myIkufyiQomSQH34K981kbz6cG4WUoerRUh7F4XhlQI=
|
||||
github.com/luxfi/lpm v1.10.1/go.mod h1:cy3dgb91fAgn1WXqyt8IJol3mlEKwCzQ2yOlaO2ykMo=
|
||||
github.com/luxfi/magnetar v1.2.3 h1:n4UrJZLK+mhDDZr1HLl2H/KgA6o6v62r5oiC61R7awE=
|
||||
github.com/luxfi/magnetar v1.2.3/go.mod h1:z9PLkqzzYiaFGT/qFBQSnNoHmZrg8y7JlYGiNnHAAdk=
|
||||
github.com/luxfi/math v1.4.1 h1:1t9bCCsEqnl9yIKrShlbs80DBKyYTWdnzkVfBqEeO7Q=
|
||||
github.com/luxfi/math v1.4.1/go.mod h1:QvbRxauQyE1w4lvbcLSe6c8yeJz2Zj1Bq1rayGgs2tA=
|
||||
github.com/luxfi/math v1.5.1 h1:FDOY75e4vn/Xra1ij99xOS/9XdxQGCPP6HONHRkCwfg=
|
||||
github.com/luxfi/math v1.5.1/go.mod h1:3j9R24hVfPhrbvs45YSJP7jAyVNfwx/cj/+lAO8IGro=
|
||||
github.com/luxfi/math/big v0.1.0 h1:Vz4c0RsZVPdIKPsHPgAJChH/R3p15WHRUz7LkLf+NIQ=
|
||||
github.com/luxfi/math/big v0.1.0/go.mod h1:BuxSu22RbO93xBLk5Eam5nldFponoJ73xDFz4uJ3Huk=
|
||||
github.com/luxfi/math/safe v0.0.1 h1:GfSBINV9mOFgHzd32JbgfHSLhlNn0BwnP43rteYEosc=
|
||||
github.com/luxfi/math/safe v0.0.1/go.mod h1:EejrmOJHh03YAD8+Zww8cPcMR1K3Q2I7w1dX4sMloeo=
|
||||
github.com/luxfi/mdns v0.1.1 h1:g2eRr9AXcziPkkcd24M+Qu9ApEpoKKjfI79QSNqv0rQ=
|
||||
github.com/luxfi/mdns v0.1.1/go.mod h1:dbp5f3h3aE7CGzwbaWzBM9cwdcekhmSrWhQevgYhhNA=
|
||||
github.com/luxfi/metric v1.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/metric v1.8.1 h1:v58GgPFAOLPVxSa/JiNLwqJQNEFHdWbXZV28piMXX4s=
|
||||
github.com/luxfi/metric v1.8.1/go.mod h1:R1OPAIeW4UBW3osK7j2r3/XPmczfNRFTXg4bnlemTuE=
|
||||
github.com/luxfi/mlwe v0.3.0 h1:5mtXLbO2RxaE45r75sj43c6UdpjDKQ5nTQcOGuoRQT8=
|
||||
github.com/luxfi/mlwe v0.3.0/go.mod h1:DD9EHTeiyh/y0KGGeqL+q9S4n8raeGiGdaG/BQPAvT0=
|
||||
github.com/luxfi/mock v0.1.1 h1:0HEtIjg1J6CWz+IUyP6rsGqNWTcmxjFnSQIhaDuARwY=
|
||||
github.com/luxfi/mock v0.1.1/go.mod h1:jo35akl3Vtd8LbzDts8VJ0jmSVycrd1/eBi6g6t5hKU=
|
||||
github.com/luxfi/net v0.0.5 h1:F1lD3NsIioV0wr2V5jWc4TtMyiE/Ffo1LoeblFv3TrI=
|
||||
github.com/luxfi/net v0.0.5/go.mod h1:BEQR1HEVmkjii/F1R6vJrNUVE7wr55b4eMq9Iz5wjUw=
|
||||
github.com/luxfi/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/net v0.1.1 h1:jIQCb8ulBGEvHIcorzDDNCeWzutFzLVtRWodutrQE24=
|
||||
github.com/luxfi/net v0.1.1/go.mod h1:SwxbUQ538u4QAcgC/N61uahDk1TDHL6Ku89CX/WV2lk=
|
||||
github.com/luxfi/netrunner v1.20.1/go.mod h1:B3xcQObhRofrZP0KLswL4szwiQG40USCZ5uGeH+mnWs=
|
||||
github.com/luxfi/oracle v1.1.1/go.mod h1:ST/4cVIUBa08GnkI9vQbfg8GGt0oDCXfTnEkx0WsnPs=
|
||||
github.com/luxfi/ordering v0.0.1/go.mod h1:Ld5UAayScKxicsd9BYuQ/D7vzEUndOC7cGCT+fj++jM=
|
||||
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/password v0.0.1/go.mod h1:xKhi28+V3xqPc4+kIZHRp7oZ1W7nUKze6EauvMVq19s=
|
||||
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/pubsub v1.0.0/go.mod h1:lCVBPBK/fhD0Ffg57E9R44CEI6Bo1fqb9ISJrBlj4ug=
|
||||
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/relay v1.1.1/go.mod h1:jdgwxl8G0iykzyqW43uazRPsdz49awgv24kev3cMiQI=
|
||||
github.com/luxfi/resource v0.1.1 h1:k11s5xLGX85UWq/iLZyWLhnqeLTlp3FHEt8u/8AHdkY=
|
||||
github.com/luxfi/resource v0.1.1/go.mod h1:s7SbZOSVbgj9bWFOcLCcXgnMlHxbqtqhAeJ3f0Xuy/Y=
|
||||
github.com/luxfi/rpc v1.1.0 h1:B/PJbK399th1mHRDSufhCpVbAciZqId3LsaWhIGNWH4=
|
||||
github.com/luxfi/rpc v1.1.0/go.mod h1:s0bI7/Wg1ZdFdG/cQK+4pZNdEmUsXNBA3HeZRZ+XLeM=
|
||||
github.com/luxfi/runtime v1.1.3 h1:6Yp/PKwQCohjXmBR9GA+gamdSAp+xA2rdN6J/74Y4aw=
|
||||
github.com/luxfi/runtime v1.1.3/go.mod h1:r1uonDnxRCnPz6N6WYwaC72HW95KbFIAyChnJyxePGs=
|
||||
github.com/luxfi/runtime v1.3.1 h1:vsQZ3sl6XMeyHuNGCMM86d0whrE/lhMre4CCJaClPdE=
|
||||
github.com/luxfi/runtime v1.3.1/go.mod h1:Tct998uUcmCQbUC1WLEeGLDH2IaVgMb+SkKcHKZpHV0=
|
||||
github.com/luxfi/sampler v1.1.0 h1:u3iRDl7V06ARh0e85h3HT+aZ1saCFo2yMMsh+dCJbqk=
|
||||
github.com/luxfi/sampler v1.1.0/go.mod h1:kJa53S3tC9+VSbuV3RFu68MmbCCBlr2UM39LOClQ/Hs=
|
||||
github.com/luxfi/sdk v1.17.9 h1:MfExzWNym7IicO2egiHg6N0WnImLtAUpjCpiD/zc2ZE=
|
||||
github.com/luxfi/sdk v1.17.9/go.mod h1:XvZuopyltjR4SvHvA1c6wtNcnO+FzLyjfm0v+FyN9sI=
|
||||
github.com/luxfi/staking v1.5.1 h1:f9MaGnRm0xc02crDm5Qs1T2r88d3KzNkHZypAvsmAlU=
|
||||
github.com/luxfi/staking v1.5.1/go.mod h1:lT7KLaiTpdq3lg78H0gp2qSEfX9LaK1vs7w73XV/9nw=
|
||||
github.com/luxfi/sdk v1.18.1 h1:KnqkL6bNATtQXHbnTMsue5pLz9tn74YFAoux2ek+k90=
|
||||
github.com/luxfi/sdk v1.18.1/go.mod h1:8BE1iF2ewkDbsV31TPE0/HT7RGJILR1ZALbKE1GgHMc=
|
||||
github.com/luxfi/sdk/api v0.0.2/go.mod h1:p0fv13IlUKFTx16y//rtpp6D/qBZa8xX9gYkKp6j/9g=
|
||||
github.com/luxfi/staking v1.6.1 h1:be023mY88AnFgF9P+MMvILX21I2CaqVAVkGcc+a20so=
|
||||
github.com/luxfi/staking v1.6.1/go.mod h1:X8i/uCLc009mlIUnFMErrQMCwfGaOVCAVf+GbDN5nn4=
|
||||
github.com/luxfi/sys v0.1.0 h1:M7RYOt8W4Wws7cxxsyOHe50UKMYTzIu7HYknqW4xt0Y=
|
||||
github.com/luxfi/sys v0.1.0/go.mod h1:GT8vGdYTfoqRy9/11blmRuqPPypzwrudCTHZXT+ru9M=
|
||||
github.com/luxfi/threshold v1.12.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/threshold v1.12.3 h1:CYqcPfzVUs0M/+e/ja1q7aLBZZUi6Rv/XIXR5lrZ7aE=
|
||||
github.com/luxfi/threshold v1.12.3/go.mod h1:xQT8xzmh9pQ1CdBgpl7P3ezXQZcqTPv3tqpIytvuiTA=
|
||||
github.com/luxfi/timer v1.1.1 h1:54GiNBKydQ0VF5/EwVc/mCsbqe0yJNfZV7Ae8qJhCwk=
|
||||
github.com/luxfi/timer v1.1.1/go.mod h1:OXY/8ZFKCdEsimpfnUG1MQWvzjjFbsmBOiW/m6KSrC0=
|
||||
github.com/luxfi/tls v1.1.1 h1:BSZ0gHSp7U8vzlmzx7WSSCz+b7Ky4JtD9HDDhn7vrDg=
|
||||
github.com/luxfi/tls v1.1.1/go.mod h1:+5TDy8UtLL+tz124brZzpUDBRj+sKrq0JFqdmpMUHgw=
|
||||
github.com/luxfi/trace v1.2.1 h1:MPV079P2eTijB7F06AyJU1HJwpQVxRx1qYXhva9YsvM=
|
||||
github.com/luxfi/trace v1.2.1/go.mod h1:/bX8g0RRHPHUq7kX8of/Aaq6C7rD0JuNH57vVbw7ZG8=
|
||||
github.com/luxfi/units v1.0.0 h1:2aNVB+WsP1XeDob71IsO0w3jJqP3FtZdYnFsmORkJZg=
|
||||
github.com/luxfi/units v1.0.0/go.mod h1:tma28v4ed1tupdS0kpSeyO+u1wWK/g1NqODPbN1YzmA=
|
||||
github.com/luxfi/upgrade v1.0.1 h1:7+ygYeUf/MuLeGL7pjIu6ckQimxctCp+Swybhpy64go=
|
||||
github.com/luxfi/upgrade v1.0.1/go.mod h1:Re7g9Y+SYf/LvkHFpN0vbtlVH/Rr5ZpHQdPeVFEo3Jw=
|
||||
github.com/luxfi/utils v1.2.0 h1:gtEiI7/NM6PQ/OasEpH0PvB+e5hIS/tpum9r64pYjMc=
|
||||
github.com/luxfi/utils v1.2.0/go.mod h1:T2OCKT1xG9jtKR/gyJQoSkticzrE9WFQ8eohJHGu9Fg=
|
||||
github.com/luxfi/utxo v0.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/upgrade v1.0.3 h1:mFMfIb88HzoL4fp7I6w1g+VnxlAWj6UQdZOdf4AkCLk=
|
||||
github.com/luxfi/upgrade v1.0.3/go.mod h1:3c/u4T/n7EsODofkWg5VigSt9BATQM8EmaNttfHMCrU=
|
||||
github.com/luxfi/utils v1.3.1 h1:Us02ag60kGu94B41XIelExa7c+K6zPKwDJyq+eB+hc0=
|
||||
github.com/luxfi/utils v1.3.1/go.mod h1:ROZrzpt6Kx8ttS1mo12oqsOzRB088GQ1h9jXEoDDpNA=
|
||||
github.com/luxfi/utxo v0.5.8 h1:HydTOKERb8vY0Z39Dvg/V3qg7My3N/eXOY4nLv3iezg=
|
||||
github.com/luxfi/utxo v0.5.8/go.mod h1:kqkwMm99NbWwGZrOzuRzF0vck+lCJwrlejmmCwj5pZc=
|
||||
github.com/luxfi/validators v1.3.1 h1:+/7j0CTXlMKyaSLFM+gd6Fq64/edORJfrD/xGnf7Xcg=
|
||||
github.com/luxfi/validators v1.3.1/go.mod h1:sIQyUZOvXoJ/9/RCOhHSzjumZIoxiqacNOn5mQWVozU=
|
||||
github.com/luxfi/version v1.0.1 h1:T/1KYWEMmsrNQk7pN7PFPAwh/7XbeX7cFAKLBqI37Sk=
|
||||
github.com/luxfi/version v1.0.1/go.mod h1:Y5fPkQ2DB0XRBCxgSPXp4ISzL1/jptKnmFknShRJCyg=
|
||||
github.com/luxfi/vm v1.2.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/vm v1.3.1 h1:U2Bv7IDRFv8JtjUARfY53ZnOPRB2iPrPHSMDCQ+T0Js=
|
||||
github.com/luxfi/vm v1.3.1/go.mod h1:6YR/uFV2FoofmogQShv+HM7V8alz5rbrQYjp5w0bNVA=
|
||||
github.com/luxfi/vm v1.3.3 h1:BEBamD9VUcPG8mtL2Ga0Us1lTQ+ubaZqng/PZjNqzPc=
|
||||
github.com/luxfi/vm v1.3.3/go.mod h1:v2XjW0yymxHwy1xPsSz338s0J8nRqOIb6RiDf0wUEgk=
|
||||
github.com/luxfi/warp v1.24.1 h1:9F+z6fy4sxmXp+3LlR9m3TiZ8Dfthh+s5KOeewSJL30=
|
||||
github.com/luxfi/warp v1.24.1/go.mod h1:kRGQDt6EB3oRLYo71aXqnmJuCBVfND3oQ5/2miaLoyg=
|
||||
github.com/luxfi/zap v1.2.6 h1:NBpbm9Gib41Oi/XAkAZKQ3hb+xCafo7JsrUjw+bKiAc=
|
||||
github.com/luxfi/zap v1.2.6/go.mod h1:sTAe/AMMamoE85cVoe81+NbqHJkgvqS0LhY9ByHEmr0=
|
||||
github.com/luxfi/zapcodec v1.1.1 h1:SdYexj7oWdks/wfF9s+8m/9PAYt3S8QjORxURutvIs0=
|
||||
github.com/luxfi/zapcodec v1.1.1/go.mod h1:fmmgd8C/JrQdh3b1OzBkrvPN4TYs5uRfVV3sVtbiLbQ=
|
||||
github.com/luxfi/zapdb v1.10.1 h1:XV3k4UTTKKxUMgbfC7woPXgUEIJd3P5nj2lGTQ88xeE=
|
||||
github.com/luxfi/zapdb v1.10.1/go.mod h1:3Y0hH2A9kvjR+Bp9N2yEbtHnhXGHhqCQOLvBRkHrrM0=
|
||||
github.com/luxfi/zwing v0.5.2 h1:2+nDKHVIdT8GvaKO79GuO3x/3DgJvX0gI32h4P+P6RU=
|
||||
github.com/luxfi/zwing v0.5.2/go.mod h1:8nixkEL3bhO2LrqVqhJ8WgT+QGUtnCPIQIhFQh9gQio=
|
||||
github.com/luxfi/zwing v0.6.1 h1:Ve7RvYVBXx4JWe8TZhLfpez1N1G685hl/PSMDsR4RI4=
|
||||
github.com/luxfi/zwing v0.6.1/go.mod h1:Eal4hnjmdFXc6rxciA6cxeCfV1BKs8le03v83W5oiKg=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
|
||||
github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
|
||||
github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
|
||||
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
|
||||
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/melbahja/goph v1.4.0/go.mod h1:uG+VfK2Dlhk+O32zFrRlc3kYKTlV6+BtvPWd/kK7U68=
|
||||
github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE=
|
||||
github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A=
|
||||
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
@@ -454,23 +578,36 @@ github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI
|
||||
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.0.100/go.mod h1:EtGNKtlX20iL2yaYnxEigaIvj0G0GwSDnifnG8ClIdw=
|
||||
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
||||
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/pointerstructure v1.2.1 h1:ZhBBeX8tSlRpu/FFhXH4RC4OJzFlqsQhoHZAz4x7TIw=
|
||||
github.com/mitchellh/pointerstructure v1.2.1/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4=
|
||||
github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/montanaflynn/stats v0.9.0 h1:tsBJ0RXwph9BmAuFoCmqGv6e8xa0MENQ8m0ptKq29mQ=
|
||||
github.com/montanaflynn/stats v0.9.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||
github.com/mr-tron/base58 v1.3.0 h1:K6Y13R2h+dku0wOqKtecgRnBUBPrZzLZy5aIj8lCcJI=
|
||||
github.com/mr-tron/base58 v1.3.0/go.mod h1:2BuubE67DCSWwVfx37JWNG8emOC0sHEU4/HpcYgCLX8=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0=
|
||||
github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E=
|
||||
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 h1:4kuARK6Y6FxaNu/BnU2OAaLF86eTVhP2hjTB6iMvItA=
|
||||
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354/go.mod h1:KSVJerMDfblTH7p5MZaTt+8zaT2iEk3AkVb9PQdZuE8=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/oapi-codegen/runtime v1.3.1/go.mod h1:kOdeacKy7t40Rclb1je37ZLFboFxh+YLy0zaPCMibPY=
|
||||
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
|
||||
github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
@@ -491,6 +628,7 @@ github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
|
||||
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
|
||||
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
|
||||
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63 h1:+FZIDR/D97YOPik4N4lPDaUcLDF/EQPogxtlHB2ZZRM=
|
||||
@@ -507,26 +645,34 @@ github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkY
|
||||
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
|
||||
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
|
||||
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg=
|
||||
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posthog/posthog-go v1.8.2/go.mod h1:ueZiJCmHezyDHI/swIR1RmOfktLehnahJnFxEvQ9mnQ=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/common v0.68.0 h1:8rQJvQmYltsR2L7h8Zw0Iyj8WYNNmpwikoQTZXwfVeA=
|
||||
github.com/prometheus/common v0.68.0/go.mod h1:4soH+U8yJSROk7OJ//hmTiWKsxapv6zRGgTt3keN8gQ=
|
||||
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/protolambda/bls12-381-util v0.1.0/go.mod h1:cdkysJTRpeFeuUVx/TXGDQNMTiRAalk1vQw3TYTHcE4=
|
||||
github.com/protolambda/zrnt v0.34.1/go.mod h1:A0fezkp9Tt3GBLATSPIbuY4ywYESyAuc/FFmPKg8Lqs=
|
||||
github.com/protolambda/ztyp v0.2.2/go.mod h1:9bYgKGqg3wJqT9ac1gI2hnVb0STQq7p/1lapqrqY1dU=
|
||||
github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
|
||||
github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc=
|
||||
github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
|
||||
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
@@ -537,8 +683,15 @@ github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88ee
|
||||
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
||||
github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo=
|
||||
github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
|
||||
github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
@@ -550,6 +703,7 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
github.com/status-im/keycard-go v0.3.3/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@@ -584,27 +738,40 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
|
||||
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk=
|
||||
github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk=
|
||||
github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU=
|
||||
github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI=
|
||||
github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4=
|
||||
github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg=
|
||||
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
|
||||
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.73.0 h1:ocTOORnBWtJ+P8t/6wAjdkchMzdfHmWx2VD/DPbgZ7s=
|
||||
github.com/valyala/fasthttp v1.73.0/go.mod h1:EtXQDHaR+5P18p8wqDRFpUhxr108Ga9mXvVJXHRrN2k=
|
||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
||||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
|
||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
|
||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
github.com/zap-proto/http v0.0.0-20260506200741-fd6047874433 h1:7WsCr/pZvWozimdYNffL3B9K6gLr8w0Z7WAi1+eZWtc=
|
||||
github.com/zap-proto/http v0.0.0-20260506200741-fd6047874433/go.mod h1:xySyVTIwjknmVE+p+6rukkX86rFZtXeAu/QY7KXMYqw=
|
||||
github.com/zap-proto/go v1.1.0 h1:DDGhuTBNqkmNax7QV/VvkInJR7hiOcybwUjN3YBt8fg=
|
||||
github.com/zap-proto/go v1.1.0/go.mod h1:914SNGTH6Rv3Yu1MweWJBPEN8FZlo5C39QyhaB0C7Q0=
|
||||
github.com/zap-proto/http v0.3.0 h1:l7DvlngiYqmzNY6fzyRYw2ZIAhF35FqwOe6mvAOqpMg=
|
||||
github.com/zap-proto/http v0.3.0/go.mod h1:UYfGhDDCetgxs65XSev8Lpf65COg5vKQK+cWwZGh4zQ=
|
||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
|
||||
@@ -615,8 +782,12 @@ go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5
|
||||
go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/zpages v0.62.0/go.mod h1:C8kXoiC1Ytvereztus2R+kqdSa6W/MZ8FfS8Zwj+LiM=
|
||||
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
|
||||
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
@@ -625,8 +796,10 @@ go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfC
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
||||
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
@@ -643,16 +816,17 @@ golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnf
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||
golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9 h1:4d4PbuBNwaxMXkXI8yiIYjydtMU+04RHeuSxJdgKftM=
|
||||
golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
|
||||
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
@@ -667,15 +841,17 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/perf v0.0.0-20230113213139-801c7ef9e5c5/go.mod h1:UBKtEnL8aqnd+0JHqZ+2qoMDwtuy6cYhhKNoHLBiTQc=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -698,17 +874,19 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57/go.mod h1:3AWMyWHS+caVoiEXpiq6+tzKA40J4vQT3MYr80ZtQpc=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -718,8 +896,10 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||
golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
|
||||
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -727,6 +907,14 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
gonum.org/v1/plot v0.15.2/go.mod h1:DX+x+DWso3LTha+AdkJEv5Txvi+Tql3KAGkehP0/Ubg=
|
||||
gonum.org/v1/tools v0.0.0-20200318103217-c168b003ce8c/go.mod h1:fy6Otjqbk477ELp8IXTpw1cObQtLbRCBVonY+bTTfcM=
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
|
||||
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=
|
||||
@@ -740,11 +928,15 @@ google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
@@ -754,3 +946,19 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug=
|
||||
k8s.io/apimachinery v0.34.0/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
|
||||
k8s.io/client-go v0.34.0/go.mod h1:ozgMnEKXkRjeMvBZdV1AijMHLTh3pbACPvK7zFR+QQY=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/sqlite v1.50.0/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA=
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
|
||||
+1
-1
@@ -70,4 +70,4 @@ esac
|
||||
|
||||
echo ""
|
||||
echo "Import complete! Verify with:"
|
||||
echo " curl -s -X POST --data '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_blockNumber\",\"params\":[]}' -H 'content-type:application/json' \$RPC_URL/ext/bc/<blockchain-id>/rpc"
|
||||
echo " curl -s -X POST --data '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_blockNumber\",\"params\":[]}' -H 'content-type:application/json' \$RPC_URL/v1/bc/<blockchain-id>/rpc"
|
||||
|
||||
+2
-2
@@ -21,8 +21,8 @@ type Client struct {
|
||||
// calls.
|
||||
// [uri] is the path to make API calls to.
|
||||
// For example:
|
||||
// - http://1.2.3.4:9650/ext/index/C/block
|
||||
// - http://1.2.3.4:9650/ext/index/X/tx
|
||||
// - http://1.2.3.4:9650/v1/index/C/block
|
||||
// - http://1.2.3.4:9650/v1/index/X/tx
|
||||
func NewClient(uri string) *Client {
|
||||
return &Client{
|
||||
Requester: rpc.NewEndpointRequester(uri),
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
// and prints the ID of the block and its transactions.
|
||||
func main() {
|
||||
var (
|
||||
uri = primary.LocalAPIURI + "/ext/index/P/block"
|
||||
uri = primary.LocalAPIURI + "/v1/index/P/block"
|
||||
client = indexer.NewClient(uri)
|
||||
ctx = context.Background()
|
||||
nextIndex uint64
|
||||
@@ -39,7 +39,7 @@ func main() {
|
||||
platformvmBlockBytes = proposerVMBlock.Block()
|
||||
}
|
||||
|
||||
platformvmBlock, err := platformvmblock.Parse(platformvmblock.Codec, platformvmBlockBytes)
|
||||
platformvmBlock, err := platformvmblock.Parse(platformvmBlockBytes)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to parse platformvm block: %s\n", err)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
// and prints the ID of the block and its transactions.
|
||||
func main() {
|
||||
var (
|
||||
uri = primary.LocalAPIURI + "/ext/index/X/block"
|
||||
uri = primary.LocalAPIURI + "/v1/index/X/block"
|
||||
xChainID = ids.FromStringOrPanic("2eNy1mUFdmaxXNj1eQHUe7Np4gju9sJsEtWQ4MX3ToiNKuADed")
|
||||
client = indexer.NewClient(uri)
|
||||
ctx = context.Background()
|
||||
|
||||
+13
-13
@@ -25,29 +25,29 @@ Each chain has one or more index. To see if a C-Chain block is accepted, for exa
|
||||
### C-Chain Blocks
|
||||
|
||||
```
|
||||
/ext/index/C/block
|
||||
/v1/index/C/block
|
||||
```
|
||||
|
||||
### P-Chain Blocks
|
||||
|
||||
```
|
||||
/ext/index/P/block
|
||||
/v1/index/P/block
|
||||
```
|
||||
|
||||
### X-Chain Transactions
|
||||
|
||||
```
|
||||
/ext/index/X/tx
|
||||
/v1/index/X/tx
|
||||
```
|
||||
|
||||
### X-Chain Blocks
|
||||
|
||||
```
|
||||
/ext/index/X/block
|
||||
/v1/index/X/block
|
||||
```
|
||||
|
||||
<Callout type="warn">
|
||||
To ensure historical data can be accessed, the `/ext/index/X/vtx` is still accessible, even though it is no longer populated with chain data since the X-Chain block linearization landed. If you are using `V1.10.0` or higher, you need to migrate to using the `/ext/index/X/block` endpoint.
|
||||
To ensure historical data can be accessed, the `/v1/index/X/vtx` is still accessible, even though it is no longer populated with chain data since the X-Chain block linearization landed. If you are using `V1.10.0` or higher, you need to migrate to using the `/v1/index/X/block` endpoint.
|
||||
</Callout>
|
||||
|
||||
## Methods
|
||||
@@ -87,7 +87,7 @@ index.getContainerByID({
|
||||
**Example Call**:
|
||||
|
||||
```sh
|
||||
curl --location --request POST 'localhost:9630/ext/index/X/tx' \
|
||||
curl --location --request POST 'localhost:9630/v1/index/X/tx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"jsonrpc": "2.0",
|
||||
@@ -151,7 +151,7 @@ index.getContainerByIndex({
|
||||
**Example Call**:
|
||||
|
||||
```sh
|
||||
curl --location --request POST 'localhost:9630/ext/index/X/tx' \
|
||||
curl --location --request POST 'localhost:9630/v1/index/X/tx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"jsonrpc": "2.0",
|
||||
@@ -223,7 +223,7 @@ index.getContainerRange({
|
||||
**Example Call**:
|
||||
|
||||
```sh
|
||||
curl --location --request POST 'localhost:9630/ext/index/X/tx' \
|
||||
curl --location --request POST 'localhost:9630/v1/index/X/tx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"jsonrpc": "2.0",
|
||||
@@ -282,7 +282,7 @@ index.getIndex({
|
||||
**Example Call**:
|
||||
|
||||
```sh
|
||||
curl --location --request POST 'localhost:9630/ext/index/X/tx' \
|
||||
curl --location --request POST 'localhost:9630/v1/index/X/tx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"jsonrpc": "2.0",
|
||||
@@ -339,7 +339,7 @@ index.getLastAccepted({
|
||||
**Example Call**:
|
||||
|
||||
```sh
|
||||
curl --location --request POST 'localhost:9630/ext/index/X/tx' \
|
||||
curl --location --request POST 'localhost:9630/v1/index/X/tx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"jsonrpc": "2.0",
|
||||
@@ -394,7 +394,7 @@ index.isAccepted({
|
||||
**Example Call**:
|
||||
|
||||
```sh
|
||||
curl --location --request POST 'localhost:9630/ext/index/X/tx' \
|
||||
curl --location --request POST 'localhost:9630/v1/index/X/tx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"jsonrpc": "2.0",
|
||||
@@ -430,7 +430,7 @@ To get an X-Chain transaction by its index (the order it was accepted in), use I
|
||||
For example, to get the second transaction (note that `"index":1`) accepted on the X-Chain, do:
|
||||
|
||||
```sh
|
||||
curl --location --request POST 'https://indexer-demo.lux.network/ext/index/X/tx' \
|
||||
curl --location --request POST 'https://indexer-demo.lux.network/v1/index/X/tx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"jsonrpc": "2.0",
|
||||
@@ -474,7 +474,7 @@ curl -X POST --data '{
|
||||
"txID":"ZGYTSU8w3zUP6VFseGC798vA2Vnxnfj6fz1QPfA9N93bhjJvo",
|
||||
"encoding": "json"
|
||||
}
|
||||
}' -H 'content-type:application/json;' https://api.lux.network/ext/bc/X
|
||||
}' -H 'content-type:application/json;' https://api.lux.network/v1/bc/X
|
||||
```
|
||||
|
||||
**Response**:
|
||||
|
||||
@@ -79,7 +79,7 @@ spec:
|
||||
cpu: "4"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /ext/health
|
||||
path: /v1/health
|
||||
port: 9630
|
||||
initialDelaySeconds: 120
|
||||
periodSeconds: 30
|
||||
@@ -87,7 +87,7 @@ spec:
|
||||
timeoutSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /ext/health
|
||||
path: /v1/health
|
||||
port: 9630
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
@@ -95,7 +95,7 @@ spec:
|
||||
timeoutSeconds: 5
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /ext/health
|
||||
path: /v1/health
|
||||
port: 9630
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
|
||||
+36
@@ -34,6 +34,20 @@ type Net interface {
|
||||
// IsBootstrapped returns true if the chains in this chain are done bootstrapping
|
||||
IsBootstrapped() bool
|
||||
|
||||
// Bootstrapping returns the IDs of the chains in this net that have NOT yet
|
||||
// finished initial sync. The net owns the bootstrapping set, so it is the
|
||||
// net — not its caller — that can name those chains.
|
||||
Bootstrapping() []ids.ID
|
||||
|
||||
// IsChainBootstrapped reports whether a SPECIFIC chain in this net has finished
|
||||
// initial sync — i.e. Bootstrapped(chainID) was called for it (the chain reached
|
||||
// the network frontier and its VM went to normal operation). This is the per-chain
|
||||
// truth that info.isBootstrapped keys on, distinct from IsBootstrapped() which is
|
||||
// the net-wide "no chain still bootstrapping" aggregate. A chain that is merely
|
||||
// tracked (added, sync goroutine launched) but has NOT converged is still in the
|
||||
// bootstrapping set and reads false here — closing the premature-true masking bug.
|
||||
IsChainBootstrapped(chainID ids.ID) bool
|
||||
|
||||
// Bootstrapped marks the chain as done bootstrapping
|
||||
Bootstrapped(chainID ids.ID)
|
||||
|
||||
@@ -66,6 +80,28 @@ func (s *chain) IsBootstrapped() bool {
|
||||
return s.bootstrapping.Len() == 0
|
||||
}
|
||||
|
||||
func (s *chain) Bootstrapping() []ids.ID {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
|
||||
return s.bootstrapping.List()
|
||||
}
|
||||
|
||||
// IsChainBootstrapped assumes MONOTONIC per-process bootstrapped state: a chain
|
||||
// only ever moves bootstrapping→bootstrapped (Bootstrapped is forward-only and
|
||||
// AddChain refuses to re-add a chain already in either set), so this signal — and
|
||||
// the Bootstrapping() health check that shares the set — never report stale-true.
|
||||
// INVARIANT: if a future path ever moves a live chain BACK to bootstrapping (e.g.
|
||||
// SetState(Bootstrapping) on a running chain) it MUST remove the chainID from
|
||||
// bootstrapped, or both this signal and the readiness health check will report a
|
||||
// re-syncing chain as still bootstrapped.
|
||||
func (s *chain) IsChainBootstrapped(chainID ids.ID) bool {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
|
||||
return s.bootstrapped.Contains(chainID)
|
||||
}
|
||||
|
||||
func (s *chain) Bootstrapped(chainID ids.ID) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
@@ -68,3 +68,26 @@ func TestIsAllowed(t *testing.T) {
|
||||
require.False(s.IsAllowed(ids.GenerateTestNodeID(), false), "Non-validator should not be allowed with validator only rules and allowed nodes")
|
||||
require.True(s.IsAllowed(allowedNodeID, true), "Non-validator allowed node should be allowed with validator only rules and allowed nodes")
|
||||
}
|
||||
|
||||
// Bootstrapping must name the CHAINS still syncing. The net-level aggregate
|
||||
// IsBootstrapped() only says "something here is unconverged"; only the chain
|
||||
// IDs say what.
|
||||
func TestNetBootstrappingNamesTheChains(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
chainID0 := ids.GenerateTestID()
|
||||
chainID1 := ids.GenerateTestID()
|
||||
|
||||
s := New(ids.GenerateTestNodeID(), Config{})
|
||||
require.Empty(s.Bootstrapping())
|
||||
|
||||
s.AddChain(chainID0)
|
||||
s.AddChain(chainID1)
|
||||
require.ElementsMatch([]ids.ID{chainID0, chainID1}, s.Bootstrapping())
|
||||
|
||||
s.Bootstrapped(chainID0)
|
||||
require.Equal([]ids.ID{chainID1}, s.Bootstrapping())
|
||||
|
||||
s.Bootstrapped(chainID1)
|
||||
require.Empty(s.Bootstrapping())
|
||||
}
|
||||
|
||||
+8
-6
@@ -340,15 +340,16 @@ func NewNetwork(
|
||||
// AddPermissionlessValidatorTx (modern). Handle both.
|
||||
switch tx := validatorTx.Unsigned.(type) {
|
||||
case *txs.AddPermissionlessValidatorTx:
|
||||
nodeID := tx.Validator.NodeID
|
||||
weight := tx.Validator.Wght
|
||||
validator := tx.Validator()
|
||||
nodeID := validator.NodeID
|
||||
weight := validator.Wght
|
||||
if weight == 0 {
|
||||
weight = 1
|
||||
}
|
||||
|
||||
var blsKey []byte
|
||||
if tx.Signer != nil {
|
||||
if pubKey := tx.Signer.Key(); pubKey != nil {
|
||||
if s := tx.Signer(); s != nil {
|
||||
if pubKey := s.Key(); pubKey != nil {
|
||||
blsKey = bls.PublicKeyToCompressedBytes(pubKey)
|
||||
}
|
||||
}
|
||||
@@ -368,8 +369,9 @@ func NewNetwork(
|
||||
zap.Int("blsKeyLen", len(blsKey)),
|
||||
)
|
||||
case *txs.AddValidatorTx:
|
||||
nodeID := tx.Validator.NodeID
|
||||
weight := tx.Validator.Wght
|
||||
validator := tx.Validator()
|
||||
nodeID := validator.NodeID
|
||||
weight := validator.Wght
|
||||
if weight == 0 {
|
||||
weight = 1
|
||||
}
|
||||
|
||||
+74
-5
@@ -14,9 +14,10 @@ import (
|
||||
"github.com/luxfi/math/set"
|
||||
"github.com/luxfi/node/message"
|
||||
"github.com/luxfi/node/proto/p2p"
|
||||
"github.com/luxfi/node/trace"
|
||||
"github.com/luxfi/node/version"
|
||||
"github.com/luxfi/timer"
|
||||
"github.com/luxfi/node/trace"
|
||||
luxversion "github.com/luxfi/version"
|
||||
)
|
||||
|
||||
// router implements Router interface for routing messages to chain handlers
|
||||
@@ -193,26 +194,94 @@ func (r *chainRouter) AddChain(ctx context.Context, chainID ids.ID, h handler.Ha
|
||||
)
|
||||
}
|
||||
|
||||
// versionedConnector is the capability a chain handler advertises when it can
|
||||
// forward a peer's REAL application version to its VM. The consensus
|
||||
// handler.Handler.Connected signature carries only the nodeID (the version was
|
||||
// dropped at that boundary); a handler that implements this receives the real
|
||||
// version instead. blockHandler implements it. The version must survive to the
|
||||
// inner VM: the C-Chain (coreth) state-sync peer tracker compares peer versions
|
||||
// and dereferences a nil version, panicking a state-syncing node.
|
||||
type versionedConnector interface {
|
||||
ConnectedWithVersion(ctx context.Context, nodeID ids.NodeID, nodeVersion *luxversion.Application) error
|
||||
}
|
||||
|
||||
// toAppVersion converts the node's peer version (github.com/luxfi/node/version)
|
||||
// to the github.com/luxfi/version.Application the VM Connected boundary
|
||||
// (chain.VersionInfo) expects. nil-safe: a nil peer version maps to nil.
|
||||
func toAppVersion(v *version.Application) *luxversion.Application {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
return &luxversion.Application{
|
||||
Name: v.Name,
|
||||
Major: v.Major,
|
||||
Minor: v.Minor,
|
||||
Patch: v.Patch,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *chainRouter) Connected(nodeID ids.NodeID, nodeVersion *version.Application, netID ids.ID) {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
|
||||
r.connectedPeers.Add(nodeID)
|
||||
handlers := make([]handler.Handler, 0, len(r.chains))
|
||||
for _, h := range r.chains {
|
||||
handlers = append(handlers, h)
|
||||
}
|
||||
r.lock.Unlock()
|
||||
|
||||
r.log.Debug("peer connected",
|
||||
log.Stringer("nodeID", nodeID),
|
||||
log.Any("version", nodeVersion),
|
||||
log.Stringer("netID", netID),
|
||||
)
|
||||
|
||||
// Deliver the connection to every chain handler so VMs observe peer
|
||||
// connectivity — the P-chain uptime tracker in particular. Handlers dedup, so
|
||||
// repeated dispatch of the same connection (once per tracked network) is
|
||||
// safe. Dispatch OUTSIDE the router lock: a handler must never re-enter the
|
||||
// router while we hold it.
|
||||
//
|
||||
// Deliver the REAL peer version through the versionedConnector capability so
|
||||
// it reaches the inner VM (proposervm → coreth state-sync). Only handlers
|
||||
// that cannot carry a version fall back to the plain nodeID-only Connected.
|
||||
appVersion := toAppVersion(nodeVersion)
|
||||
for _, h := range handlers {
|
||||
var err error
|
||||
if vc, ok := h.(versionedConnector); ok {
|
||||
err = vc.ConnectedWithVersion(context.Background(), nodeID, appVersion)
|
||||
} else {
|
||||
err = h.Connected(context.Background(), nodeID)
|
||||
}
|
||||
if err != nil {
|
||||
r.log.Debug("chain handler Connected failed",
|
||||
log.Stringer("nodeID", nodeID),
|
||||
log.Err(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *chainRouter) Disconnected(nodeID ids.NodeID) {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
|
||||
r.connectedPeers.Remove(nodeID)
|
||||
handlers := make([]handler.Handler, 0, len(r.chains))
|
||||
for _, h := range r.chains {
|
||||
handlers = append(handlers, h)
|
||||
}
|
||||
r.lock.Unlock()
|
||||
|
||||
r.log.Debug("peer disconnected",
|
||||
log.Stringer("nodeID", nodeID),
|
||||
)
|
||||
|
||||
for _, h := range handlers {
|
||||
if err := h.Disconnected(context.Background(), nodeID); err != nil {
|
||||
r.log.Debug("chain handler Disconnected failed",
|
||||
log.Stringer("nodeID", nodeID),
|
||||
log.Err(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *chainRouter) Benched(chainID ids.ID, nodeID ids.NodeID) {
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package node
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/luxfi/consensus/networking/handler"
|
||||
"github.com/luxfi/constants"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/math/set"
|
||||
"github.com/luxfi/node/version"
|
||||
luxversion "github.com/luxfi/version"
|
||||
)
|
||||
|
||||
// fakeVersionedHandler implements handler.Handler AND the versionedConnector
|
||||
// capability, recording which path the router used and the version delivered.
|
||||
type fakeVersionedHandler struct {
|
||||
mu sync.Mutex
|
||||
versionedHit bool
|
||||
plainHit bool
|
||||
gotAppVersion *luxversion.Application
|
||||
}
|
||||
|
||||
func (h *fakeVersionedHandler) HandleInbound(context.Context, handler.Message) error { return nil }
|
||||
func (h *fakeVersionedHandler) HandleOutbound(context.Context, handler.Message) error { return nil }
|
||||
|
||||
func (h *fakeVersionedHandler) Connected(context.Context, ids.NodeID) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.plainHit = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *fakeVersionedHandler) Disconnected(context.Context, ids.NodeID) error { return nil }
|
||||
|
||||
func (h *fakeVersionedHandler) ConnectedWithVersion(_ context.Context, _ ids.NodeID, v *luxversion.Application) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.versionedHit = true
|
||||
h.gotAppVersion = v
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestChainRouterConnectedDeliversConvertedVersion is the router half of the
|
||||
// RED CRITICAL #1 fix: chainRouter.Connected must deliver the REAL peer version
|
||||
// to a version-capable handler, converting it from the node's peer version type
|
||||
// (github.com/luxfi/node/version) to the VM boundary type
|
||||
// (github.com/luxfi/version, aka chain.VersionInfo). The old code dropped the
|
||||
// version at dispatch (h.Connected(ctx, nodeID)); the fix routes through the
|
||||
// versionedConnector capability so the real, converted version survives.
|
||||
func TestChainRouterConnectedDeliversConvertedVersion(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
h := &fakeVersionedHandler{}
|
||||
chainID := ids.GenerateTestID()
|
||||
|
||||
r := &chainRouter{
|
||||
log: log.Noop(),
|
||||
chains: map[ids.ID]handler.Handler{chainID: h},
|
||||
connectedPeers: set.NewSet[ids.NodeID](1),
|
||||
}
|
||||
|
||||
nodeID := ids.GenerateTestNodeID()
|
||||
peerVersion := &version.Application{Name: "lux", Major: 1, Minor: 36, Patch: 27}
|
||||
|
||||
r.Connected(nodeID, peerVersion, constants.PrimaryNetworkID)
|
||||
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
require.True(h.versionedHit, "router must use the versioned capability path for a version-capable handler")
|
||||
require.False(h.plainHit, "router must NOT fall back to the nil-version plain Connected")
|
||||
require.NotNil(h.gotAppVersion, "handler must receive a non-nil converted version")
|
||||
require.Equal("lux", h.gotAppVersion.Name)
|
||||
require.Equal(1, h.gotAppVersion.Major)
|
||||
require.Equal(36, h.gotAppVersion.Minor)
|
||||
require.Equal(27, h.gotAppVersion.Patch)
|
||||
}
|
||||
|
||||
// TestToAppVersionNilSafe documents that a nil peer version converts to nil
|
||||
// (never a panic) — the conversion is defensive at the boundary.
|
||||
func TestToAppVersionNilSafe(t *testing.T) {
|
||||
require.Nil(t, toAppVersion(nil))
|
||||
}
|
||||
-265
@@ -1,265 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package node
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/crypto/bls"
|
||||
"github.com/luxfi/genesis/pkg/genesis"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/math/set"
|
||||
"github.com/luxfi/node/server/http"
|
||||
"github.com/luxfi/node/benchlist"
|
||||
"github.com/luxfi/node/chains"
|
||||
"github.com/luxfi/node/nets"
|
||||
"github.com/luxfi/node/network"
|
||||
"github.com/luxfi/node/trace"
|
||||
"github.com/luxfi/node/upgrade"
|
||||
"github.com/luxfi/node/vms/platformvm/txs/fee"
|
||||
"github.com/luxfi/timer"
|
||||
"github.com/luxfi/node/utils/profiler"
|
||||
)
|
||||
|
||||
type APIIndexerConfig struct {
|
||||
IndexAPIEnabled bool `json:"indexAPIEnabled"`
|
||||
IndexAllowIncomplete bool `json:"indexAllowIncomplete"`
|
||||
}
|
||||
|
||||
type TargeterConfig struct {
|
||||
// VdrAlloc is the percentage of resource usage that is attributed to validators
|
||||
// The range is [0, 1], defaults to 1 (100%)
|
||||
VdrAlloc float64 `json:"vdrAlloc"`
|
||||
// MaxNonVdrUsage is the maximum amount of resources that non-validators can use
|
||||
// The range is [0, 1], defaults to 0
|
||||
MaxNonVdrUsage float64 `json:"maxNonVdrUsage"`
|
||||
// MaxNonVdrNodeUsage is the maximum amount of resources that a non-validator node can use
|
||||
// The range is [0, 1], defaults to 0
|
||||
MaxNonVdrNodeUsage float64 `json:"maxNonVdrNodeUsage"`
|
||||
}
|
||||
|
||||
type HTTPConfig struct {
|
||||
server.HTTPConfig
|
||||
APIConfig `json:"apiConfig"`
|
||||
HTTPHost string `json:"httpHost"`
|
||||
HTTPPort uint16 `json:"httpPort"`
|
||||
|
||||
HTTPSEnabled bool `json:"httpsEnabled"`
|
||||
HTTPSKey []byte `json:"-"`
|
||||
HTTPSCert []byte `json:"-"`
|
||||
|
||||
HTTPAllowedOrigins []string `json:"httpAllowedOrigins"`
|
||||
HTTPAllowedHosts []string `json:"httpAllowedHosts"`
|
||||
|
||||
ShutdownTimeout time.Duration `json:"shutdownTimeout"`
|
||||
ShutdownWait time.Duration `json:"shutdownWait"`
|
||||
}
|
||||
|
||||
type APIConfig struct {
|
||||
APIIndexerConfig `json:"indexerConfig"`
|
||||
|
||||
// Enable/Disable APIs
|
||||
AdminAPIEnabled bool `json:"adminAPIEnabled"`
|
||||
InfoAPIEnabled bool `json:"infoAPIEnabled"`
|
||||
KeystoreAPIEnabled bool `json:"keystoreAPIEnabled"`
|
||||
MetricsAPIEnabled bool `json:"metricsAPIEnabled"`
|
||||
HealthAPIEnabled bool `json:"healthAPIEnabled"`
|
||||
}
|
||||
|
||||
type IPConfig struct {
|
||||
PublicIP string `json:"publicIP"`
|
||||
PublicIPResolutionService string `json:"publicIPResolutionService"`
|
||||
PublicIPResolutionFreq time.Duration `json:"publicIPResolutionFreq"`
|
||||
// The host portion of the address to listen on. The port to
|
||||
// listen on will be sourced from IPPort.
|
||||
//
|
||||
// - If empty, listen on all interfaces (both ipv4 and ipv6).
|
||||
// - If populated, listen only on the specified address.
|
||||
ListenHost string `json:"listenHost"`
|
||||
ListenPort uint16 `json:"listenPort"`
|
||||
}
|
||||
|
||||
type StakingConfig struct {
|
||||
genesis.StakingConfig
|
||||
SybilProtectionEnabled bool `json:"sybilProtectionEnabled"`
|
||||
PartialSyncPrimaryNetwork bool `json:"partialSyncPrimaryNetwork"`
|
||||
StakingTLSCert tls.Certificate `json:"-"`
|
||||
StakingSigningKey *bls.SecretKey `json:"-"`
|
||||
SybilProtectionDisabledWeight uint64 `json:"sybilProtectionDisabledWeight"`
|
||||
StakingKeyPath string `json:"stakingKeyPath"`
|
||||
StakingCertPath string `json:"stakingCertPath"`
|
||||
StakingSignerPath string `json:"stakingSignerPath"`
|
||||
}
|
||||
|
||||
type StateSyncConfig struct {
|
||||
StateSyncIDs []ids.NodeID `json:"stateSyncIDs"`
|
||||
StateSyncIPs []netip.AddrPort `json:"stateSyncIPs"`
|
||||
}
|
||||
|
||||
type BootstrapConfig struct {
|
||||
// Timeout before emitting a warn log when connecting to bootstrapping beacons
|
||||
BootstrapBeaconConnectionTimeout time.Duration `json:"bootstrapBeaconConnectionTimeout"`
|
||||
|
||||
// Max number of containers in an ancestors message sent by this node.
|
||||
BootstrapAncestorsMaxContainersSent int `json:"bootstrapAncestorsMaxContainersSent"`
|
||||
|
||||
// This node will only consider the first [AncestorsMaxContainersReceived]
|
||||
// containers in an ancestors message it receives.
|
||||
BootstrapAncestorsMaxContainersReceived int `json:"bootstrapAncestorsMaxContainersReceived"`
|
||||
|
||||
// Max time to spend fetching a container and its
|
||||
// ancestors while responding to a GetAncestors message
|
||||
BootstrapMaxTimeGetAncestors time.Duration `json:"bootstrapMaxTimeGetAncestors"`
|
||||
|
||||
Bootstrappers []genesis.Bootstrapper `json:"bootstrappers"`
|
||||
|
||||
// Skip bootstrapping and start processing immediately
|
||||
SkipBootstrap bool `json:"skipBootstrap"`
|
||||
|
||||
// Enable automining in POA mode
|
||||
EnableAutomining bool `json:"enableAutomining"`
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
// If true, all writes are to memory and are discarded at node shutdown.
|
||||
ReadOnly bool `json:"readOnly"`
|
||||
|
||||
// Path to database
|
||||
Path string `json:"path"`
|
||||
|
||||
// Name of the database type to use
|
||||
Name string `json:"name"`
|
||||
|
||||
// Path to config file
|
||||
Config []byte `json:"-"`
|
||||
}
|
||||
|
||||
// Config contains all of the configurations of a Lux node.
|
||||
type Config struct {
|
||||
HTTPConfig `json:"httpConfig"`
|
||||
IPConfig `json:"ipConfig"`
|
||||
StakingConfig `json:"stakingConfig"`
|
||||
fee.StaticConfig `json:"txFeeConfig"`
|
||||
StateSyncConfig `json:"stateSyncConfig"`
|
||||
BootstrapConfig `json:"bootstrapConfig"`
|
||||
DatabaseConfig `json:"databaseConfig"`
|
||||
|
||||
UpgradeConfig upgrade.Config `json:"upgradeConfig"`
|
||||
|
||||
// Genesis information
|
||||
GenesisBytes []byte `json:"-"`
|
||||
UTXOAssetID ids.ID `json:"utxoAssetID"`
|
||||
|
||||
// ID of the network this node should connect to
|
||||
NetworkID uint32 `json:"networkID"`
|
||||
|
||||
// Health
|
||||
HealthCheckFreq time.Duration `json:"healthCheckFreq"`
|
||||
|
||||
// Network configuration
|
||||
NetworkConfig network.Config `json:"networkConfig"`
|
||||
|
||||
AdaptiveTimeoutConfig timer.AdaptiveTimeoutConfig `json:"adaptiveTimeoutConfig"`
|
||||
|
||||
BenchlistConfig benchlist.Config `json:"benchlistConfig"`
|
||||
|
||||
ProfilerConfig profiler.Config `json:"profilerConfig"`
|
||||
|
||||
// LoggingConfig log.Config `json:"loggingConfig"` // log.Config doesn't exist
|
||||
|
||||
PluginDir string `json:"pluginDir"`
|
||||
// DevMode enables local PoA + auto-mine mode (network-id=local, sybil-protection disabled)
|
||||
DevMode bool `json:"devMode"`
|
||||
|
||||
// File Descriptor Limit
|
||||
FdLimit uint64 `json:"fdLimit"`
|
||||
|
||||
// Metrics
|
||||
MeterVMEnabled bool `json:"meterVMEnabled"`
|
||||
// Delay between automatic block proposals when DevMode is set
|
||||
DevBlockDelay time.Duration `json:"devBlockDelay"`
|
||||
|
||||
RouterHealthConfig HealthConfig `json:"routerHealthConfig"`
|
||||
ConsensusShutdownTimeout time.Duration `json:"consensusShutdownTimeout"`
|
||||
// Poll for new frontiers every [FrontierPollFrequency]
|
||||
FrontierPollFrequency time.Duration `json:"consensusGossipFreq"`
|
||||
// ConsensusAppConcurrency defines the maximum number of goroutines to
|
||||
// handle App messages per chain.
|
||||
ConsensusAppConcurrency int `json:"consensusAppConcurrency"`
|
||||
|
||||
TrackedChains set.Set[ids.ID] `json:"trackedChains"`
|
||||
TrackAllChains bool `json:"trackAllChains"`
|
||||
|
||||
NetConfigs map[ids.ID]nets.Config `json:"chainConfigs"`
|
||||
|
||||
ChainConfigs map[string]chains.ChainConfig `json:"-"`
|
||||
ChainAliases map[ids.ID][]string `json:"chainAliases"`
|
||||
|
||||
VMAliases map[ids.ID][]string `json:"vmAliases"`
|
||||
|
||||
// Halflife to use for the processing requests tracker.
|
||||
// Larger halflife --> usage metrics change more slowly.
|
||||
SystemTrackerProcessingHalflife time.Duration `json:"systemTrackerProcessingHalflife"`
|
||||
|
||||
// Frequency to check the real resource usage of tracked processes.
|
||||
// More frequent checks --> usage metrics are more accurate, but more
|
||||
// expensive to track
|
||||
SystemTrackerFrequency time.Duration `json:"systemTrackerFrequency"`
|
||||
|
||||
// Halflife to use for the cpu tracker.
|
||||
// Larger halflife --> cpu usage metrics change more slowly.
|
||||
SystemTrackerCPUHalflife time.Duration `json:"systemTrackerCPUHalflife"`
|
||||
|
||||
// Halflife to use for the disk tracker.
|
||||
// Larger halflife --> disk usage metrics change more slowly.
|
||||
SystemTrackerDiskHalflife time.Duration `json:"systemTrackerDiskHalflife"`
|
||||
|
||||
CPUTargeterConfig TargeterConfig `json:"cpuTargeterConfig"`
|
||||
|
||||
DiskTargeterConfig TargeterConfig `json:"diskTargeterConfig"`
|
||||
|
||||
RequiredAvailableDiskSpace uint64 `json:"requiredAvailableDiskSpace"`
|
||||
WarningThresholdAvailableDiskSpace uint64 `json:"warningThresholdAvailableDiskSpace"`
|
||||
|
||||
TraceConfig trace.Config `json:"traceConfig"`
|
||||
|
||||
// See comment on [UseCurrentHeight] in platformvm.Config
|
||||
UseCurrentHeight bool `json:"useCurrentHeight"`
|
||||
|
||||
// ProvidedFlags contains all the flags set by the user
|
||||
ProvidedFlags map[string]interface{} `json:"-"`
|
||||
|
||||
// ChainDataDir is the root path for per-chain directories where VMs can
|
||||
// write arbitrary data.
|
||||
ChainDataDir string `json:"chainDataDir"`
|
||||
|
||||
// ImportChainData is the path to import blockchain data from another chain
|
||||
ImportChainData string `json:"importChainData"`
|
||||
|
||||
// Path to write process context to (including PID, API URI, and
|
||||
// staking address).
|
||||
ProcessContextFilePath string `json:"processContextFilePath"`
|
||||
|
||||
// POA Mode Configuration
|
||||
POAModeEnabled bool `json:"poaModeEnabled"`
|
||||
POASingleNodeMode bool `json:"poaSingleNodeMode"`
|
||||
POAMinBlockTime time.Duration `json:"poaMinBlockTime"`
|
||||
POAAuthorizedNodes []string `json:"poaAuthorizedNodes"`
|
||||
|
||||
// Low Memory Configuration
|
||||
LowMemoryEnabled bool `json:"lowMemoryEnabled"`
|
||||
DBCacheSize uint64 `json:"dbCacheSize"`
|
||||
DBMemtableSize uint64 `json:"dbMemtableSize"`
|
||||
StateCacheSize uint64 `json:"stateCacheSize"`
|
||||
BlockCacheSize uint64 `json:"blockCacheSize"`
|
||||
DisableBloomFilters bool `json:"disableBloomFilters"`
|
||||
LazyChainLoading bool `json:"lazyChainLoading"`
|
||||
SingleValidatorMode bool `json:"singleValidatorMode"`
|
||||
|
||||
// Logging
|
||||
Log log.Logger `json:"-"`
|
||||
}
|
||||
+9
-5
@@ -1354,6 +1354,10 @@ func (n *Node) initChainManager(utxoAssetID ids.ID) error {
|
||||
SybilProtectionEnabled: n.Config.SybilProtectionEnabled,
|
||||
StakingTLSSigner: n.StakingTLSSigner,
|
||||
StakingTLSCert: n.StakingTLSCert,
|
||||
StakingMLDSASigner: n.Config.StakingConfig.StakingMLDSA,
|
||||
StakingMLDSAPub: n.Config.StakingConfig.StakingMLDSAPub,
|
||||
ProposerWindowDuration: n.Config.ProposerWindowDuration,
|
||||
ProposerMinBlockDelay: n.Config.ProposerMinBlockDelay,
|
||||
StakingBLSKey: n.Config.StakingSigningKey,
|
||||
Log: n.Log,
|
||||
LogFactory: n.LogFactory,
|
||||
@@ -1825,18 +1829,18 @@ func (n *Node) initInfoAPI() error {
|
||||
// initSecurityAPI exposes the chain-wide ChainSecurityProfile as a
|
||||
// read-only API surface. Three endpoints share one handler:
|
||||
//
|
||||
// - JSON-RPC: POST /ext/security with methods securityProfile and
|
||||
// - JSON-RPC: POST /v1/security with methods securityProfile and
|
||||
// blockSecurity (dispatched on the wire as security_securityProfile
|
||||
// / security_blockSecurity per gorilla/rpc namespace convention)
|
||||
// - REST: GET /ext/security/profile
|
||||
// - REST: GET /ext/security/block/{n}
|
||||
// - REST: GET /v1/security/profile
|
||||
// - REST: GET /v1/security/block/{n}
|
||||
//
|
||||
// All three share the same Service receiver; the shape returned is
|
||||
// the SCREAMING_SNAKE canonical profile JSON consumed by audit tooling,
|
||||
// wallet posture banners, and block explorers.
|
||||
//
|
||||
// Prometheus gauges for the active profile are stamped onto the
|
||||
// node-wide metrics gatherer here so /ext/metrics carries the profile
|
||||
// node-wide metrics gatherer here so /v1/metrics carries the profile
|
||||
// posture immediately after boot.
|
||||
//
|
||||
// Closes F102 follow-ups (securityProfile RPC + profile metrics).
|
||||
@@ -1844,7 +1848,7 @@ func (n *Node) initSecurityAPI() error {
|
||||
n.Log.Info("initializing security API")
|
||||
|
||||
// Register profile metrics under the "security" namespace on the
|
||||
// node-wide gatherer so /ext/metrics carries them alongside the
|
||||
// node-wide gatherer so /v1/metrics carries them alongside the
|
||||
// existing process / api / chain metric families.
|
||||
securityMetricsReg, err := metric.MakeAndRegister(
|
||||
n.MetricsGatherer,
|
||||
|
||||
+24
-11
@@ -114,17 +114,29 @@ var CoreVMs = map[ids.ID]CoreVM{
|
||||
// today's opt-in-flag behavior) while the gate itself remains fail-closed.
|
||||
//
|
||||
// C (evm) and the remaining app VMs are plugin-loaded but ungated today.
|
||||
//
|
||||
// Declaring a VM here is a statement of intent, not a guarantee that a binary
|
||||
// exists: the registry scan simply finds nothing and the chain cannot start.
|
||||
// fhevm (F-Chain) is exactly that case today — see its entry below.
|
||||
var OptionalVMs = map[ids.ID]PluginSpec{
|
||||
constants.DexVMID: {Name: "dexvm", RequiredNFT: &NFTRequirement{Collection: "dex-operator", GroupID: 0}},
|
||||
constants.BridgeVMID: {Name: "bridgevm", RequiredNFT: &NFTRequirement{Collection: "bridge-operator", GroupID: 0}},
|
||||
constants.EVMID: {Name: "evm"},
|
||||
constants.AIVMID: {Name: "aivm"},
|
||||
constants.GraphVMID: {Name: "graphvm"},
|
||||
constants.IdentityVMID: {Name: "identityvm"},
|
||||
constants.KeyVMID: {Name: "keyvm"},
|
||||
constants.OracleVMID: {Name: "oraclevm"},
|
||||
constants.RelayVMID: {Name: "relayvm"},
|
||||
constants.ThresholdVMID: {Name: "thresholdvm"},
|
||||
constants.DexVMID: {Name: "dexvm", RequiredNFT: &NFTRequirement{Collection: "dex-operator", GroupID: 0}},
|
||||
constants.BridgeVMID: {Name: "bridgevm", RequiredNFT: &NFTRequirement{Collection: "bridge-operator", GroupID: 0}},
|
||||
constants.EVMID: {Name: "evm"},
|
||||
constants.AIVMID: {Name: "aivm"},
|
||||
constants.GraphVMID: {Name: "graphvm"},
|
||||
constants.IdentityVMID: {Name: "identityvm"},
|
||||
constants.KeyVMID: {Name: "keyvm"},
|
||||
constants.OracleVMID: {Name: "oraclevm"},
|
||||
constants.RelayVMID: {Name: "relayvm"},
|
||||
constants.MPCVMID: {Name: "mpcvm"},
|
||||
// F-Chain. Reserved ID, no shipping VM: luxfi/chains has no fhevm/
|
||||
// directory, so `make` produces no fhevm binary and this scan always comes
|
||||
// up empty. The FHE runtime library lives at luxfi/chains/mpcvm/fhe as a
|
||||
// package inside mpcvm, not as a standalone chain. F-Chain is a spec
|
||||
// (LP-8200, LP-167). Kept declared so the intent stays visible and the ID
|
||||
// stays reserved — remove it only when F-Chain is cancelled, not merely
|
||||
// because it is unbuilt.
|
||||
constants.FHEVMID: {Name: "fhevm"},
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -154,7 +166,8 @@ func assertRegistriesDisjoint() error {
|
||||
// registerCoreVMs registers the in-process core VMs that carry a concrete
|
||||
// factory (Q, Z). P and X are registered separately in node.go because their
|
||||
// factories require live node dependencies (RegisteredInNodeGo=true in
|
||||
// CoreVMs). The optional chain VMs (A/B/C/D/G/I/K/O/R/T) are NEVER registered
|
||||
// CoreVMs). The optional chain VMs (A/B/C/D/F/G/I/K/M/O/R — the keys of
|
||||
// OptionalVMs; there is no T, LP-134 dissolved it) are NEVER registered
|
||||
// here — they load from PluginDir via the VMRegistry scan, exactly like any
|
||||
// other plugin. Registering an optional VM in-process would shadow its plugin
|
||||
// (the registry skips any VMID the manager already has — vms/registry/
|
||||
|
||||
@@ -58,7 +58,7 @@ func TestOptionalVMsNotLinkedInProcess(t *testing.T) {
|
||||
"github.com/luxfi/chains/keyvm",
|
||||
"github.com/luxfi/chains/oraclevm",
|
||||
"github.com/luxfi/chains/relayvm",
|
||||
"github.com/luxfi/chains/thresholdvm",
|
||||
"github.com/luxfi/chains/mpcvm",
|
||||
}
|
||||
for _, pkg := range []string{"./node/", "./main"} {
|
||||
deps := goListDeps(t, pkg)
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
FROM bufbuild/buf:1.26.1 AS builder
|
||||
|
||||
FROM ubuntu:20.04
|
||||
|
||||
RUN apt-get update && apt -y install bash curl unzip git
|
||||
WORKDIR /opt
|
||||
|
||||
RUN \
|
||||
curl -L https://golang.org/dl/go1.24.5.linux-amd64.tar.gz > golang.tar.gz && \
|
||||
mkdir golang && \
|
||||
tar -zxvf golang.tar.gz -C golang/
|
||||
|
||||
ENV PATH="${PATH}:/opt/golang/go/bin"
|
||||
|
||||
COPY --from=builder /usr/local/bin/buf /usr/local/bin/
|
||||
|
||||
# any version changes here should also be bumped in scripts/protobuf_codegen.sh
|
||||
RUN \
|
||||
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.30.0 && \
|
||||
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.3.0
|
||||
|
||||
ENV PATH="${PATH}:/root/go/bin/"
|
||||
@@ -1,36 +0,0 @@
|
||||
# Lux gRPC
|
||||
|
||||
Now Serving: **Protocol Version 42**
|
||||
|
||||
Protobuf files are hosted at
|
||||
[https://buf.build/luxfi/lux](https://buf.build/luxfi/lux) and
|
||||
can be used as dependencies in other projects.
|
||||
|
||||
Protobuf linting and generation for this project is managed by
|
||||
[buf](https://github.com/bufbuild/buf).
|
||||
|
||||
Please find installation instructions on
|
||||
[https://docs.buf.build/installation/](https://docs.buf.build/installation/).
|
||||
|
||||
Any changes made to proto definition can be updated by running
|
||||
`protobuf_codegen.sh` located in the `scripts/` directory of Lux Node.
|
||||
|
||||
`buf` Quickstart
|
||||
[https://buf.build/docs/cli/quickstart](https://buf.build/docs/cli/quickstart)
|
||||
|
||||
## Protocol Version Compatibility
|
||||
|
||||
The protobuf definitions and generated code are versioned based on the
|
||||
[RPCChainVMProtocol](../version/version.go#L13) defined for the RPCChainVM.
|
||||
Many versions of a Lux client can use the same
|
||||
[RPCChainVMProtocol](../version/version.go#L13). But each Lux client and
|
||||
chain VM must use the same protocol version to be compatible.
|
||||
|
||||
## Publishing to Buf Schema Registry
|
||||
|
||||
- Checkout appropriate tag in Lux Node `git checkout v1.10.1`
|
||||
- Change to proto/ directory `cd proto`.
|
||||
- Publish new tag to buf registry. `buf push -t v26`
|
||||
|
||||
Note: Publishing requires auth to the luxfi org in buf
|
||||
https://buf.build/luxfi/repositories
|
||||
@@ -1,8 +0,0 @@
|
||||
version: v1
|
||||
plugins:
|
||||
- name: go
|
||||
out: pb
|
||||
opt: paths=source_relative
|
||||
- name: go-grpc
|
||||
out: pb
|
||||
opt: paths=source_relative
|
||||
@@ -1,36 +0,0 @@
|
||||
# Lux gRPC
|
||||
|
||||
Now Serving: **Protocol Version 42**
|
||||
|
||||
Protobuf files are hosted at
|
||||
[https://buf.build/luxfi/lux](https://buf.build/luxfi/lux) and
|
||||
can be used as dependencies in other projects.
|
||||
|
||||
Protobuf linting and generation for this project is managed by
|
||||
[buf](https://github.com/bufbuild/buf).
|
||||
|
||||
Please find installation instructions on
|
||||
[https://docs.buf.build/installation/](https://docs.buf.build/installation/).
|
||||
|
||||
Any changes made to proto definition can be updated by running
|
||||
`protobuf_codegen.sh` located in the `scripts/` directory of Lux Node.
|
||||
|
||||
`buf` Quickstart
|
||||
[https://buf.build/docs/cli/quickstart](https://buf.build/docs/cli/quickstart)
|
||||
|
||||
## Protocol Version Compatibility
|
||||
|
||||
The protobuf definitions and generated code are versioned based on the
|
||||
[RPCChainVMProtocol](../version/version.go#L13) defined for the RPCChainVM.
|
||||
Many versions of a Lux client can use the same
|
||||
[RPCChainVMProtocol](../version/version.go#L13). But each Lux client and
|
||||
chain VM must use the same protocol version to be compatible.
|
||||
|
||||
## Publishing to Buf Schema Registry
|
||||
|
||||
- Checkout appropriate tag in Lux Node `git checkout v1.10.1`
|
||||
- Change to proto/ directory `cd proto`.
|
||||
- Publish new tag to buf registry. `buf push -t v26`
|
||||
|
||||
Note: Publishing requires auth to the luxfi org in buf
|
||||
https://buf.build/luxfi/repositories
|
||||
@@ -1,30 +0,0 @@
|
||||
version: v1
|
||||
name: buf.build/luxfi/lux
|
||||
build:
|
||||
excludes: []
|
||||
breaking:
|
||||
use:
|
||||
- FILE
|
||||
# deps removed - now using local io/metric/client/metrics.proto
|
||||
lint:
|
||||
use:
|
||||
- STANDARD
|
||||
except:
|
||||
- SERVICE_SUFFIX # service requirement of <name>+Service
|
||||
- RPC_REQUEST_STANDARD_NAME # explicit <rpc>+Request naming
|
||||
- RPC_RESPONSE_STANDARD_NAME # explicit <rpc>+Response naming
|
||||
- PACKAGE_VERSION_SUFFIX # versioned naming <service>.v1beta
|
||||
ignore:
|
||||
# TODO: how will fixing this affect functionality. Multiple fields are used as the request
|
||||
# or response type for multiple RPCs
|
||||
- aliasreader/aliasreader.proto
|
||||
- net/conn/conn.proto
|
||||
# Third-party proto from prometheus/client_model - don't lint
|
||||
- io/metric/client/metrics.proto
|
||||
# allows RPC requests or responses to be google.protobuf.Empty messages. This can be set if you
|
||||
# want to allow messages to be void forever, that is they will never take any parameters.
|
||||
rpc_allow_google_protobuf_empty_requests: true
|
||||
rpc_allow_google_protobuf_empty_responses: true
|
||||
# allows the same message type to be used for a single RPC's request and response type.
|
||||
# TODO: this should not be tolerated and if it is only perscriptivly.
|
||||
rpc_allow_same_request_response: true
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Rename types, methods, and functions to remove "App" prefix
|
||||
# Targeted at p2p message types and builder functions
|
||||
|
||||
# 1. Rename Message Types and Fields (Getters)
|
||||
find . -name "*.go" -type f -print0 | xargs -0 sed -i '' \
|
||||
-e 's/AppRequest/Request/g' \
|
||||
-e 's/AppResponse/Response/g' \
|
||||
-e 's/AppGossip/Gossip/g' \
|
||||
-e 's/AppError/Error/g' \
|
||||
-e 's/GetAppRequest/GetRequest/g' \
|
||||
-e 's/GetAppResponse/GetResponse/g' \
|
||||
-e 's/GetAppGossip/GetGossip/g' \
|
||||
-e 's/GetAppError/GetError/g'
|
||||
|
||||
# 2. Rename Builder Functions (Inbound/Outbound)
|
||||
# Note: s/AppRequest/Request/g above already handled the suffix.
|
||||
# Now we need to handle prefixes if they still exist.
|
||||
# e.g. InboundAppRequest -> InboundRequest (if AppRequest matched first, it became InboundRequest)
|
||||
# Check: InboundAppRequest -> InboundRequest.
|
||||
# So IsInboundAppRequest -> IsInboundRequest?
|
||||
# We should just ensure "InboundApp" -> "Inbound" (for any other patterns)
|
||||
find . -name "*.go" -type f -print0 | xargs -0 sed -i '' \
|
||||
-e 's/InboundApp/Inbound/g' \
|
||||
-e 's/OutboundApp/Outbound/g'
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Replace package declarations in moved files
|
||||
# Ensure we are fixing package names for moved files
|
||||
# vm/manager was 'package manager', so it fits node/vms/manager
|
||||
# vm/rpc was 'package rpc' (likely), needs to be 'package rpcchainvm'
|
||||
|
||||
if [ -d "vms/manager" ]; then
|
||||
sed -i '' 's/^package.*/package manager/' vms/manager/*.go
|
||||
fi
|
||||
if [ -d "vms/rpcchainvm" ]; then
|
||||
sed -i '' 's/^package.*/package rpcchainvm/' vms/rpcchainvm/*.go
|
||||
fi
|
||||
|
||||
# Global replacements
|
||||
DIRS=". ../precompile ../coreth ../evm ../rpc ../wallet ../staking"
|
||||
|
||||
for dir in $DIRS; do
|
||||
if [ -d "$dir" ]; then
|
||||
echo "Processing $dir..."
|
||||
find "$dir" -name "*.go" -type f -print0 | xargs -0 sed -i '' \
|
||||
-e 's|github.com/luxfi/consensus/runtime|github.com/luxfi/runtime|g' \
|
||||
-e 's|github.com/luxfi/consensus/validator|github.com/luxfi/validators|g' \
|
||||
-e 's|github.com/luxfi/consensus/version|github.com/luxfi/version|g' \
|
||||
-e 's|github.com/luxfi/vm/manager|github.com/luxfi/node/vms/manager|g' \
|
||||
-e 's|github.com/luxfi/vm/rpc|github.com/luxfi/node/vms/rpcchainvm|g' \
|
||||
-e 's|github.com/luxfi/vm/chains|github.com/luxfi/node/chains|g' \
|
||||
-e 's|version\.Current()|version.CurrentApp|g' \
|
||||
-e 's|consensusversion\.Current()|version.CurrentApp|g'
|
||||
fi
|
||||
done
|
||||
+13
-5
@@ -54,8 +54,16 @@ source "${REPO_ROOT}"/scripts/constants.sh
|
||||
# Determine the git commit hash to use for the build
|
||||
source "${REPO_ROOT}"/scripts/git_commit.sh
|
||||
|
||||
# Configure build based on profile
|
||||
tags=""
|
||||
# Configure build based on profile.
|
||||
#
|
||||
# `metrics` is a base tag, not a profile choice: without it luxfi/metric's
|
||||
# NewRegistry() resolves to the no-op registry (registry_noop.go, //go:build
|
||||
# !metrics), every metric registers into a black hole, and /v1/metrics answers
|
||||
# 200 with a zero-byte body — while --api-metrics-enabled=true still reports
|
||||
# metrics as on. Whether metrics are served is the runtime flag's decision
|
||||
# alone; the build must not silently overrule it.
|
||||
base_tags="metrics"
|
||||
tags="${base_tags}"
|
||||
ldflags="-X github.com/luxfi/node/version.GitCommit=$git_commit \
|
||||
-X github.com/luxfi/node/version.VersionMajor=$version_major \
|
||||
-X github.com/luxfi/node/version.VersionMinor=$version_minor \
|
||||
@@ -67,7 +75,7 @@ upx_compress=false
|
||||
case "${profile}" in
|
||||
minimal)
|
||||
echo "Profile: minimal (ZAP, all VMs, NAT, stripped)"
|
||||
tags="nattraversal"
|
||||
tags="${base_tags},nattraversal"
|
||||
strip_flags="-s -w"
|
||||
;;
|
||||
core)
|
||||
@@ -77,7 +85,7 @@ case "${profile}" in
|
||||
;;
|
||||
full)
|
||||
echo "Profile: full (gRPC+ZAP, all VMs, all features, stripped)"
|
||||
tags="grpc,nattraversal,zxcvbn,metrics"
|
||||
tags="${base_tags},grpc,nattraversal,zxcvbn"
|
||||
strip_flags="-s -w"
|
||||
;;
|
||||
dev)
|
||||
@@ -86,7 +94,7 @@ case "${profile}" in
|
||||
;;
|
||||
tiny)
|
||||
echo "Profile: tiny (all VMs, NAT + UPX compressed)"
|
||||
tags="nattraversal"
|
||||
tags="${base_tags},nattraversal"
|
||||
strip_flags="-s -w"
|
||||
upx_compress=true
|
||||
;;
|
||||
|
||||
@@ -36,9 +36,11 @@ export CGO_ENABLED="${CGO_ENABLED:-1}"
|
||||
# Disable version control fallbacks
|
||||
export GOPROXY="${GOPROXY:-https://proxy.golang.org}"
|
||||
|
||||
# Use GOPRIVATE to bypass Go proxy for luxfi packages (zip too large for proxy)
|
||||
export GOPRIVATE="${GOPRIVATE:-github.com/luxfi/*}"
|
||||
export GONOSUMDB="${GONOSUMDB:-github.com/luxfi/*}"
|
||||
# Only genuinely-private modules bypass the proxy. Everything else in luxfi is
|
||||
# public: GOPRIVATE would disable checksum verification for it, which is how
|
||||
# moved tags got captured into module caches unverified.
|
||||
export GOPRIVATE="${GOPRIVATE:-github.com/lux-private/*}"
|
||||
export GONOSUMDB="${GONOSUMDB:-github.com/lux-private/*}"
|
||||
|
||||
# Configure pkg-config path for C++ libraries (luxcpp)
|
||||
# Searches for installed libraries in common locations
|
||||
|
||||
@@ -72,7 +72,7 @@ echo ""
|
||||
# Wait for RPC to be ready
|
||||
echo -n "Waiting for RPC..."
|
||||
for _ in {1..30}; do
|
||||
if curl -s "http://127.0.0.1:$HTTP_PORT/ext/info" >/dev/null 2>&1; then
|
||||
if curl -s "http://127.0.0.1:$HTTP_PORT/v1/info" >/dev/null 2>&1; then
|
||||
echo " ready!"
|
||||
break
|
||||
fi
|
||||
@@ -83,10 +83,10 @@ done
|
||||
# Show status
|
||||
echo ""
|
||||
echo "=== Instance Ready ==="
|
||||
echo " C-Chain RPC: http://127.0.0.1:$HTTP_PORT/ext/bc/C/rpc"
|
||||
echo " X-Chain RPC: http://127.0.0.1:$HTTP_PORT/ext/bc/X"
|
||||
echo " P-Chain RPC: http://127.0.0.1:$HTTP_PORT/ext/bc/P"
|
||||
echo " Info API: http://127.0.0.1:$HTTP_PORT/ext/info"
|
||||
echo " C-Chain RPC: http://127.0.0.1:$HTTP_PORT/v1/bc/C/rpc"
|
||||
echo " X-Chain RPC: http://127.0.0.1:$HTTP_PORT/v1/bc/X"
|
||||
echo " P-Chain RPC: http://127.0.0.1:$HTTP_PORT/v1/bc/P"
|
||||
echo " Info API: http://127.0.0.1:$HTTP_PORT/v1/info"
|
||||
echo ""
|
||||
echo " Logs: tail -f $LOG_FILE"
|
||||
echo " Stop: kill $(cat "$PID_FILE")"
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if ! [[ "$0" =~ scripts/protobuf_codegen.sh ]]; then
|
||||
echo "must be run from repository root"
|
||||
exit 255
|
||||
fi
|
||||
|
||||
# the versions here should match those of the binaries installed in the nix dev shell
|
||||
|
||||
## ensure the correct version of "buf" is installed
|
||||
BUF_VERSION='1.47.2'
|
||||
if [[ $(buf --version | cut -f2 -d' ') != "${BUF_VERSION}" ]]; then
|
||||
echo "could not find buf ${BUF_VERSION}, is it installed + in PATH?"
|
||||
exit 255
|
||||
fi
|
||||
|
||||
## ensure the correct version of "protoc-gen-go" is installed
|
||||
PROTOC_GEN_GO_VERSION='v1.35.1'
|
||||
if [[ $(protoc-gen-go --version | cut -f2 -d' ') != "${PROTOC_GEN_GO_VERSION}" ]]; then
|
||||
echo "could not find protoc-gen-go ${PROTOC_GEN_GO_VERSION}, is it installed + in PATH?"
|
||||
exit 255
|
||||
fi
|
||||
|
||||
## ensure the correct version of "protoc-gen-go-grpc" is installed
|
||||
PROTOC_GEN_GO_GRPC_VERSION='1.3.0'
|
||||
if [[ $(protoc-gen-go-grpc --version | cut -f2 -d' ') != "${PROTOC_GEN_GO_GRPC_VERSION}" ]]; then
|
||||
echo "could not find protoc-gen-go-grpc ${PROTOC_GEN_GO_GRPC_VERSION}, is it installed + in PATH?"
|
||||
exit 255
|
||||
fi
|
||||
|
||||
BUF_MODULES=("proto" "connectproto")
|
||||
|
||||
REPO_ROOT=$PWD
|
||||
for BUF_MODULE in "${BUF_MODULES[@]}"; do
|
||||
TARGET=$REPO_ROOT/$BUF_MODULE
|
||||
if [ -n "${1:-}" ]; then
|
||||
TARGET="$1"
|
||||
fi
|
||||
|
||||
# move to buf module directory
|
||||
cd "$TARGET"
|
||||
|
||||
echo "Generating for buf module $BUF_MODULE"
|
||||
echo "Running protobuf fmt for..."
|
||||
buf format -w
|
||||
|
||||
echo "Running protobuf lint check..."
|
||||
if ! buf lint; then
|
||||
echo "ERROR: protobuf linter failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Re-generating protobuf..."
|
||||
if ! buf generate; then
|
||||
echo "ERROR: protobuf generation failed"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user