mirror of
https://github.com/luxfi/node.git
synced 2026-07-29 23:43:17 +00:00
Compare commits
167
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e90dd9342c | ||
|
|
303242ea5a | ||
|
|
1d84e03e5b | ||
|
|
3631ff5dc2 | ||
|
|
28e9fe0032 | ||
|
|
6258af7af9 | ||
|
|
f0733b749c | ||
|
|
5c164e84c7 | ||
|
|
2220065985 | ||
|
|
e9ab022ca7 | ||
|
|
7003d69384 | ||
|
|
5b0eca3270 | ||
|
|
c0d9ccacde | ||
|
|
3919991f48 | ||
|
|
0c573e6aa7 | ||
|
|
711d2519c2 | ||
|
|
9323caa1fc | ||
|
|
ff9faa3ef3 | ||
|
|
cae6f18ffb | ||
|
|
d48292b9dc | ||
|
|
c926953b60 | ||
|
|
7184449585 | ||
|
|
8e5bfb68dc | ||
|
|
51d4bd9520 | ||
|
|
2e18d7da38 | ||
|
|
b4a3ecdd75 | ||
|
|
f6639e661b | ||
|
|
130927f056 | ||
|
|
530b428159 | ||
|
|
f86909a928 | ||
|
|
3fb51e1995 | ||
|
|
cbd10105be | ||
|
|
eb510e0ca4 | ||
|
|
04902cdca5 | ||
|
|
5cbb1e4431 | ||
|
|
bcbb141378 | ||
|
|
bcd6c6b46d | ||
|
|
91e91218df | ||
|
|
000a0c84ff | ||
|
|
d12c3457af | ||
|
|
63d602b8ca | ||
|
|
ef0c581714 | ||
|
|
f852526092 | ||
|
|
18f54f9eb0 | ||
|
|
964be2fad8 | ||
|
|
ca7cdb4a77 | ||
|
|
99eddf0827 | ||
|
|
742c35485f | ||
|
|
e94b025395 | ||
|
|
ee81e8ea8f | ||
|
|
5a92bff2cd | ||
|
|
0aef65bc5d | ||
|
|
6fdc4ddfa7 | ||
|
|
708268aa71 | ||
|
|
2f644a14bc | ||
|
|
573346c6b8 | ||
|
|
0e8856758a | ||
|
|
ba8a1fc1a7 | ||
|
|
6780c4fdee | ||
|
|
129dfd7b46 | ||
|
|
b6d1bdca7c | ||
|
|
ffb627a51f | ||
|
|
b2c2376678 | ||
|
|
8bcc23efdb | ||
|
|
2663090827 | ||
|
|
65b7d6a1ae | ||
|
|
d15be7c524 | ||
|
|
d812ada7df | ||
|
|
2abb88530c | ||
|
|
dedb7eb806 | ||
|
|
a03785d922 | ||
|
|
ea066101a6 | ||
|
|
69258c94b0 | ||
|
|
23bd9575ea | ||
|
|
fab47e2b93 | ||
|
|
e0125e315d | ||
|
|
e27d954097 | ||
|
|
75da501683 | ||
|
|
9239065fdc | ||
|
|
95610b4b83 | ||
|
|
7a5f31da30 | ||
|
|
8d2ffbd4c9 | ||
|
|
263115933e | ||
|
|
b691a0d07e | ||
|
|
8c874943f9 | ||
|
|
01c40f969e | ||
|
|
8884c17f54 | ||
|
|
87e2ac3615 | ||
|
|
b6eae71825 | ||
|
|
c44df7e15c | ||
|
|
92c430ee12 | ||
|
|
3be49b29ec | ||
|
|
0486947913 | ||
|
|
105fd207c0 | ||
|
|
e1550aaea6 | ||
|
|
25adc9c75c | ||
|
|
09dffe5430 | ||
|
|
df173e4263 | ||
|
|
b561269ef8 | ||
|
|
20506a5950 | ||
|
|
0ebfa14b6d | ||
|
|
f97f552e87 | ||
|
|
2bde2c707f | ||
|
|
913acca108 | ||
|
|
50b27dbf94 | ||
|
|
2f9ef85652 | ||
|
|
9c42fe1126 | ||
|
|
e42b295617 | ||
|
|
d7312ad8a9 | ||
|
|
c827de99a7 | ||
|
|
24aaa598c5 | ||
|
|
3d5466eef4 | ||
|
|
b5a2a8d3db | ||
|
|
b5a4fc3f1f | ||
|
|
90738f212a | ||
|
|
50b4002e51 | ||
|
|
15ab4dbd47 | ||
|
|
bdf3c5d00e | ||
|
|
dadaad8a22 | ||
|
|
eea02da21a | ||
|
|
476c6dd12a | ||
|
|
35d48b9d1c | ||
|
|
be24ff49f0 | ||
|
|
8b8280c1e7 | ||
|
|
e0ebfac9d3 | ||
|
|
abc8a0856f | ||
|
|
2c49007406 | ||
|
|
06a47b11b5 | ||
|
|
0af39d7d40 | ||
|
|
182401b3be | ||
|
|
6357802b7d | ||
|
|
4dd4910bf3 | ||
|
|
766e8b2010 | ||
|
|
77010b6652 | ||
|
|
9759c2e253 | ||
|
|
05b139aadc | ||
|
|
d86c155eb1 | ||
|
|
77fff17b01 | ||
|
|
0568d68b80 | ||
|
|
9a6b7d2455 | ||
|
|
61b3e380e0 | ||
|
|
9c9e6b9e28 | ||
|
|
4c1529da2a | ||
|
|
4a345fe8e3 | ||
|
|
102023f249 | ||
|
|
aee5923e68 | ||
|
|
ea32845a7d | ||
|
|
3bb951e59e | ||
|
|
517acef79b | ||
|
|
cc856c020c | ||
|
|
a0d1a2bc31 | ||
|
|
9c95d6473a | ||
|
|
6f0cdee116 | ||
|
|
2060761d42 | ||
|
|
a4208faf95 | ||
|
|
1d94973509 | ||
|
|
009f00ba91 | ||
|
|
c26319db84 | ||
|
|
fd40807831 | ||
|
|
9b397125c1 | ||
|
|
e9c2d81ff5 | ||
|
|
df8af35979 | ||
|
|
1113675482 | ||
|
|
131e6a588c | ||
|
|
c790abb494 | ||
|
|
fcde96701d | ||
|
|
6e76c846fb |
@@ -18,7 +18,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build-x86_64-binaries-tarball:
|
||||
runs-on: [self-hosted, linux, amd64]
|
||||
runs-on: lux-build
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
rm -rf ${{ github.workspace }}/luxd-pkg
|
||||
|
||||
build-arm64-binaries-tarball:
|
||||
runs-on: [self-hosted, linux, amd64]
|
||||
runs-on: lux-build
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
@@ -21,17 +21,15 @@ on:
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
# Cross-compile both darwin/amd64 and darwin/arm64 from Linux. The build
|
||||
# uses CGO_ENABLED=0 + GOOS=darwin GOARCH=$arch — pure Go, no Darwin
|
||||
# toolchain required. We run on the self-hosted `lux-build` ARC pool
|
||||
# rather than `macos-13` (which has long GitHub-hosted queues) to keep
|
||||
# the release pipeline unblocked.
|
||||
# This workflow contains a single job called "build"
|
||||
# Cross-compile both darwin/amd64 and darwin/arm64 on macos-13.
|
||||
build-mac:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
goarch: [amd64, arm64]
|
||||
runs-on: [self-hosted, linux, amd64]
|
||||
# The type of runner that the job will run on (GH-hosted arm64 macos forbidden).
|
||||
runs-on: macos-13
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
@@ -65,10 +63,7 @@ jobs:
|
||||
run: |
|
||||
# CLI expects: node-macos-{arch}-{version}.zip containing build/luxd
|
||||
mkdir -p build
|
||||
if ! command -v zip >/dev/null 2>&1; then
|
||||
sudo apt-get update && sudo apt-get install -y zip
|
||||
fi
|
||||
zip "node-macos-${{ matrix.goarch }}-${TAG}.zip" build/luxd
|
||||
7z a "node-macos-${{ matrix.goarch }}-${TAG}.zip" build/luxd
|
||||
env:
|
||||
TAG: ${{ env.TAG }}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build-jammy-amd64-package:
|
||||
runs-on: [self-hosted, linux, amd64]
|
||||
runs-on: lux-build
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
rm -rf /tmp/luxd
|
||||
|
||||
build-focal-amd64-package:
|
||||
runs-on: [self-hosted, linux, amd64]
|
||||
runs-on: lux-build
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -18,7 +18,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build-jammy-arm64-package:
|
||||
runs-on: [self-hosted, linux, amd64]
|
||||
runs-on: lux-build
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
rm -rf /tmp/luxd
|
||||
|
||||
build-focal-arm64-package:
|
||||
runs-on: [self-hosted, linux, amd64]
|
||||
runs-on: lux-build
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -14,38 +14,13 @@ env:
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
# In-cluster ARC pool (lux-build autoscalingrunnerset in lux-k8s, org-scoped to
|
||||
# github.com/luxfi, amd64). GitHub-hosted ubuntu-latest is disabled for this org
|
||||
# (NO GITHUB BUILDERS) — that is why this job red-X'd while docker.yml (lux-build)
|
||||
# published the image. GoReleaser cross-compiles all GOOS/GOARCH from one linux
|
||||
# amd64 host with CGO_ENABLED=0, so a single amd64 runner is sufficient.
|
||||
runs-on: lux-build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: ./.github/actions/setup-go-for-project
|
||||
# GoReleaser shells out to `go build`, which must fetch private luxfi
|
||||
# modules across REPOS (container, crypto, database, proto, ...). The
|
||||
# repo-scoped github.token cannot read other private luxfi repos
|
||||
# (`could not read Password ... exit 128`); use the cross-org PAT that
|
||||
# docker.yml already relies on (org GH_TOKEN, else repo UNIVERSE_PAT).
|
||||
- id: pat
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
UNIVERSE_PAT: ${{ secrets.UNIVERSE_PAT }}
|
||||
shell: bash
|
||||
run: |
|
||||
tok="$GH_TOKEN"; src="GH_TOKEN"
|
||||
if [ -z "$tok" ]; then tok="$UNIVERSE_PAT"; src="UNIVERSE_PAT"; fi
|
||||
if [ -z "$tok" ]; then
|
||||
echo "::error::No GH_TOKEN or UNIVERSE_PAT secret set — go build cannot fetch private cross-repo luxfi modules"
|
||||
exit 1
|
||||
fi
|
||||
echo "Using secret: $src (length=${#tok})"
|
||||
echo "::add-mask::$tok"
|
||||
git config --global url."https://x-access-token:${tok}@github.com/".insteadOf "https://github.com/"
|
||||
- name: Run GoReleaser (release)
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
uses: goreleaser/goreleaser-action@v7
|
||||
|
||||
@@ -12,10 +12,9 @@ permissions:
|
||||
|
||||
jobs:
|
||||
build-amd64:
|
||||
# In-cluster ARC pool (lux-build autoscalingrunnerset in lux-k8s, amd64
|
||||
# DOKS nodes, DinD sidecar). Replaces the offline evo classic runner.
|
||||
# ARC matches on the scale-set name, NOT classic [self-hosted,linux,amd64]
|
||||
# labels — the org runner group + arcd repo allowlist enforce isolation.
|
||||
# Self-hosted `lux-build` ARC pool on DOKS hanzo-k8s (max 20 runners,
|
||||
# scales 0→N on demand). Org-isolated: luxfi workflows must use this
|
||||
# pool (never the hanzoai pools). amd64 only — DOKS has no arm64 yet.
|
||||
runs-on: lux-build
|
||||
outputs:
|
||||
digest: ${{ steps.build.outputs.digest }}
|
||||
@@ -109,7 +108,7 @@ jobs:
|
||||
notify-universe:
|
||||
needs: build-amd64
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
runs-on: lux-build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: peter-evans/repository-dispatch@v4
|
||||
with:
|
||||
|
||||
@@ -14,10 +14,7 @@ permissions:
|
||||
jobs:
|
||||
# Validate semantic version is < v2.0.0
|
||||
validate-version:
|
||||
# lux-build ARC pool (in-cluster, lux-k8s, org-scoped to github.com/luxfi). The org
|
||||
# disables GitHub-hosted runners (NO GITHUB BUILDERS); ubuntu-latest never gets a
|
||||
# runner, which red-X'd this job and cascaded skips to every platform build below.
|
||||
runs-on: lux-build
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.extract.outputs.version }}
|
||||
is_prerelease: ${{ steps.check.outputs.is_prerelease }}
|
||||
@@ -97,10 +94,7 @@ jobs:
|
||||
- build-linux-binaries
|
||||
- build-macos
|
||||
- build-windows
|
||||
# lux-build ARC pool (in-cluster, lux-k8s, org-scoped to github.com/luxfi). This job
|
||||
# only downloads artifacts + cuts the GitHub Release — no compile — but ubuntu-latest
|
||||
# is disabled for the org (NO GITHUB BUILDERS), so it must run on a self-hosted pool.
|
||||
runs-on: lux-build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
+6
-3
@@ -73,6 +73,9 @@ vendor
|
||||
|
||||
*.bak*
|
||||
|
||||
AGENTS.md
|
||||
CLAUDE.md
|
||||
GEMINI.md
|
||||
GROK.md
|
||||
QWEN.md
|
||||
.env
|
||||
@@ -84,10 +87,10 @@ genesis-gen
|
||||
/luxd
|
||||
evm-plugin-*
|
||||
|
||||
LLM.md
|
||||
QWEN.md
|
||||
.AGENTS.md
|
||||
GEMINI.md
|
||||
|
||||
# Built dev-tool binaries
|
||||
/cmd/gen_zoo_addr/gen_zoo_addr
|
||||
|
||||
# Local ceremony test binary (out-of-tree compile artifact)
|
||||
/ceremony
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ Thank you for your interest in contributing to Lux Node! This document provides
|
||||
|
||||
To start developing on Lux Node, you'll need a few things installed.
|
||||
|
||||
- Golang version >= 1.26.3
|
||||
- Golang version >= 1.23.9
|
||||
- gcc
|
||||
- g++
|
||||
|
||||
@@ -34,7 +34,7 @@ We are committed to fostering a welcoming and inclusive community. Please be res
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Go 1.26.3 or higher
|
||||
- Go 1.21.12 or higher
|
||||
- Git
|
||||
- Make
|
||||
- GCC/G++ compiler
|
||||
|
||||
+28
-229
@@ -1,8 +1,6 @@
|
||||
# The version is supplied as a build argument rather than hard-coded
|
||||
# to minimize the cost of version changes. Must be >= the `go` directive
|
||||
# in go.mod (1.26.4); the EVM plugin pulls luxfi/upgrade@v1.0.1 which
|
||||
# floors the toolchain at 1.26.4.
|
||||
ARG GO_VERSION=1.26.4
|
||||
# to minimize the cost of version changes.
|
||||
ARG GO_VERSION=1.26.1
|
||||
|
||||
# ============= Go Installation Stage ================
|
||||
FROM --platform=$BUILDPLATFORM debian:bookworm-slim AS go-installer
|
||||
@@ -38,15 +36,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Skip checksum verification for luxfi + hanzoai packages: both are cross-org
|
||||
# deps not registered in the public sum.golang.org / proxy (reading e.g.
|
||||
# hanzoai/vfs@v0.4.1's go.mod via the public sumdb 404s and fails the build).
|
||||
ENV GONOSUMCHECK=github.com/luxfi/*,github.com/hanzoai/*
|
||||
ENV GONOSUMDB=github.com/luxfi/*,github.com/hanzoai/*
|
||||
# Use Go proxy for most deps (gonum.org is flaky via direct), direct only for
|
||||
# the cross-org private modules.
|
||||
# Skip checksum verification for luxfi packages (tags may be rewritten)
|
||||
ENV GONOSUMCHECK=github.com/luxfi/*
|
||||
ENV GONOSUMDB=github.com/luxfi/*
|
||||
# Use Go proxy for most deps (gonum.org is flaky via direct), direct only for luxfi
|
||||
ENV GOPROXY=https://proxy.golang.org,direct
|
||||
ENV GONOPROXY=github.com/luxfi/*,github.com/hanzoai/*
|
||||
ENV GONOPROXY=github.com/luxfi/*
|
||||
ENV GOFLAGS="-mod=mod"
|
||||
|
||||
# Copy and download lux dependencies using go mod.
|
||||
@@ -66,8 +61,7 @@ RUN --mount=type=secret,id=ghtok,required=false \
|
||||
if [ -s /run/secrets/ghtok ]; then \
|
||||
git config --global url."https://x-access-token:$(cat /run/secrets/ghtok)@github.com/".insteadOf "https://github.com/"; \
|
||||
fi && \
|
||||
sed -i -E '/^github.com\/(luxfi|hanzoai)\//d' go.sum && \
|
||||
go mod download -x
|
||||
go mod download
|
||||
|
||||
# Copy the code into the container
|
||||
COPY . .
|
||||
@@ -98,26 +92,16 @@ RUN if [ "$TARGETPLATFORM" = "linux/arm64" ] && [ "$BUILDPLATFORM" != "linux/arm
|
||||
echo "export CC=gcc" > ./build_env.sh \
|
||||
; fi
|
||||
|
||||
# Fetch pre-built lux-accel (GPU crypto library). The release assets live in
|
||||
# the PRIVATE luxcpp/accel repo (resolves to lux-private/accel) — an
|
||||
# unauthenticated GitHub release-download 404s, so the fetch is authenticated
|
||||
# with the same `ghtok` BuildKit secret used for private go modules. It is
|
||||
# also best-effort (matches the cevm/lpm fetch contract below): the library is
|
||||
# ONLY linked at CGO_ENABLED=1, so a CGO_ENABLED=0 build (the canonical CI/devnet
|
||||
# build, pure-Go) does not need it and must not fail when it is unreachable.
|
||||
# Fetch pre-built lux-accel (GPU crypto library)
|
||||
ARG ACCEL_VERSION=v0.1.0
|
||||
RUN --mount=type=secret,id=ghtok,required=false \
|
||||
ARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) && \
|
||||
RUN ARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) && \
|
||||
if [ "$ARCH" = "amd64" ]; then ACCEL_ARCH="linux-x86_64"; else ACCEL_ARCH="linux-arm64"; fi && \
|
||||
mkdir -p /usr/local/include /usr/local/lib && \
|
||||
AUTH=""; [ -s /run/secrets/ghtok ] && AUTH="--header=Authorization: Bearer $(cat /run/secrets/ghtok)"; \
|
||||
( wget -q ${AUTH:+"$AUTH"} \
|
||||
"https://github.com/luxcpp/accel/releases/download/${ACCEL_VERSION}/lux-accel-${ACCEL_ARCH}.tar.gz" \
|
||||
-O /tmp/accel.tar.gz \
|
||||
&& tar -xzf /tmp/accel.tar.gz -C /usr/local \
|
||||
&& rm /tmp/accel.tar.gz \
|
||||
&& ldconfig 2>/dev/null \
|
||||
) || echo "WARN: lux-accel ${ACCEL_VERSION} fetch skipped (private/unreachable; GPU accel unused at CGO_ENABLED=0)"
|
||||
wget -q "https://github.com/luxcpp/accel/releases/download/${ACCEL_VERSION}/lux-accel-${ACCEL_ARCH}.tar.gz" \
|
||||
-O /tmp/accel.tar.gz && \
|
||||
tar -xzf /tmp/accel.tar.gz -C /usr/local && \
|
||||
rm /tmp/accel.tar.gz && \
|
||||
ldconfig 2>/dev/null || true
|
||||
|
||||
# Fetch pre-built luxcpp/cevm libs (libevm, libevm-gpu, libluxgpu,
|
||||
# libcevm_precompiles + go_bridge.h). When CGO_ENABLED=1 these libraries are
|
||||
@@ -149,11 +133,6 @@ ARG BUILD_SCRIPT=build.sh
|
||||
ARG LUXD_COMMIT=""
|
||||
ENV CGO_ENABLED=${CGO_ENABLED}
|
||||
RUN . ./build_env.sh && \
|
||||
# `COPY . .` above restored the committed go.sum (stale first-party hashes when
|
||||
# a luxfi/hanzoai module was re-tagged). Re-strip first-party lines so -mod=mod
|
||||
# re-records the CURRENT content hashes already in the module cache (from the
|
||||
# `go mod download` step). Without this, a re-tag => go.sum SECURITY ERROR.
|
||||
sed -i -E '/^github.com\/(luxfi|hanzoai)\//d' go.sum && \
|
||||
echo "{CC=$CC, TARGETPLATFORM=$TARGETPLATFORM, BUILDPLATFORM=$BUILDPLATFORM, CGO_ENABLED=${CGO_ENABLED}}" && \
|
||||
export GOARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) && \
|
||||
export LUXD_COMMIT="${LUXD_COMMIT}" && \
|
||||
@@ -168,114 +147,13 @@ RUN . ./build_env.sh && \
|
||||
# post-rename. Older evm tags (e.g. v0.8.40 → node v1.23.4) ship a
|
||||
# plugin that os.Getenv()'s the old key, mismatching the host that
|
||||
# sets the new key, and C-chain never bootstraps.
|
||||
#
|
||||
# v0.19.0 (Quasar Edition): EtnaTimestamp Go field + json tag renamed
|
||||
# to QuasarTimestamp/quasarTimestamp. Strict upgradeBytes decode via
|
||||
# json.NewDecoder(...).DisallowUnknownFields() — stale etnaTimestamp
|
||||
# in any deployed upgrade.json now fails parse loudly at boot rather
|
||||
# than silently disabling the fork.
|
||||
#
|
||||
# v0.19.1 bumps luxfi/precompile v0.5.27 → v0.5.35 (commit 794912f):
|
||||
# each of the 7 EIP-2537 bls12381 sub-configs (G1/G2 × {Add,Mul,MSM} +
|
||||
# Pairing) now returns its own ConfigKey, fixing a Key() collision
|
||||
# that forced #114 to drop bls12381 entries from mainnet upgrade.json.
|
||||
# Re-adding them is gated on this plugin version.
|
||||
#
|
||||
# v0.19.2 bumps luxfi/vm v1.1.5 → v1.1.6, which drops the obsolete
|
||||
# GetNetworkUpgrades() method on the VMContext interface. Required by
|
||||
# the Etna→Quasar rename in v0.19.0: vm v1.1.5 still expected
|
||||
# upgrade.Config to implement the old IsEtnaActivated() runtime
|
||||
# interface, which no longer exists.
|
||||
#
|
||||
# v0.19.3 closes the cascade: bumps vm v1.1.6 → v1.1.7 + runtime
|
||||
# v1.0.1 → v1.1.0. runtime v1.1.0 ripped the always-true
|
||||
# NetworkUpgrades interface (decomplect 9e6e597) and renamed
|
||||
# XAssetID → UTXOAssetID (refactor 034ec47). v1.1.6 anticipated the
|
||||
# rip in rpc/context.go but still pinned the old runtime, leaving
|
||||
# itself internally inconsistent. v1.1.7 pins the post-rip runtime.
|
||||
# MUST track node's go.mod luxfi/evm (the C-Chain EVM plugin = the native 0x9999
|
||||
# receipt/atomic surface). v1.99.31 = the native-atomic seam (precompile v0.5.51,
|
||||
# geth v1.17.12 CallIndex). Bump this with every evm release or the bundled
|
||||
# C-Chain plugin silently goes stale vs node's deps.
|
||||
#
|
||||
# v1.99.33 (precompile v0.5.53): 0x9999 DEX settlement activates at a SINGLE canonical
|
||||
# dated fork — extras.DexSettleActivationTime = 1766704800 (Dec 25 2025 00:00:00 UTC) —
|
||||
# with ZERO per-net config (no dexSettleConfig genesis/upgrade entry; one built-in fork,
|
||||
# identical on every net). At the fork it BOTH enables dispatch AND installs the standard
|
||||
# EXTCODESIZE marker (nonce=1 + code) into 0x9999 going forward, so eth_getCode(0x9999)
|
||||
# !=0x and a typed Solidity IPoolManager(0x9999).swap(...) passes the contract-existence
|
||||
# guard. The marker is installed FORWARD (never in historical genesis), so pre-Dec-25
|
||||
# history (the ~/work/lux/state RLP snapshot, replayed via admin.importChain) stays
|
||||
# byte-identical to canonical state — a pre-fork value transfer to 0x9999 hits a PLAIN
|
||||
# account, not the precompile. The D-Chain (dexvm) peer is resolved at RUNTIME via the
|
||||
# consensus-context "D" alias (contract.AtomicState.DChainID()) and the
|
||||
# protocolFeeController is the built-in DAO treasury. 0x9010 is REMOVED (not a registered
|
||||
# precompile, no dispatch, no forward); 0x9999 is the SOLE canonical DEX precompile. For a
|
||||
# fresh net whose genesis ts >= the fork, the marker is present from block 0.
|
||||
#
|
||||
# v1.99.34 (precompile v0.5.54): fixes a consensus-divergence on the relaunch/replay path.
|
||||
# The EVM dispatch gate (core.LuxPrecompileOverrider.PrecompileOverride) used to read the
|
||||
# process-global params.lastRulesContext (via GetRulesExtra(Rules{})), which is rewritten
|
||||
# last-writer-wins by every concurrent Rules() call (eth_call/estimateGas/worker). On a
|
||||
# live, RPC-serving post-fork node replaying a PRE-fork RLP block (admin.importChain), a
|
||||
# concurrent post-fork eth_call could overwrite the global timestamp between the verify
|
||||
# goroutine's NewEVM(pre-fork block) and its tx dispatch — making the pre-fork block see
|
||||
# 0x9999 ENABLED and dispatch SettleContract.Run during plain-account execution => state
|
||||
# divergence / consensus split. Fix: the dispatch gate now decides from the overrider's OWN
|
||||
# per-EVM fields (o.chainConfig + o.timestamp) via the pure params.GetExtrasRules, never the
|
||||
# global — so every replay of a block yields the same enabled set on every validator. Also:
|
||||
# the registry stateDBBridge.SubBalance fallback now FAILS CLOSED (panic → reverted call)
|
||||
# instead of returning a zero "previous balance" without debiting (silent native mint); and
|
||||
# the genesis-config builders (SetAllGenesisPrecompiles/AllGenesisPrecompiles) skip AlwaysOn
|
||||
# modules so 0x9999 can never get a timestamp-0 genesis config that bypasses the dated fork.
|
||||
# The money path (V4 swap ABI, marker install, two-phase atomic settle) is byte-for-byte
|
||||
# unchanged: ONLY the dispatch-path timestamp source, the SubBalance fail-mode, the genesis
|
||||
# builder guard, and stale 0x9010 comments changed.
|
||||
#
|
||||
# v1.99.37 (precompile v0.5.57): wires the 0x9999 ERC-20 Call surface to the DEX
|
||||
# settlement precompile (commit 2cf30e43d) and gates that Call surface to the DEX
|
||||
# settlement family 0x9999/0x9996 (commit 9579f2e34). Before this, a CALL into
|
||||
# 0x9999's ERC-20 settle path saw a nil PrecompileEnv (GetPrecompileEnv == nil) and
|
||||
# could not resolve the token-transfer Call seam — the two-phase atomic settle's
|
||||
# ERC-20 leg had no env to execute against. precompile v0.5.57 also adds the
|
||||
# CALL-only DELEGATECALL guard (commit feeaab5a0) so the settle surface is reachable
|
||||
# only via CALL (not DELEGATECALL, which would run it in the caller's context). Also
|
||||
# converges deps to latest patch within v1.x.x (threshold v1.9.9, crypto v1.19.21,
|
||||
# database v1.20.3, geth v1.17.12, warp v1.19.5, vm v1.2.5, api v1.0.15 — the
|
||||
# UTXOAssetID rename that fixed the LuxAssetID build break) and removes the dead
|
||||
# vendored dexConfig upgrade fixtures. The 0x9999 swap ABI + dated-fork activation
|
||||
# (DexSettleActivationTime = 1766704800) are unchanged from v1.99.34.
|
||||
#
|
||||
# v1.99.40 (precompile v0.5.59, chains v1.3.19, consensus v1.25.21): the permissionless
|
||||
# 0x9999 DEX value path lands end-to-end. precompile v0.5.58/59 = AssetResolver (real
|
||||
# on-chain canonical resolution, NO admin allowlist), one synchronous router/book/journal,
|
||||
# minOut on every route, no keeper/venue/live-ZAP/second-book; the env→Call ERC-20 vault
|
||||
# seam + in-state-vault resolution make a real ERC-20 settle. evm v1.99.39 = the
|
||||
# reprocess-bind fix: NewBlockChain binds the chain Runtime (networkID/C-Chain id) BEFORE
|
||||
# startup reprocess, so an unclean restart after a 0x9999 swap re-executes the committed
|
||||
# swap with the correct identity instead of (0, Empty) — previously that reverted the swap,
|
||||
# failed ValidateState, and BRICKED the node. Proven on-node: real swap → kill -9 →
|
||||
# clean reboot, state intact. v1.99.40 = v1.99.39 + deps to latest. consensus v1.25.21 =
|
||||
# stake-weighted alpha-of-K quorum finality + per-height single-finalize + epoch-bound certs.
|
||||
ARG EVM_VERSION=v1.99.40
|
||||
ARG EVM_VERSION=v0.18.14
|
||||
ARG EVM_VM_ID=mgj786NP7uDwBCcq6YwThhaN8FLyybkCa4zBWTQbNgmK6k9A6
|
||||
# the pinned evm go.mod may pin a dead luxfi/upgrade pseudo-version
|
||||
# (v1.0.1-0.20260603055252-f51810805436 — commit pruned from origin). Heal it to
|
||||
# the released upgrade v1.0.1 tag (semver-forward, not a downgrade). Then strip
|
||||
# first-party go.sum lines so -mod=mod re-records current content hashes for the
|
||||
# re-published luxfi modules at their pinned versions (integrity repair, no version drift).
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
mkdir -p /luxd/build/plugins && \
|
||||
git clone --depth 1 --branch ${EVM_VERSION} https://github.com/luxfi/evm.git /tmp/evm && \
|
||||
cd /tmp/evm && \
|
||||
. /build/build_env.sh && \
|
||||
go mod edit -require=github.com/luxfi/upgrade@v1.0.1 && \
|
||||
# evm v1.99.40 pins luxfi/chains v1.3.19, whose dexvm/registry was an incomplete
|
||||
# refactor (forbidden.go deleted -> AssertNoForbiddenAssetRefs/looksLikeASCIITickerID/
|
||||
# toHex/fromHex undefined => won't compile). Force v1.3.21 (forbidden.go restored),
|
||||
# matching node's go.mod and the chain-VM plugin stage (CHAINS_REF) below.
|
||||
go mod edit -require=github.com/luxfi/chains@v1.3.21 && \
|
||||
find /tmp/evm -name go.sum -exec sed -i -E '/^github.com\/(luxfi|hanzoai)\//d' {} + && \
|
||||
GOARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) \
|
||||
CGO_ENABLED=0 GOFLAGS=-mod=mod \
|
||||
go build -ldflags="-s -w" -o /luxd/build/plugins/${EVM_VM_ID} ./plugin && \
|
||||
@@ -298,25 +176,18 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
# thresholdvm -> tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t
|
||||
# zkvm -> vv3qPfyTVXZ5ArRZA9Jh4hbYDTBe43f7sgQg4CHfNg1rnnvX9
|
||||
|
||||
# MUST track node's go.mod luxfi/chains (the D-Chain dexvm + 10 VM plugins).
|
||||
# v1.3.14 = the native-atomic seam (rail-bound D->C atomic, LP committed-liquidity).
|
||||
# Bump with every chains release or the bundled VM plugins go stale vs node's deps.
|
||||
# v1.3.21 == node go.mod's luxfi/chains pin (the finality-complete go-live); keeps the
|
||||
# 10 baked VM plugins (incl. the FATAL-gated bridgevm) in lockstep with the host node.
|
||||
ARG CHAINS_REF=v1.3.21
|
||||
ARG CHAINS_REF=main
|
||||
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' {} +
|
||||
git clone --depth 1 --branch ${CHAINS_REF} https://github.com/luxfi/chains.git /tmp/chains
|
||||
|
||||
# Each VM is an independent Go module under /tmp/chains/<vm>/.
|
||||
# Build each plugin binary and place it at /luxd/build/plugins/<cb58-vm-id>.
|
||||
#
|
||||
# The recursive go.sum strip above lets -mod=mod re-record current content hashes
|
||||
# for re-published first-party modules at their pinned versions (integrity repair).
|
||||
# At a tagged CHAINS_REF (v1.3.11) the sibling go.mod replace directives that
|
||||
# affect `main` are absent, so every VM module builds in isolation. The bridgevm
|
||||
# plugin (B-Chain) MUST build — it blank-imports crypto/threshold/bls to register
|
||||
# the BLS threshold scheme; a miss reintroduces the v1.30.16 B-Chain init failure.
|
||||
# NB(2026-05-19): luxfi/chains main has unresolved sibling go.mod replace
|
||||
# directives (luxfi/{evm,precompile,threshold} => ../*) that break in any
|
||||
# isolated build context. Each chain plugin is therefore built best-effort;
|
||||
# production deployments pull plugins from S3 (`pluginSource.bucket`) at
|
||||
# runtime per LuxNetwork CR, so an embedded plugin miss is non-fatal.
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
. /build/build_env.sh && \
|
||||
export GOARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) && \
|
||||
@@ -324,7 +195,9 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
( cd /tmp/chains/aivm && CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
|
||||
-o /luxd/build/plugins/juFxSrbCM4wszxddKepj1GWwmrn9YgN1g4n3VUWPpRo9JjERA ./cmd/plugin ) || echo "WARN: aivm plugin build skipped" ; \
|
||||
( cd /tmp/chains/bridgevm && CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
|
||||
-o /luxd/build/plugins/kMhHABHM8j4bH94MCc4rsTNdo5E9En37MMyiujk4WdNxgXFsY ./cmd/plugin ) ; \
|
||||
-o /luxd/build/plugins/kMhHABHM8j4bH94MCc4rsTNdo5E9En37MMyiujk4WdNxgXFsY ./cmd/plugin ) || echo "WARN: bridgevm plugin build skipped" ; \
|
||||
( cd /tmp/chains/dexvm && CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
|
||||
-o /luxd/build/plugins/mDVT5EWMumBp3LCqvKwuyZQeY1VXr1jvjGNAt8nL4UFiXvqXr ./cmd/plugin ) || echo "WARN: dexvm plugin build skipped" ; \
|
||||
( cd /tmp/chains/graphvm && CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
|
||||
-o /luxd/build/plugins/nZQm4Dmg1rjX18rb8maL9gamYyXPf1xCvF7ymWzxp6a1nSQTt ./cmd/plugin ) || echo "WARN: graphvm plugin build skipped" ; \
|
||||
( cd /tmp/chains/identityvm && CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
|
||||
@@ -342,56 +215,8 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
( cd /tmp/chains/zkvm && CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
|
||||
-o /luxd/build/plugins/vv3qPfyTVXZ5ArRZA9Jh4hbYDTBe43f7sgQg4CHfNg1rnnvX9 ./cmd/plugin ) || echo "WARN: zkvm plugin build skipped" ; \
|
||||
( chmod +x /luxd/build/plugins/* 2>/dev/null || true ) && \
|
||||
test -s /luxd/build/plugins/kMhHABHM8j4bH94MCc4rsTNdo5E9En37MMyiujk4WdNxgXFsY \
|
||||
|| { echo "FATAL: bridgevm (B-Chain) plugin missing — the v1.30.16 regression would recur"; exit 1; } && \
|
||||
rm -rf /tmp/chains
|
||||
|
||||
# ============= Native D-Chain DEX VM Plugin Stage ================
|
||||
# The D-Chain (dexvm slot, vmID mDVT5EWMumBp3LCqvKwuyZQeY1VXr1jvjGNAt8nL4UFiXvqXr)
|
||||
# is the NATIVE consensus matcher VM: github.com/luxfi/dex/pkg/dchain implements
|
||||
# block.ChainVM and runs the lx.OrderBook matcher INSIDE luxd consensus
|
||||
# (Block.Verify against a versiondb overlay; Block.Accept commits) — the trade IS
|
||||
# the D-Chain state transition, sequenced by luxd's multi-validator engine. This
|
||||
# REPLACES the former chains/dexvm proxy (which relayed clob_* over ZAP to a
|
||||
# standalone dchain-venue): there is no DexZapEndpoint and no standalone venue in
|
||||
# the trading path. cmd/dchain wraps the VM in the SAME rpc.Serve plugin harness
|
||||
# luxfi/evm boots through, and is pure-Go (CGO=0) — the optional GPU AMM
|
||||
# accelerator in pkg/lx is a separate concern gated by its own cuda/metal tags and
|
||||
# is NOT linked here. v1.5.10 is the first tag whose cmd/dchain builds CGO=0
|
||||
# (drops the phantom dchain+cgo gate); v1.5.11 wires CLOB order ingestion over the
|
||||
# node HTTP router (VM.CreateHandlers -> /ext/bc/D/dex/<method>, pkg/dchain/ingest.go)
|
||||
# so an order POSTed to the node flows submitTx -> mempool -> consensus -> Verify
|
||||
# (match) -> Accept; v1.5.12 persists the head block so the VM survives a restart
|
||||
# once advanced past genesis (GetBlock(lastAccepted) no longer ErrNotFound);
|
||||
# v1.5.13 indexes processing blocks so the plugin transport's ID-only Accept
|
||||
# (GetBlock(builtID)) resolves the just-built block — without it the engine's
|
||||
# self-finalize Accept is a silent no-op and the clob submitTx waiter hangs (no
|
||||
# D-Chain blocks). v1.5.14 consumes luxfi/database v1.20.4, which fixes prefixdb
|
||||
# returning ZERO rows for a nil-start prefix scan over a prefixdb-wrapped chain
|
||||
# DB (every plugin VM via vm/rpc) — that stranded the native D-Chain order book
|
||||
# (rebuildBookFromDB folded empty -> 0 fills despite committed asks). v1.5.15 adds
|
||||
# the committed-state READ surface (clob_get_trades/orders/markets/book over
|
||||
# /ext/bc/D/dex/<method>, pkg/dchain/read.go): read-only JSON of the durable trade
|
||||
# log / resting book / markets, served beside the writes with ZERO consensus
|
||||
# impact. Needed to VERIFY a fill replicated identically across validators (query
|
||||
# every node, diff the trade rows + head root) and to feed markets-display (native
|
||||
# fills are trade: rows). Bump with every dex release that changes the VM, like
|
||||
# CHAINS_REF for the other 10 VMs.
|
||||
ARG DEX_REF=v1.5.15
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
git clone --depth 1 --branch ${DEX_REF} https://github.com/luxfi/dex.git /tmp/dex && \
|
||||
find /tmp/dex -name go.sum -exec sed -i -E '/^github.com\/(luxfi|hanzoai)\//d' {} + && \
|
||||
cd /tmp/dex && \
|
||||
. /build/build_env.sh && \
|
||||
GOARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) \
|
||||
CGO_ENABLED=0 GOFLAGS=-mod=mod \
|
||||
go build -ldflags="-s -w" \
|
||||
-o /luxd/build/plugins/mDVT5EWMumBp3LCqvKwuyZQeY1VXr1jvjGNAt8nL4UFiXvqXr ./cmd/dchain && \
|
||||
chmod +x /luxd/build/plugins/mDVT5EWMumBp3LCqvKwuyZQeY1VXr1jvjGNAt8nL4UFiXvqXr && \
|
||||
test -s /luxd/build/plugins/mDVT5EWMumBp3LCqvKwuyZQeY1VXr1jvjGNAt8nL4UFiXvqXr \
|
||||
|| { echo "FATAL: native D-Chain (dexvm) plugin missing — D-Chain cannot start"; exit 1; } && \
|
||||
rm -rf /tmp/dex
|
||||
|
||||
# lpm (Lux Plugin Manager) -- optional, skip if build fails
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
--mount=type=cache,target=/root/go/pkg/mod \
|
||||
@@ -419,34 +244,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
# Pure Go fallbacks are used when the library is absent.
|
||||
RUN ldconfig 2>/dev/null || true
|
||||
|
||||
# Maintain compatibility with previous images.
|
||||
# In the builder stage /luxd/build contains ONLY plugins/ (the luxd + lpm binaries
|
||||
# live at /build/build and are COPYed separately below). The plugin set (~192MB of
|
||||
# 12 VM plugins) is split across several COPY layers so each blob stays well under
|
||||
# ~100MB: a single monolithic plugins layer cannot reliably complete its registry
|
||||
# blob upload over a contended uplink, whereas sub-100MB layers push reliably (and
|
||||
# resume independently by digest).
|
||||
# plugin group 1
|
||||
COPY --from=builder \
|
||||
/luxd/build/plugins/mgj786NP7uDwBCcq6YwThhaN8FLyybkCa4zBWTQbNgmK6k9A6 \
|
||||
/luxd/build/plugins/juFxSrbCM4wszxddKepj1GWwmrn9YgN1g4n3VUWPpRo9JjERA \
|
||||
/luxd/build/plugins/kMhHABHM8j4bH94MCc4rsTNdo5E9En37MMyiujk4WdNxgXFsY \
|
||||
/luxd/build/plugins/
|
||||
# plugin group 2
|
||||
COPY --from=builder \
|
||||
/luxd/build/plugins/mDVT5EWMumBp3LCqvKwuyZQeY1VXr1jvjGNAt8nL4UFiXvqXr \
|
||||
/luxd/build/plugins/nZQm4Dmg1rjX18rb8maL9gamYyXPf1xCvF7ymWzxp6a1nSQTt \
|
||||
/luxd/build/plugins/oR6tnZHezwogyf9fRnomNXC9ojwCEBAU6jdUzpgy2PB1tD7fM \
|
||||
/luxd/build/plugins/pJJCSV7hHYVY6TUZwR8qUPAfuhX8JLb2C1AzNSezrYNbgau8M \
|
||||
/luxd/build/plugins/
|
||||
# plugin group 3
|
||||
COPY --from=builder \
|
||||
/luxd/build/plugins/r5m1ujrmXxVcQetG3CQfuDLHp2RHKh6vCDaFgBRQfUcTZh7eS \
|
||||
/luxd/build/plugins/ry9Sg8rZdT26iEKvJDmC2wkESs4SDKgZEhk5BgLSwg1EpcNug \
|
||||
/luxd/build/plugins/sP6dLqrrBR9w3soP18fbJ3YzZecZdD7DDdfH2cFhhLq7Hy9bz \
|
||||
/luxd/build/plugins/tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t \
|
||||
/luxd/build/plugins/vv3qPfyTVXZ5ArRZA9Jh4hbYDTBe43f7sgQg4CHfNg1rnnvX9 \
|
||||
/luxd/build/plugins/
|
||||
# Maintain compatibility with previous images
|
||||
COPY --from=builder /luxd/build /luxd/build
|
||||
WORKDIR /luxd/build
|
||||
|
||||
# Copy the executables into the container
|
||||
|
||||
+694
@@ -0,0 +1,694 @@
|
||||
# Lux Network -- Development Timeline
|
||||
|
||||
> Comprehensive history of development across Hanzo AI, Lux Network, and Zoo Labs Foundation.
|
||||
> All dates sourced from `git log` across 445 repositories. Fork provenance noted where applicable.
|
||||
|
||||
**Generated**: 2026-04-07 from live git history
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Metric | Count |
|
||||
|--------|-------|
|
||||
| Total repositories | 445 (Hanzo 209, Lux 179, Zoo 57) |
|
||||
| Total commits | 1,103,364 (Hanzo 852,218 / Lux 175,459 / Zoo 75,687) |
|
||||
| Research papers (LaTeX) | 329 (Hanzo 152, Lux 136, Zoo 41) |
|
||||
| Formal proofs (Lean4) | 13,160 files (Lux 6,851 / Hanzo 6,309) |
|
||||
| TLA+ specifications | 4 |
|
||||
| Tamarin protocol proofs | 2 |
|
||||
| Halmos symbolic tests | 10 |
|
||||
| Security audits | 23 reports |
|
||||
| Governance proposals | 1,735 (LIPs 848, HIPs 784, ZIPs 103) |
|
||||
| Patent applications | 2 portfolios (Hanzo, Zoo) |
|
||||
| Years of continuous development | 12 (2014--2026) |
|
||||
|
||||
### Key Technologies (Original Work)
|
||||
|
||||
- **Quasar Consensus** -- Multi-metric BFT with FPC, Wave protocol, pipelined block production
|
||||
- **Corona** -- Post-quantum signature scheme (ML-DSA + FROST hybrid)
|
||||
- **LuxFHE** -- Fully homomorphic encryption engine with Go bindings, NTT SIMD acceleration
|
||||
- **Lattice Cryptography** -- ML-KEM (FIPS 203), constant-time CBD sampler, CKKS/BFV schemes
|
||||
- **MPC Engine** -- CGGMP21 + FROST threshold signing, WebAuthn integration
|
||||
- **Jin Architecture** -- Multimodal AI (saccade JEPA, vision-language-audio)
|
||||
- **Zen Model Family** -- Qwen3+ fine-tuning, refusal removal, agentic datasets
|
||||
- **Hanzo Candle** -- Rust ML inference framework
|
||||
- **GPU EVM** -- CUDA-accelerated opcode dispatch, GPU ecrecover, GPU state hashing
|
||||
- **FHE Coprocessor** -- Encrypted smart contract execution
|
||||
|
||||
---
|
||||
|
||||
## Founder
|
||||
|
||||
Zach Kelling (zeekay) -- computer scientist, cryptographer, AI/ML researcher, musician, composer, architect, engineer, mathematician.
|
||||
|
||||
- **1983**: Born
|
||||
- **1998**: Enrolled in university for Computer Science at age 15
|
||||
- **Early 2000s**: Digidesign (Pro Tools) -- audio engineering, DSP, signal processing. Music composition and production.
|
||||
- **2000s--2010s**: Software engineering across distributed systems, infrastructure, and early machine learning. Artist, writer, composer, architect, mathematician.
|
||||
- **2008**: First open source contributions
|
||||
- **2011**: GitHub activity begins (github.com/zeekay) -- Python, Vim, shell frameworks, distributed systems
|
||||
- **2014**: Open-source AI/ML and commerce tooling -- the precursor work to Hanzo AI
|
||||
|
||||
Today: **1,239+ public repositories** across github.com/zeekay (547), github.com/hanzoai (366), github.com/luxfi (305), and additional orgs. 15+ years of continuous open source contribution.
|
||||
|
||||
Everything built has been open source, permissively licensed, and given to the public for free. This is not a commercial play -- it is a contribution to humanity's infrastructure.
|
||||
|
||||
---
|
||||
|
||||
## 2014--2016: Foundations
|
||||
|
||||
Early open-source work in commerce, automation, infrastructure, and AI/ML tooling. These repositories represent the precursor work to Hanzo AI.
|
||||
|
||||
### Original Hanzo Repositories
|
||||
|
||||
| Repository | First Commit | Description |
|
||||
|------------|-------------|-------------|
|
||||
| `hanzo/autogui` | 2014-07-17 | GUI automation framework |
|
||||
| `hanzo/classic` | 2014-09-29 | E-commerce platform (original, 7,395 commits) |
|
||||
| `hanzo/commerce` | 2014-09-29 | Commerce engine (original, 7,636 commits) |
|
||||
| `hanzo/s3-cli` | 2015-01-14 | S3-compatible object storage CLI |
|
||||
| `hanzo/openapi` | 2016-01-14 | API specification and documentation |
|
||||
| `hanzo/tasks` | 2016-10-24 | Distributed task execution engine |
|
||||
|
||||
### Forked Infrastructure (upstream dates precede Hanzo)
|
||||
|
||||
These repositories were forked from established open-source projects. The earliest commit dates reflect upstream history, not Hanzo origination.
|
||||
|
||||
| Repository | Upstream | Upstream First Commit |
|
||||
|------------|----------|----------------------|
|
||||
| `hanzo/postgres` / `hanzo/sql` | postgres/postgres | 1996-07-09 |
|
||||
| `hanzo/datastore` | ClickHouse/ClickHouse | 2008-12-01 |
|
||||
| `hanzo/kv` | valkey-io/valkey | 2009-03-22 |
|
||||
| `hanzo/redis` | redis/redis | 2009-03-22 |
|
||||
| `hanzo/kv-go` | redis/go-redis | 2012-07-25 |
|
||||
| `hanzo/pubsub-go` | nats-io/nats.go | 2012-08-15 |
|
||||
| `hanzo/pubsub` | nats-io/nats-server | 2012-10-29 |
|
||||
| `hanzo/storage` | minio/minio | 2014-10-30 |
|
||||
| `hanzo/ingress` | (custom proxy, original) | 2015-08-28 |
|
||||
| `hanzo/dns` | coredns/coredns | 2016-03-18 |
|
||||
| `hanzo/golang-migrate` | golang-migrate/migrate | 2014-08-11 |
|
||||
| `hanzo/dbx` | pocketbase/dbx | 2015-12-10 |
|
||||
|
||||
### Lux Precursor Forks
|
||||
|
||||
| Repository | Upstream | Upstream First Commit | Notes |
|
||||
|------------|----------|----------------------|-------|
|
||||
| `lux/coreth` / `lux/geth` | go-ethereum | 2013-12-26 | EVM fork, Lux-specific work begins ~2022 |
|
||||
| `lux/czmq` | (ZeroMQ C bindings) | 2014-09-05 | Messaging infrastructure |
|
||||
|
||||
### Commit Activity
|
||||
|
||||
| Year | Hanzo | Lux | Zoo |
|
||||
|------|-------|-----|-----|
|
||||
| 2014 | 14,547 | 5,405 | -- |
|
||||
| 2015 | 23,098 | 9,028 | -- |
|
||||
| 2016 | 17,989 | 2,681 | -- |
|
||||
|
||||
---
|
||||
|
||||
## 2017--2018: Hanzo AI Founded (Techstars '17)
|
||||
|
||||
Hanzo AI is accepted into Techstars 2017. Focus on AI-powered commerce, analytics, and infrastructure services.
|
||||
|
||||
### New Hanzo Repositories
|
||||
|
||||
| Repository | First Commit | Description |
|
||||
|------------|-------------|-------------|
|
||||
| `hanzo/datastore-go` | 2017-01-11 | Go client for analytics datastore |
|
||||
| `hanzo/documentdb-go` | 2017-01-25 | Document database Go driver |
|
||||
| `hanzo/docker` | 2017-07-18 | Container orchestration configs |
|
||||
| `hanzo/krakend` | 2017-12-03 | API gateway (KrakenD-based) |
|
||||
| `hanzo/search` | 2018-04-22 | Search engine (13,728 commits) |
|
||||
| `hanzo/telemetry` | 2018-06-05 | Observability platform (8,002 commits) |
|
||||
| `hanzo/rrweb` | 2018-09-30 | Session recording/replay |
|
||||
|
||||
### Lux Precursor Work
|
||||
|
||||
| Repository | First Commit | Description |
|
||||
|------------|-------------|-------------|
|
||||
| `lux/zapdb` | 2017-01-26 | Key-value store (fork of Badger) |
|
||||
| `lux/hid` | 2017-02-17 | Hardware device interface |
|
||||
| `lux/onnx` | 2017-09-06 | Open Neural Network Exchange |
|
||||
| `lux/safe` | 2017-09-27 | Multisig wallet (fork of Gnosis Safe) |
|
||||
| `lux/explorer` | 2018-01-16 | Block explorer (replaced by luxfi/explorer) |
|
||||
| `lux/zmq` | 2018-04-13 | ZeroMQ Go bindings |
|
||||
|
||||
### Zoo Precursor
|
||||
|
||||
| Repository | First Commit | Description |
|
||||
|------------|-------------|-------------|
|
||||
| `zoo/explorer` | 2018-01-16 | Block explorer (shared with Lux) |
|
||||
|
||||
### Commit Activity
|
||||
|
||||
| Year | Hanzo | Lux | Zoo |
|
||||
|------|-------|-----|-----|
|
||||
| 2017 | 20,219 | 3,854 | -- |
|
||||
| 2018 | 24,508 | 7,427 | 3,009 |
|
||||
|
||||
---
|
||||
|
||||
## 2019--2020: Lux Network Founded
|
||||
|
||||
Lux Network development begins in late 2019. Core blockchain node (`luxd`) launches March 2020. JavaScript SDK, wallet, and DeFi primitives follow.
|
||||
|
||||
### Lux Core Chain
|
||||
|
||||
| Repository | First Commit | Description |
|
||||
|------------|-------------|-------------|
|
||||
| `lux/assets` | 2019-08-09 | Token asset registry |
|
||||
| `lux/lattice` | 2019-08-12 | Lattice-based cryptography (CKKS, BFV, BGV schemes) |
|
||||
| `lux/cex` | 2019-08-16 | Exchange frontend |
|
||||
| `lux/exchange-sdk` | 2019-11-08 | Exchange SDK |
|
||||
| `lux/js` | 2020-01-21 | JavaScript SDK (initial pre-release) |
|
||||
| `lux/node` | 2020-03-10 | Core blockchain node -- 11,623 commits |
|
||||
| `lux/trace` | 2020-03-10 | Transaction tracing |
|
||||
| `lux/wwallet` | 2020-07-21 | Web wallet |
|
||||
| `lux/build` | 2020-11-04 | Build and release tooling |
|
||||
|
||||
### Hanzo Infrastructure Expansion
|
||||
|
||||
| Repository | First Commit | Description |
|
||||
|------------|-------------|-------------|
|
||||
| `hanzo/telemetry-go` | 2019-05-16 | Go telemetry client |
|
||||
| `hanzo/search-go` | 2019-12-08 | Go search client |
|
||||
| `hanzo/insights` | 2020-01-23 | Product analytics (35,710 commits) |
|
||||
| `hanzo/posthog-python` | 2020-02-09 | Python analytics SDK |
|
||||
| `hanzo/insights-node` | 2020-02-19 | Node.js analytics SDK |
|
||||
| `hanzo/insights-go` | 2020-02-27 | Go analytics SDK |
|
||||
| `hanzo/storage-console` | 2020-04-01 | Object storage management UI |
|
||||
| `hanzo/vector` | 2020-05-30 | Log aggregation pipeline |
|
||||
| `hanzo/analytics` | 2020-07-17 | Analytics engine (5,662 commits) |
|
||||
| `hanzo/ingress-parser` | 2020-08-15 | Ingress log parser |
|
||||
| `hanzo/livekit` | 2020-09-29 | Real-time audio/video |
|
||||
| `hanzo/iam` | 2020-10-20 | Identity and access management (3,746 commits) |
|
||||
|
||||
### Commit Activity
|
||||
|
||||
| Year | Hanzo | Lux | Zoo |
|
||||
|------|-------|-----|-----|
|
||||
| 2019 | 30,747 | 10,229 | 4,467 |
|
||||
| 2020 | 46,845 | 18,333 | 1,151 |
|
||||
|
||||
---
|
||||
|
||||
## 2021--2022: Expanding the Stack
|
||||
|
||||
Wallet, CLI, EVM, DeFi, threshold cryptography, and the first key management systems.
|
||||
|
||||
### Lux Ecosystem Growth
|
||||
|
||||
| Repository | First Commit | Description |
|
||||
|------------|-------------|-------------|
|
||||
| `lux/threshold` | 2021-02-16 | Threshold ECDSA (tECDSA) library |
|
||||
| `lux/erc20-go` | 2021-05-21 | ERC-20 Go bindings |
|
||||
| `lux/netrunner` | 2021-10-22 | Network testing framework (2,384 commits) |
|
||||
| `lux/evm` | 2021-12-15 | Subnet EVM (1,632 commits) |
|
||||
| `lux/devops` / `lux/lux-ops` | 2022-01-28 | Infrastructure automation |
|
||||
| `lux/ledger` | 2022-03-14 | Ledger hardware wallet integration |
|
||||
| `lux/lpm` | 2022-03-28 | Lux Plugin Manager |
|
||||
| `lux/plugins-core` | 2022-03-29 | Core VM plugins |
|
||||
| `lux/standard` | 2022-04-19 | Token standards |
|
||||
| `lux/cli` | 2022-04-23 | Command-line interface (2,153 commits) |
|
||||
| `lux/faucet` | 2022-05-12 | Testnet faucet |
|
||||
| `lux/netrunner-sdk` | 2022-05-13 | Network runner SDK |
|
||||
| `lux/explorer-rs` | 2022-05-20 | Rust block explorer |
|
||||
| `lux/market` / `lux/marketplace` | 2022-05-31 | NFT marketplace |
|
||||
| `lux/explore` | 2022-05-31 | Block explorer frontend |
|
||||
| `lux/monitoring` | 2022-06-02 | Network monitoring |
|
||||
| `lux/finance` | 2022-08-04 | DeFi protocols |
|
||||
| `lux/teleport` | 2022-09-13 | Cross-chain teleport bridge |
|
||||
| `lux/kms` | 2022-11-17 | Key management system (14,395 commits) |
|
||||
|
||||
### Hanzo Platform Build-Out
|
||||
|
||||
| Repository | First Commit | Description |
|
||||
|------------|-------------|-------------|
|
||||
| `hanzo/o11y` | 2021-01-03 | Observability stack |
|
||||
| `hanzo/sql-vector` | 2021-04-20 | Vector search in PostgreSQL |
|
||||
| `hanzo/insights-rs` | 2021-04-27 | Rust analytics SDK |
|
||||
| `hanzo/treasury` | 2021-06-11 | Treasury management |
|
||||
| `hanzo/team` | 2021-08-02 | Team management |
|
||||
| `hanzo/docdb` | 2021-10-31 | Document database (FerretDB-based) |
|
||||
| `hanzo/cloud` | 2022-03-31 | Cloud platform |
|
||||
| `hanzo/faucet` | 2022-05-12 | Token faucet |
|
||||
| `hanzo/mds` | 2022-05-17 | Metadata service |
|
||||
| `hanzo/otel-collector` | 2022-06-11 | OpenTelemetry collector |
|
||||
| `hanzo/vector-go` | 2022-06-24 | Go vector client |
|
||||
| `hanzo/base` | 2022-07-07 | Application backend framework (2,287 commits) |
|
||||
| `hanzo/evm` | 2022-09-19 | EVM utilities |
|
||||
| `hanzo/chat` | 2022-10-20 | Real-time chat |
|
||||
| `hanzo/sign` | 2022-11-14 | Document e-signing |
|
||||
| `hanzo/payments` | 2022-11-16 | Payment processing |
|
||||
| `hanzo/kms` | 2022-11-17 | Secret management (19,820 commits) |
|
||||
|
||||
### Zoo Ecosystem Begins
|
||||
|
||||
| Repository | First Commit | Description |
|
||||
|------------|-------------|-------------|
|
||||
| `zoo/solidity` | 2021-01-09 | Smart contract library |
|
||||
| `zoo/hardhat` | 2021-02-15 | Development framework |
|
||||
| `zoo/node` | 2021-06-15 | Zoo blockchain node |
|
||||
| `zoo/zoo-test` / `zoo/zoo-v4` / `zoo/zoo2` / `zoo/zoo3` | 2021-07-10 | Iterative protocol versions |
|
||||
| `zoo/zoogov-app` | 2022-03-03 | Governance application |
|
||||
| `zoo/zdk` | 2022-03-22 | Zoo Development Kit |
|
||||
| `zoo/explorer-app` | 2022-05-31 | Explorer frontend |
|
||||
| `zoo/CGI_Animation` | 2022-12-15 | AI-generated media |
|
||||
|
||||
### Commit Activity
|
||||
|
||||
| Year | Hanzo | Lux | Zoo |
|
||||
|------|-------|-----|-----|
|
||||
| 2021 | 58,199 | 27,811 | 8,923 |
|
||||
| 2022 | 59,535 | 20,333 | 10,029 |
|
||||
|
||||
---
|
||||
|
||||
## 2023: Post-Quantum + MPC + AI Agents
|
||||
|
||||
Major cryptographic research: threshold signing, MPC engines, lattice crypto. AI work accelerates with Jin architecture, ML frameworks, and computer-use agents.
|
||||
|
||||
### Lux Cryptography and Protocol
|
||||
|
||||
| Repository | First Commit | Description |
|
||||
|------------|-------------|-------------|
|
||||
| `lux/sdk` | 2023-02-19 | Unified SDK (225 commits) |
|
||||
| `lux/markets` | 2023-06-24 | DeFi market infrastructure |
|
||||
| `lux/web` | 2023-10-13 | Lux Network website |
|
||||
| `lux/wallet` | 2023-10-16 | Production wallet (1,203 commits) |
|
||||
| `lux/mpc` | 2023-11-03 | MPC engine -- CGGMP21 + FROST (388 commits) |
|
||||
| `lux/audits` | 2023-12-28 | Security audit reports (23 reports) |
|
||||
| `lux/bridge` | 2023-12-30 | Cross-chain bridge (1,919 commits) |
|
||||
|
||||
### Hanzo AI Systems
|
||||
|
||||
| Repository | First Commit | Description |
|
||||
|------------|-------------|-------------|
|
||||
| `hanzo/ui` | 2023-01-24 | Shared UI component library (1,114 commits) |
|
||||
| `hanzo/flow` | 2023-02-08 | AI workflow orchestration (17,390 commits) |
|
||||
| `hanzo/cli` | 2023-04-03 | Developer CLI (10,596 commits) |
|
||||
| `hanzo/jin` | 2023-05-15 | Multimodal AI -- saccade JEPA architecture |
|
||||
| `hanzo/console` | 2023-05-18 | Admin console |
|
||||
| `hanzo/dataroom` | 2023-05-27 | Secure document sharing |
|
||||
| `hanzo/ml` | 2023-06-19 | Rust ML framework -- Candle (2,619 commits) |
|
||||
| `hanzo/node` | 2023-06-25 | Distributed compute node (11,711 commits) |
|
||||
| `hanzo/docs` | 2023-07-03 | Documentation platform |
|
||||
| `hanzo/visor` / `hanzo/vm` | 2023-07-30 | Virtual machine runtime |
|
||||
| `hanzo/desktop` | 2023-08-30 | Desktop application |
|
||||
| `hanzo/cua` | 2023-11-03 | Computer-Use Agent (649 commits) |
|
||||
|
||||
### Zoo DeSci / DeAI
|
||||
|
||||
| Repository | First Commit | Description |
|
||||
|------------|-------------|-------------|
|
||||
| `zoo/ui` | 2023-01-24 | Shared UI library |
|
||||
| `zoo/zones` | 2023-02-18 | Zone management |
|
||||
| `zoo/gym-v1` | 2023-04-13 | AI training gym v1 |
|
||||
| `zoo/foundation` | 2023-05-08 | Zoo Labs Foundation website |
|
||||
| `zoo/zooai` | 2023-05-19 | Zoo AI platform |
|
||||
| `zoo/gym` | 2023-05-28 | AI training gym |
|
||||
| `zoo/agent` | 2023-06-25 | AI agent framework |
|
||||
| `zoo/app` | 2023-08-30 | Zoo application |
|
||||
|
||||
### Commit Activity
|
||||
|
||||
| Year | Hanzo | Lux | Zoo |
|
||||
|------|-------|-----|-----|
|
||||
| 2023 | 99,672 | 24,417 | 13,855 |
|
||||
|
||||
---
|
||||
|
||||
## 2024: BFT Consensus + Hardware Wallets + Compute
|
||||
|
||||
Byzantine fault tolerance research, hardware signing, Corona post-quantum signatures, and AI model refinement.
|
||||
|
||||
### Lux Advanced Protocol
|
||||
|
||||
| Repository | First Commit | Description |
|
||||
|------------|-------------|-------------|
|
||||
| `lux/chat` | 2024-04-06 | Network communication |
|
||||
| `lux/kms-go` | 2024-06-05 | KMS Go SDK |
|
||||
| `lux/liquid` | 2024-06-18 | Liquid staking |
|
||||
| `lux/corona` | 2024-07-08 | Post-quantum signature scheme (30 commits) |
|
||||
| `lux/xwallet` | 2024-07-09 | Extended wallet |
|
||||
| `lux/bank` | 2024-07-09 | Banking integration |
|
||||
| `lux/tokens` | 2024-07-15 | Token management |
|
||||
| `lux/uni-v4-subgraph` | 2024-07-23 | Uniswap V4 subgraph |
|
||||
| `lux/dwallet` | 2024-07-31 | Decentralized wallet |
|
||||
| `lux/kit` | 2024-08-07 | Development toolkit |
|
||||
| `lux/bft` | 2024-08-28 | BFT consensus research (140 commits) |
|
||||
|
||||
### Hanzo AI Platform
|
||||
|
||||
| Repository | First Commit | Description |
|
||||
|------------|-------------|-------------|
|
||||
| `hanzo/captable` | 2024-01-08 | Cap table management |
|
||||
| `hanzo/runtime` | 2024-02-06 | ML inference runtime |
|
||||
| `hanzo/sentry` | 2024-02-15 | Error monitoring |
|
||||
| `hanzo/engine` | 2024-02-26 | Standalone AI inference engine (3,258 commits) |
|
||||
| `hanzo/enso` | 2024-03-28 | Code generation |
|
||||
| `hanzo/paas` / `hanzo/platform` | 2024-04-19 | Platform-as-a-Service |
|
||||
| `hanzo/web` | 2024-04-29 | Web framework |
|
||||
| `hanzo/remove-refusals` | 2024-05-16 | Model uncensoring -- permanent weight modification |
|
||||
| `hanzo/kms-go-sdk` | 2024-06-05 | KMS Go SDK |
|
||||
| `hanzo/studio-desktop` | 2024-08-12 | AI Studio desktop app |
|
||||
| `hanzo/capnp-es` | 2024-08-16 | Cap'n Proto TypeScript bindings |
|
||||
| `hanzo/kms-python-sdk` | 2024-08-19 | KMS Python SDK |
|
||||
| `hanzo/kms-node-sdk` | 2024-08-29 | KMS Node.js SDK |
|
||||
|
||||
### Zoo Growth
|
||||
|
||||
| Repository | First Commit | Description |
|
||||
|------------|-------------|-------------|
|
||||
| `zoo/game` | 2024-08-06 | AI gaming platform |
|
||||
| `zoo/tools` | 2024-12-17 | Developer tooling |
|
||||
|
||||
### Commit Activity
|
||||
|
||||
| Year | Hanzo | Lux | Zoo |
|
||||
|------|-------|-----|-----|
|
||||
| 2024 | 141,053 | 20,987 | 21,139 |
|
||||
|
||||
---
|
||||
|
||||
## 2025: FHE + Formal Verification + Production Hardening
|
||||
|
||||
Fully homomorphic encryption, NTT SIMD acceleration, formal proofs in Lean4/TLA+/Tamarin, 23 security audits, and the full agent SDK stack.
|
||||
|
||||
### Lux Cryptography and Consensus
|
||||
|
||||
| Repository | First Commit | Description |
|
||||
|------------|-------------|-------------|
|
||||
| `lux/safe-frost` | 2025-04-11 | On-chain FROST signature verification (37 commits) |
|
||||
| `lux/consensus` | 2025-07-28 | Quasar consensus -- multi-metric BFT (504 commits) |
|
||||
| `lux/crypto` | 2025-07-25 | Unified crypto library -- BLS, ML-DSA, ML-KEM, secp256k1 |
|
||||
| `lux/database` | 2025-07-25 | Database abstraction layer |
|
||||
| `lux/ids` | 2025-07-25 | Identity and addressing |
|
||||
| `lux/warp` | 2025-07-24 | Warp cross-chain messaging |
|
||||
| `lux/go-bip32` / `lux/go-bip39` | 2025-07-25 | HD wallet key derivation |
|
||||
| `lux/p2p` | 2025-12-04 | Peer-to-peer networking |
|
||||
| `lux/cache` | 2025-12-04 | Caching layer |
|
||||
| `lux/vm` | 2025-12-19 | Virtual machine framework |
|
||||
| `lux/fhe` | 2025-12-28 | Fully homomorphic encryption engine (94 commits) |
|
||||
| `lux/proofs` / `lux/formal` | 2025-12-25 | Formal verification: 6,851 Lean4 files, 4 TLA+ specs, 2 Tamarin proofs |
|
||||
| `lux/papers` | 2025-10-28 | 136 research papers (LaTeX) |
|
||||
| `lux/lips` / `lux/lps` | 2025-07-22 | Lux Improvement Proposals (848 proposals) |
|
||||
|
||||
### Lux Infrastructure Modules (extracted from monolith)
|
||||
|
||||
| Repository | First Commit | Description |
|
||||
|------------|-------------|-------------|
|
||||
| `lux/timer` | 2025-12-04 | Timing utilities |
|
||||
| `lux/constants` | 2025-12-04 | Network constants |
|
||||
| `lux/codec` | 2025-12-04 | Serialization codec |
|
||||
| `lux/upgrade` | 2025-12-04 | Network upgrade coordination |
|
||||
| `lux/metric` | 2025-07-26 | Metrics collection |
|
||||
| `lux/math` / `lux/mock` | 2025-08-18 | Math utilities, test mocking |
|
||||
| `lux/sampler` | 2025-12-24 | Validator sampling |
|
||||
| `lux/staking` | 2025-12-24 | Staking mechanics |
|
||||
| `lux/keychain` | 2025-12-24 | Key management |
|
||||
| `lux/config` / `lux/keys` | 2025-12-21 | Configuration, key formats |
|
||||
| `lux/lamport` | 2025-12-25 | Lamport one-time signatures |
|
||||
|
||||
### Hanzo Agent and AI Stack
|
||||
|
||||
| Repository | First Commit | Description |
|
||||
|------------|-------------|-------------|
|
||||
| `hanzo/hanzo.ai` / `hanzo/hanzo.industries` | 2025-02-12 | Corporate websites |
|
||||
| `hanzo/rules` | 2025-02-17 | AI behavior rules |
|
||||
| `hanzo/operate` | 2025-03-06 | Computer operation framework |
|
||||
| `hanzo/agent` | 2025-03-11 | Agent SDK |
|
||||
| `hanzo/agency` | 2025-03-12 | Multi-agent orchestration |
|
||||
| `hanzo/python-sdk` | 2025-03-15 | Python SDK |
|
||||
| `hanzo/operative` | 2025-03-18 | Operative agent runtime |
|
||||
| `hanzo/js-sdk` / `hanzo/go-sdk` | 2025-03-26 | JavaScript and Go SDKs |
|
||||
| `hanzo/extension` | 2025-04-04 | Browser extension |
|
||||
| `hanzo/stream` | 2024-12-16 | Real-time streaming |
|
||||
| `hanzo/tools` | 2024-12-17 | Agent tool library |
|
||||
| `hanzo/hanzo.sh` | 2024-11-11 | CLI installer |
|
||||
| `hanzo/mcp` | 2025-07-24 | Model Context Protocol server |
|
||||
| `hanzo/agents` | 2025-07-24 | Agent definitions and configs |
|
||||
| `hanzo/engine` | (continued) | AI inference -- 3,258 commits |
|
||||
| `hanzo/node` | (continued) | Distributed compute -- 11,711 commits |
|
||||
| `hanzo/skills` | 2025-10-18 | Agent skill library |
|
||||
| `hanzo/computer` | 2025-10-29 | Computer-use tools |
|
||||
| `hanzo/gateway` | 2025-10-28 | API gateway |
|
||||
| `hanzo/rust-sdk` | 2025-10-28 | Rust SDK |
|
||||
| `hanzo/zen-agentic-dataset` | 2025-12-30 | Agentic training data |
|
||||
| `hanzo/patents` | 2025-12-28 | Patent portfolio |
|
||||
| `hanzo/proofs` | (2026-03-31 active) | Formal proofs: 6,309 Lean4 files |
|
||||
| `hanzo/papers` | (2026-03-31 active) | 152 research papers |
|
||||
| `hanzo/hips` | 2025-09-07 | Hanzo Improvement Proposals (784 proposals) |
|
||||
|
||||
### Zoo Labs Foundation
|
||||
|
||||
| Repository | First Commit | Description |
|
||||
|------------|-------------|-------------|
|
||||
| `zoo/wander` | 2025-03-30 | Exploration agent |
|
||||
| `zoo/nano-1` | 2025-05-23 | Nano model experiments |
|
||||
| `zoo/ZIPs` | 2025-09-07 | Zoo Improvement Proposals (103 proposals) |
|
||||
| `zoo/zoo-ai` | 2025-10-05 | Zoo AI platform |
|
||||
| `zoo/zoo-papers-site` | 2025-10-29 | Papers website |
|
||||
| `zoo/zoo.ngo` / `zoo.exchange` / `zoo.lab` / `zoo.vote` | 2025-11-02 | Foundation web properties |
|
||||
| `zoo/universe` | 2025-11-02 | CI/CD and infrastructure |
|
||||
| `zoo/docs` | 2025-12-14 | Documentation |
|
||||
| `zoo/patents` | 2025-12-28 | Patent portfolio |
|
||||
| `zoo/papers` | (2026-03-31 active) | 41 research papers |
|
||||
|
||||
### Security Audits (lux/audits)
|
||||
|
||||
| Date | Scope |
|
||||
|------|-------|
|
||||
| 2025-12-11 | DexVM, Oracle, Perpetuals |
|
||||
| 2025-12-30 | Architecture, Consensus, Contracts, Crypto, Database, Network, Oracle, PlatformVM, ProposerVM+EVM, ThresholdVM, Warp, ZKVM, DexVM, Other VMs |
|
||||
| 2026-01-30 | Standard audit (dedicated directory) |
|
||||
| 2026-03-25 | Comprehensive security audit |
|
||||
|
||||
### Commit Activity
|
||||
|
||||
| Year | Hanzo | Lux | Zoo |
|
||||
|------|-------|-----|-----|
|
||||
| 2025 | 157,036 | 19,312 | 12,520 |
|
||||
|
||||
---
|
||||
|
||||
## 2026: Launch
|
||||
|
||||
Production launch. Mainnet, exchanges, compliance engine, GPU-accelerated EVM, FHE coprocessor.
|
||||
|
||||
### Lux Production Launch
|
||||
|
||||
| Repository | First Commit | Description |
|
||||
|------------|-------------|-------------|
|
||||
| `lux/fhe-coprocessor` | 2026-01-07 | Encrypted smart contract coprocessor |
|
||||
| `lux/fpga` | 2026-01-07 | FPGA acceleration |
|
||||
| `lux/tui` | 2026-01-07 | Terminal UI for node management |
|
||||
| `lux/accel` | 2026-01-09 | Hardware acceleration layer |
|
||||
| `lux/mlx` | 2026-01-09 | Apple MLX integration |
|
||||
| `lux/benchmarks` | 2026-02-04 | Performance benchmarks |
|
||||
| `lux/operator` | 2026-02-19 | Kubernetes operator |
|
||||
| `lux/treasury` | 2026-03-02 | Treasury management |
|
||||
| `lux/exchange-api` / `lux/exchange-proxy` | 2026-03-06 | Exchange infrastructure |
|
||||
| `lux/exchange` | 2026-04-02 | DEX frontend |
|
||||
| `lux/amm` | 2026-03-25 | Automated market maker |
|
||||
| `lux/evmgpu` | 2026-03-29 | GPU-accelerated EVM (CUDA opcode dispatch) |
|
||||
| `lux/futures` / `lux/forex` | 2026-03-30 | Derivatives and forex |
|
||||
| `lux/bank-v2` | 2026-03-31 | Banking v2 |
|
||||
| `lux/genesis` | 2026-04-04 | Genesis configuration and validator management |
|
||||
| `lux/cevm` | 2026-04-05 | C-Chain EVM |
|
||||
| `lux/gpu` | 2026-04-06 | GPU compute framework |
|
||||
| `lux/sdk-rs` | 2026-04-04 | Rust SDK |
|
||||
| `lux/evm-bench` | 2026-04-04 | EVM benchmarking suite |
|
||||
|
||||
### Hanzo Production Infrastructure
|
||||
|
||||
| Repository | First Commit | Description |
|
||||
|------------|-------------|-------------|
|
||||
| `hanzo/embeddings` | 2026-01-14 | Vector embeddings service |
|
||||
| `hanzo/store-api` | 2026-01-14 | Store API |
|
||||
| `hanzo/mpc` | 2026-01-24 | MPC threshold signing (36 commits) |
|
||||
| `hanzo/mq` | 2026-01-19 | Message queue |
|
||||
| `hanzo/contracts` | 2026-01-31 | Smart contracts |
|
||||
| `hanzo/charts` | 2026-01-31 | Helm charts |
|
||||
| `hanzo/hsm` | 2026-02-14 | Hardware security module integration |
|
||||
| `hanzo/zen-gateway` | 2026-02-14 | AI model gateway |
|
||||
| `hanzo/database` | 2026-02-14 | Database service |
|
||||
| `hanzo/kms-operator` | 2026-02-15 | KMS Kubernetes operator |
|
||||
| `hanzo/iam-sdk` | 2026-02-17 | IAM SDK |
|
||||
| `hanzo/vault` | 2026-02-23 | Secret vault |
|
||||
| `hanzo/billing` | 2026-02-23 | Billing system |
|
||||
| `hanzo/orm` | 2026-02-23 | Object-relational mapping |
|
||||
| `hanzo/operator` / `hanzo/hanzo-operator` | 2026-02-24 | Kubernetes operators |
|
||||
| `hanzo/models` | 2026-02-26 | Model registry |
|
||||
| `hanzo/ANE` | 2026-02-28 | Apple Neural Engine integration |
|
||||
| `hanzo/ast` | 2026-03-02 | Abstract syntax tree tools |
|
||||
| `hanzo/ledger` | 2026-03-05 | Financial ledger |
|
||||
| `hanzo/tunnel` | 2026-03-11 | Secure tunneling |
|
||||
| `hanzo/audit` | 2026-03-25 | Audit trail |
|
||||
| `hanzo/onnxgo` | 2026-03-31 | ONNX Go runtime |
|
||||
| `hanzo/proofs` | 2026-03-31 | Formal proofs |
|
||||
| `hanzo/papers` | 2026-03-31 | Research papers |
|
||||
|
||||
### Zoo Launch
|
||||
|
||||
| Repository | First Commit | Description |
|
||||
|------------|-------------|-------------|
|
||||
| `zoo/contracts` | 2026-01-31 | Smart contracts |
|
||||
| `zoo/genesis` | 2026-02-13 | Genesis configuration |
|
||||
| `zoo/evm` | 2026-03-30 | Zoo EVM |
|
||||
| `zoo/cli` | 2026-03-30 | Zoo CLI |
|
||||
| `zoo/operator` | 2026-03-30 | Kubernetes operator |
|
||||
| `zoo/kms` | 2026-03-30 | Key management |
|
||||
| `zoo/mpc` | 2026-03-30 | MPC engine |
|
||||
| `zoo/bridge` | 2026-03-30 | Cross-chain bridge |
|
||||
| `zoo/computer` | 2026-03-31 | Compute platform |
|
||||
| `zoo/proofs` | 2026-03-31 | Formal proofs |
|
||||
| `zoo/papers` | 2026-03-31 | Research papers |
|
||||
| `zoo/formal` | 2026-04-03 | Formal verification |
|
||||
| `zoo/exchange` | 2026-04-02 | DEX |
|
||||
|
||||
### Commit Activity (YTD through 2026-04-07)
|
||||
|
||||
| Year | Hanzo | Lux | Zoo |
|
||||
|------|-------|-----|-----|
|
||||
| 2026 | 68,379 | 4,707 | 550 |
|
||||
|
||||
---
|
||||
|
||||
## Cumulative Commit History
|
||||
|
||||
```
|
||||
Year Hanzo Lux Zoo Total
|
||||
---- ----- --- --- -----
|
||||
2014 14,547 5,405 -- 19,952
|
||||
2015 23,098 9,028 -- 32,126
|
||||
2016 17,989 2,681 -- 20,670
|
||||
2017 20,219 3,854 -- 24,073
|
||||
2018 24,508 7,427 3,009 34,944
|
||||
2019 30,747 10,229 4,467 45,443
|
||||
2020 46,845 18,333 1,151 66,329
|
||||
2021 58,199 27,811 8,923 94,933
|
||||
2022 59,535 20,333 10,029 89,897
|
||||
2023 99,672 24,417 13,855 137,944
|
||||
2024 141,053 20,987 21,139 183,179
|
||||
2025 157,036 19,312 12,520 188,868
|
||||
2026 68,379 4,707 550 73,636
|
||||
|
||||
TOTAL 761,827 174,524 75,643 1,011,994
|
||||
```
|
||||
|
||||
Note: Annual totals sum to ~1,012,000. The `git rev-list --count HEAD` grand total of 1,103,364 is higher because it counts all reachable commits including merge bases and upstream fork history counted once per repo.
|
||||
|
||||
---
|
||||
|
||||
## Fork Provenance
|
||||
|
||||
The following repositories contain upstream history from established open-source projects. Lux/Hanzo contributions are layered on top.
|
||||
|
||||
| Repository | Upstream Project | Upstream Origin Date | Fork Purpose |
|
||||
|------------|-----------------|---------------------|-------------|
|
||||
| `hanzo/sql` | PostgreSQL | 1996 | Managed PostgreSQL service |
|
||||
| `hanzo/datastore` | ClickHouse | 2008 | Analytics datastore |
|
||||
| `hanzo/kv` | Valkey | 2009 | Key-value cache |
|
||||
| `hanzo/redis` | Redis | 2009 | Redis compatibility |
|
||||
| `hanzo/kv-go` | go-redis | 2012 | Go client library |
|
||||
| `hanzo/pubsub-go` | nats.go | 2012 | Go pub/sub client |
|
||||
| `hanzo/pubsub` | NATS Server | 2012 | Message broker |
|
||||
| `hanzo/storage` | MinIO | 2014 | S3-compatible storage |
|
||||
| `hanzo/dns` | CoreDNS | 2016 | DNS service |
|
||||
| `hanzo/golang-migrate` | golang-migrate | 2014 | Database migrations |
|
||||
| `hanzo/dbx` | PocketBase dbx | 2015 | Database abstraction |
|
||||
| `hanzo/tasks` | Temporal | 2016 | Distributed task engine |
|
||||
| `lux/coreth` / `lux/geth` | go-ethereum | 2013 | EVM implementation |
|
||||
| `lux/zapdb` | Badger (Dgraph) | 2017 | Embedded KV store |
|
||||
| `lux/safe` | Gnosis Safe | 2017 | Multisig contracts |
|
||||
| `lux/explorer` | custom | 2018 | Block explorer |
|
||||
| `lux/lattice` | Lattigo (EPFL) | 2019 | Lattice cryptography |
|
||||
|
||||
All other repositories are original work.
|
||||
|
||||
---
|
||||
|
||||
## Research Papers by Domain
|
||||
|
||||
### Lux Network (136 papers)
|
||||
|
||||
**Consensus**: lux-consensus, lux-quasar-consensus, lux-fpc-consensus, lux-wave-protocol
|
||||
**Cryptography**: lux-crypto-agility, lux-corona-pq, lux-pq-crypto-suite, lux-pq-migration, lux-ntt-transform
|
||||
**FHE**: lux-fhe-smart-contracts, lux-fhe-mpc-hybrid, fhe/fhevm, fhe/fhecrdt, fhe/ml-privacy, fhe/voting
|
||||
**MPC**: lux-lss-mpc, lux-mchain-mpc
|
||||
**DeFi**: lux-lightspeed-dex, lux-economics, lux-tokenomics, lux-credit-lending, lux-omnichain-yield
|
||||
**Infrastructure**: lux-bridge, lux-teleport-protocol, lux-teleport-architecture, lux-photon-protocol, lux-nova-protocol
|
||||
**Scaling**: gpu-evm-whitepaper, evmgpu-benchmark, lux-data-availability
|
||||
**Identity**: lux-achain-attestation, lux-secure-messaging, lux-zap-wire-protocol
|
||||
**Governance**: lux-dao-governance-framework, lux-adoption-roadmap
|
||||
**Markets**: lux-market-nft, lux-credit-protocol-spec
|
||||
|
||||
### Hanzo AI (152 papers)
|
||||
|
||||
**AI/ML**: hanzo-jin-architecture, hanzo-engine-ml, hanzo-candle, hanzo-analytics-ml, hanzo-hmm, hanzo-agent-grpo, hanzo-agent-sdk
|
||||
**Infrastructure**: hanzo-aci, hanzo-base, hanzo-api-gateway, hanzo-ingress-proxy, hanzo-pubsub-events, hanzo-search
|
||||
**Commerce**: crowdstart-commerce, hanzo-commerce-payments, hanzo-checkout, hanzo-ai-commerce
|
||||
**Security**: hanzo-pq-crypto, hanzo-formal-verification, hanzo-harness-hacking
|
||||
**Platform**: hanzo-iam-platform, hanzo-sdk-ecosystem, hanzo-mcp-server, hanzo-network-whitepaper, hanzo-tokenomics
|
||||
**Communication**: hanzo-chat, hanzo-flow
|
||||
**Algorithms**: algorithms/ subdirectory, defense/ subdirectory
|
||||
**Models**: zen/ subdirectory (Zen model family)
|
||||
**Computer Use**: hanzo-operate-computer, hanzo-operative
|
||||
|
||||
### Zoo Labs (41 papers)
|
||||
|
||||
**DeSci**: zoo-conservation-ai, zoo-habitat-modeling, zoo-satellite-ecology, zoo-wildlife-tracking, zoo-citizen-science, zoo-carbon-credits, zoo-educational-ai
|
||||
**DeAI**: zoo-fhe-ai, zoo-mobile-inference, zoo-agent-nft, embedding-7680, hllm-training-free-grpo, experience-ledger-dso, beluga-l3-whitepaper
|
||||
**Blockchain**: zoo-consensus, zoo-poai-consensus, zoo-quasar-benchmarks, zoo-bridge, zoo-dex, zoo-evm-l2-architecture, zoo-evm-benchmarks, zoo-gpu-evm
|
||||
**Governance**: zoo-dao-governance, zoo-tokenomics, zip-002-zen-reranker
|
||||
**Security**: zoo-pq-crypto, zoo-mpc-custody, zoo-key-management, zoo-fhe
|
||||
**Identity**: zoo-identity-chain, zoo-experience-ledger
|
||||
**Launch**: zoo-mainnet-launch-checklist
|
||||
|
||||
---
|
||||
|
||||
## Formal Verification
|
||||
|
||||
### Lean4 Proofs (13,160 files total)
|
||||
|
||||
- **Lux** (`lux/formal/lean/`, `lux/proofs/`): BFT consensus safety, bridge security, DeFi invariants, cross-chain compute, post-quantum hybrid crypto, Verkle tree, warp security, GPU scaling laws, FHE, sharia compliance
|
||||
- **Hanzo** (`hanzo/proofs/lean/`): Complementary formal proofs
|
||||
|
||||
### TLA+ Specifications (4 specs)
|
||||
|
||||
- Teleport cross-chain protocol
|
||||
- MPC bridge protocol state machine
|
||||
|
||||
### Tamarin Protocol Proofs (2 proofs)
|
||||
|
||||
- MPC bridge cryptographic protocol security
|
||||
|
||||
### Halmos Symbolic Tests (10 contracts)
|
||||
|
||||
- Bridge and yield vault Solidity verification
|
||||
|
||||
---
|
||||
|
||||
## Active Development (as of 2026-04-07)
|
||||
|
||||
Most recently committed repositories across all three organizations:
|
||||
|
||||
| Repository | Last Commit |
|
||||
|------------|-------------|
|
||||
| `lux/node` | 2026-04-07 |
|
||||
| `lux/papers` | 2026-04-07 |
|
||||
| `lux/netrunner` | 2026-04-07 |
|
||||
| `lux/threshold` | 2026-04-07 |
|
||||
| `lux/dex` | 2026-04-07 |
|
||||
| `lux/formal` | 2026-04-07 |
|
||||
| `lux/mpc` | 2026-04-07 |
|
||||
| `hanzo/blog` | 2026-04-07 |
|
||||
| `hanzo/iam` | 2026-04-07 |
|
||||
| `hanzo/kms` | 2026-04-07 |
|
||||
| `hanzo/papers` | 2026-04-07 |
|
||||
| `hanzo/cloud` | 2026-04-07 |
|
||||
| `zoo/blog` | 2026-04-07 |
|
||||
| `zoo/papers` | 2026-04-07 |
|
||||
| `zoo/universe` | 2026-04-07 |
|
||||
@@ -1,4 +1,4 @@
|
||||
# AI Development Guide
|
||||
# LLM.md - AI Development Guide
|
||||
|
||||
This file provides guidance for AI assistants working with the Lux node codebase.
|
||||
|
||||
@@ -39,8 +39,6 @@ selection, and EVM contract auth.
|
||||
|
||||
| SHA | Tag | Impact |
|
||||
|-----|-----|--------|
|
||||
| (pending) | next | LP-023 Phase 1 batch 2: 5 more native-ZAP tx types — BaseTx v1, RegisterL1ValidatorTx v1, SlashValidatorTx v1, TransferChainOwnershipTx v1, RemoveChainValidatorTx. Cross-type Parse mean speedup 8.5× over linearcodec. Variable-length nested-object schemas (Outs/Ins/full OutputOwners/Warp Message/Evidence) deferred to batch 3. |
|
||||
| `e77a7ef78e` | (pre) | LP-023 Phase 1 batch 1: 4 simple tx types + bench harness. 37× Parse, 5.6× cross-type mean. |
|
||||
| `9df72a6f55` | v1.26.10 | Wire ChainSecurityProfile into bootstrap (closes F102) |
|
||||
| `c4af52411e` | v1.26.10 | X-Chain (avm) mempool refuses classical creds under strict-PQ |
|
||||
| `a14a1601f4` | v1.26.10 | P-Chain (platformvm) mempool refuses classical creds under strict-PQ |
|
||||
@@ -137,17 +135,7 @@ charge less.
|
||||
|
||||
## Essential Commands
|
||||
|
||||
### Release & build (canonical) — via platform.hanzo.ai, NOT GitHub Actions
|
||||
The ONE way to build + publish releases is **[`RELEASE.md`](./RELEASE.md)**:
|
||||
platform.hanzo.ai reads [`hanzo.yml`](./hanzo.yml) on a `v*` tag push and
|
||||
schedules the image build onto self-hosted **arcd** pools (`lux-build-linux-*`)
|
||||
over the native long-poll fabric — no GitHub-Actions hop. ONE `Dockerfile`
|
||||
build yields BOTH artifacts: the node image (`ghcr.io/luxfi/node:vX.Y.Z`, luxd
|
||||
+ 12 baked VM plugins) and, via [`scripts/publish_plugin_set.sh`](./scripts/publish_plugin_set.sh),
|
||||
the plugin set to `s3://lux-plugins-<env>/<pluginset>/` (operator `pluginSource`).
|
||||
The `.github/workflows/*` build/release workflows are retired (RELEASE.md §Retire).
|
||||
|
||||
### Building (local dev only)
|
||||
### Building
|
||||
```bash
|
||||
# Build node binary
|
||||
./scripts/run_task.sh build
|
||||
@@ -576,7 +564,7 @@ For importing pre-merge blocks, Shanghai must be active based on `ShanghaiTime`,
|
||||
### 8. `vms/components/lux` vs `luxfi/utxo` (parallel UTXO types)
|
||||
The `github.com/luxfi/node/vms/components/lux` package contains a parallel
|
||||
`lux.UTXO`/`lux.TransferableInput` type tree alongside `github.com/luxfi/utxo`.
|
||||
External consumers (e.g. a white-label tenant's network-bootstrap tooling) need
|
||||
External consumers (e.g. `~/work/liquidity/network-bootstrap/fund.go`) need
|
||||
to import the `vms/components/lux` variant to interop with PlatformVM/AVM
|
||||
tx builders — `luxfi/utxo` types alone are not accepted by the X→P export
|
||||
path. This is a known anomaly pending #58 follow-up consolidation; do NOT
|
||||
@@ -774,47 +762,6 @@ PRIVATE_KEY="<funded_key>" \
|
||||
./bin/bench tps --chains=lux --duration=60s --concurrency=5
|
||||
```
|
||||
|
||||
## JSON rule — json/v2 at HTTP boundary only; ZAP for all internal data
|
||||
|
||||
Encoding boundaries are one-way and explicit:
|
||||
|
||||
- **External (HTTP / JSON-RPC API)** — `github.com/go-json-experiment/json` (v2).
|
||||
Never `encoding/json`. This covers: `service/*`, `server/*`, `pubsub/`,
|
||||
`vms/platformvm/service.go`, `vms/xvm/service.go`, JSON-RPC clients
|
||||
(`vms/platformvm/client_*`), CLI tools (`cmd/*`), wallet examples,
|
||||
on-disk config files (read once at boot), genesis/upgrade blobs.
|
||||
- **Internal (state, P2P, consensus, MPC, logs, metrics)** — ZAP wire only.
|
||||
No JSON in: `network/`, `consensus/`, `snow/`, `chains/` (data-plane),
|
||||
`vms/*/state/`, `vms/*/block/`, `vms/*/txs/` (struct codec), threshold
|
||||
payloads, P2P message bodies, internal databases.
|
||||
|
||||
Migration helpers (v2 API delta vs v1):
|
||||
|
||||
| v1 (encoding/json) | v2 (go-json-experiment/json) |
|
||||
|-----------------------------------|----------------------------------------------|
|
||||
| `json.Marshal(v)` | `json.Marshal(v)` (variadic opts; signature compat) |
|
||||
| `json.MarshalIndent(v, "", " ")` | `json.Marshal(v, jsontext.WithIndent(" "))` |
|
||||
| `json.Unmarshal(b, &v)` | `json.Unmarshal(b, &v)` |
|
||||
| `json.NewEncoder(w).Encode(v)` | `json.MarshalWrite(w, v)` (no trailing `\n`) |
|
||||
| `json.NewDecoder(r).Decode(&v)` | `json.UnmarshalRead(r, &v)` |
|
||||
| `json.RawMessage` | `jsontext.Value` |
|
||||
| `*json.SyntaxError` | `*jsontext.SyntacticError` |
|
||||
|
||||
v2 semantic differences worth knowing (these change wire shape):
|
||||
|
||||
- `[N]byte` field with no `MarshalJSON` ⇒ v2 marshals as base64 string,
|
||||
v1 marshalled as JSON array of byte numbers. Add `MarshalJSON` on the
|
||||
type if the array form is wanted on the wire.
|
||||
- `time.Duration` ⇒ v2 default is the standard string form ("30m");
|
||||
v1 marshalled as int nanoseconds. v1 sub-package
|
||||
(`github.com/go-json-experiment/json/v1`) exposes `FormatDurationAsNano(true)`;
|
||||
v2 root does not. Prefer the string form on new APIs.
|
||||
- v2 enforces strict UTF-8; raw arbitrary bytes in JSON strings fail.
|
||||
This matters for legacy P2P/internal blobs that happen to be stored
|
||||
through JSON — those should already be on ZAP.
|
||||
- `json.MarshalWrite` does NOT append a trailing `\n` (v1 `NewEncoder.Encode` did).
|
||||
Adjust HTTP-handler test fixtures accordingly.
|
||||
|
||||
---
|
||||
|
||||
*Last Updated*: 2026-06-06
|
||||
*Last Updated*: 2026-02-04
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
---
|
||||
|
||||
[](https://github.com/luxfi/node/actions)
|
||||
[](https://golang.org/)
|
||||
[](https://golang.org/)
|
||||
[](LICENSE)
|
||||
|
||||
Node implementation for the [Lux](https://lux.network) network -
|
||||
@@ -38,7 +38,7 @@ The minimum recommended hardware specification for nodes connected to Mainnet is
|
||||
|
||||
If you plan to build Lux Node from source, you will also need the following software:
|
||||
|
||||
- [Go](https://golang.org/doc/install) version >= 1.26.3
|
||||
- [Go](https://golang.org/doc/install) version >= 1.23.9
|
||||
- [gcc](https://gcc.gnu.org/)
|
||||
- g++
|
||||
|
||||
|
||||
-187
@@ -1,187 +0,0 @@
|
||||
# Lux release — build + publish via platform.hanzo.ai (the ONE canonical way)
|
||||
|
||||
This is the single, repeatable way to build and publish the Lux release
|
||||
artifacts. It runs entirely on our own infrastructure — **the PaaS
|
||||
(platform.hanzo.ai) + self-hosted arcd runners + DOKS/fleet**. There is **no
|
||||
GitHub Actions build path** (the `.github/workflows/*` build/release workflows
|
||||
are retired — see [§Retire](#retire-the-github-actions-build-workflows)).
|
||||
|
||||
## What a release produces
|
||||
|
||||
ONE `Dockerfile` multi-stage build (this repo) is the single source of truth.
|
||||
It compiles `luxd` + all 12 VM plugins (CGO_ENABLED=0) and yields TWO
|
||||
distribution surfaces:
|
||||
|
||||
| # | Artifact | Destination | Consumed by |
|
||||
|---|----------|-------------|-------------|
|
||||
| 1 | node image (luxd + 12 plugins baked at `/luxd/build/plugins/`) | `ghcr.io/luxfi/node:vX.Y.Z` | operator pod image; `startup.sh cp /luxd/build/plugins/*` |
|
||||
| 2 | plugin set (the 12 VM-ID binaries + `SHA256SUMS`) | `s3://lux-plugins-<env>/<pluginset>/` | operator `plugin-fetch` init container (LuxNetwork CR `pluginSource`) |
|
||||
|
||||
Artifact 2 is **extracted from** artifact 1 — the plugins are never compiled
|
||||
twice. One build, two surfaces (DRY, orthogonal).
|
||||
|
||||
The plugin versions are pinned as Dockerfile build-args, kept in lockstep with
|
||||
this repo's `go.mod`:
|
||||
|
||||
- `EVM_VERSION` (luxfi/evm — C-Chain EVM, the `0x9999` settlement surface)
|
||||
- `CHAINS_REF` (luxfi/chains — the 10 non-DEX VMs incl. bridgevm)
|
||||
- `DEX_REF` (luxfi/dex `cmd/dchain` — the native D-Chain DEX VM)
|
||||
|
||||
## The machinery
|
||||
|
||||
```
|
||||
git tag vX.Y.Z (push)
|
||||
│ GitHub App webhook ─▶ https://platform.hanzo.ai/v1/github-webhook
|
||||
▼
|
||||
platform BuildScheduler ── reads hanzo.yml @ tag, validates, enqueues
|
||||
│ one build_job per matrix entry
|
||||
▼
|
||||
native long-poll fabric (build-queue.ts) NO GitHub Actions hop
|
||||
│ arcd runner POST /v1/arcd/poll (HMAC)
|
||||
▼
|
||||
arcd runner on pool lux-build-linux-<arch>
|
||||
│ git checkout @ tag → docker build -f Dockerfile . → docker push
|
||||
▼
|
||||
ghcr.io/luxfi/node:vX.Y.Z (artifact 1)
|
||||
│ POST /v1/arcd/complete (status, image_digest)
|
||||
▼
|
||||
build_job (DB, system-of-record)
|
||||
```
|
||||
|
||||
- **PaaS**: `platform.hanzo.ai` (`~/work/hanzo/platform`,
|
||||
`pkg/platform/src/services/ci/`). Owns the schema, the scheduler, the durable
|
||||
`build_job` record, and the native long-poll dispatch.
|
||||
- **Build muscle**: self-hosted **arcd** runner pools, `lux-build-linux-amd64`
|
||||
and `lux-build-linux-arm64` (one daemon per fleet host; `spark` = linux/arm64,
|
||||
amd64 via buildx). NO GitHub-hosted runners; NO GitHub Actions orchestration
|
||||
on the native path.
|
||||
- **Contract**: `~/work/hanzo/platform/docs/PLATFORM_CI.md`.
|
||||
|
||||
## Build — the one command
|
||||
|
||||
A release is a semver tag push. The declarative entrypoint is this repo's
|
||||
[`hanzo.yml`](./hanzo.yml); the trigger is one of:
|
||||
|
||||
**(a) Tag push (normal release).** Cutting the tag IS the release:
|
||||
|
||||
```bash
|
||||
git tag v1.30.41 && git push origin v1.30.41
|
||||
```
|
||||
|
||||
The webhook maps `refs/tags/v1.30.41` → `branch=v1.30.41`; `hanzo.yml`'s
|
||||
`tag-pattern: "{{git.branch}}"` yields the image tag `v1.30.41`. Platform
|
||||
schedules the amd64 + arm64 builds onto the live `lux-build-*` arcd pools and
|
||||
pushes the multi-arch image to GHCR.
|
||||
|
||||
**(b) On-demand (re-release / backfill).** The platform `buildJob.trigger`
|
||||
tRPC mutation schedules the same build for an explicit ref, no push required:
|
||||
|
||||
```
|
||||
buildJob.trigger({
|
||||
installationId: "<luxfi GitHub App installation id>",
|
||||
repo: "luxfi/node",
|
||||
sha: "<commit at the tag>",
|
||||
ref: "refs/tags/v1.30.41",
|
||||
branch: "v1.30.41" // → image tag via {{git.branch}}
|
||||
})
|
||||
```
|
||||
|
||||
Track it: `buildJob.list` / `buildJob.one` / `buildJob.logs` (org-scoped).
|
||||
|
||||
> A pool goes **native** the moment an arcd runner self-registers for it
|
||||
> (`arcd_runner.lastSeen` within 90s); until then platform transparently falls
|
||||
> back to `workflow_dispatch` so a build is never stranded. To run a release
|
||||
> fully GitHub-free, ensure a `lux-build-linux-{amd64,arm64}` runner is live
|
||||
> (`tRPC arcd` / the `arcd_runner` table). Set platform env
|
||||
> `WORKFLOW_DISPATCH_FALLBACK=false` to forbid the legacy hop.
|
||||
|
||||
### What the runner runs (identical on a fleet host, for manual/DR builds)
|
||||
|
||||
The native path runs exactly the repo's `Dockerfile`. To reproduce on a fleet
|
||||
host directly (e.g. `spark`), with no platform and no GitHub:
|
||||
|
||||
```bash
|
||||
# on spark (linux/arm64; amd64 via buildx)
|
||||
git clone --branch v1.30.41 git@github.com:luxfi/node.git && cd node
|
||||
docker buildx build --platform linux/amd64 \
|
||||
--build-arg CGO_ENABLED=0 \
|
||||
-t ghcr.io/luxfi/node:v1.30.41 -f Dockerfile --push .
|
||||
```
|
||||
|
||||
## Publish the plugin set — step 2
|
||||
|
||||
After the image exists, publish artifact 2 from it (one command, idempotent,
|
||||
no second compile). Run on any fleet host or a DOKS Job that has `crane`/docker
|
||||
+ `mc`; typically the same arcd runner that just built the image:
|
||||
|
||||
```bash
|
||||
scripts/publish_plugin_set.sh \
|
||||
ghcr.io/luxfi/node:v1.30.41 \
|
||||
lux-plugins-<env>/<pluginset> \
|
||||
lux # mc alias for the target MinIO/S3
|
||||
# e.g. lux-plugins-testnet/v1.3.5
|
||||
```
|
||||
|
||||
It extracts the 12 plugin binaries from the image, writes `SHA256SUMS`, uploads
|
||||
all to `s3://lux-plugins-<env>/<pluginset>/`, and verifies remote==local sha.
|
||||
|
||||
S3 is the in-cluster MinIO (`s3.lux-system.svc.cluster.local:9000`, external
|
||||
`s3.lux.network`). Configure the `mc` alias once with the `hanzo-s3-secret`
|
||||
credentials:
|
||||
|
||||
```bash
|
||||
mc alias set lux <endpoint> hanzo "$(kubectl -n lux-system get secret \
|
||||
hanzo-s3-secret -o jsonpath='{.data.password}' | base64 -d)" --api s3v4
|
||||
```
|
||||
|
||||
A pluginset prefix is **immutable** — bump `<pluginset>` for a new release,
|
||||
never overwrite a prefix a live network points at.
|
||||
|
||||
## Deploy — step 3 (operator, not this repo)
|
||||
|
||||
luxd rollout is owned by the **lux operator** (`~/work/lux/operator`,
|
||||
`LuxNetwork` CR). Update the CR's `image.tag` (artifact 1) and, when the
|
||||
network fetches plugins from S3, the `pluginSource.bucket` + per-plugin
|
||||
`sha256` (artifact 2, from the `SHA256SUMS` you just published). The operator's
|
||||
`plugin-fetch` init container verifies each sha256 fail-closed. This is
|
||||
deliberately decoupled from build: `hanzo.yml` has **no `deploy:` block**.
|
||||
|
||||
## Reproducibility
|
||||
|
||||
- The build is **functionally reproducible**: same source tags + same toolchain
|
||||
(Go 1.26.4) + `CGO_ENABLED=0` ⇒ functionally identical plugins, provable by a
|
||||
fleet rebuild (verified: `spark` rebuilt evm@v1.99.37 + dexvm@v1.5.15 from the
|
||||
same tags). It is **not bit-identical by construction**: the Dockerfile plugin
|
||||
stages omit `-trimpath` and use `-mod=mod` with a first-party `go.sum` strip
|
||||
(re-resolves luxfi/* deps), so embedded paths + re-tagged module content can
|
||||
shift the bytes (Go `BuildID` differs; binary ~16 KB larger). The published
|
||||
image is the canonical artifact; verify against ITS baked sha (what
|
||||
`publish_plugin_set.sh` records), not a separate fleet build.
|
||||
- To make releases bit-reproducible (future hardening, patch-only): add
|
||||
`-trimpath` to every plugin `go build` and pin `go.sum` (drop the strip +
|
||||
`-mod=mod`). Tracked as a follow-up; not required for correctness.
|
||||
|
||||
## Retire the GitHub Actions build workflows
|
||||
|
||||
These `.github/workflows/*` build/release/CI workflows are superseded by this
|
||||
flow and must be removed/disabled (platform owns build; the native long-poll
|
||||
owns dispatch). Delete them once a `lux-build-*` arcd runner is live:
|
||||
|
||||
| Workflow | Replaced by |
|
||||
|----------|-------------|
|
||||
| `docker.yml` (built `ghcr.io/luxfi/node` on the `lux-build` ARC pool) | `hanzo.yml` (artifact 1) — native long-poll, NO GitHub Actions |
|
||||
| `release.yml` | the tag-push trigger above + `scripts/publish_plugin_set.sh` |
|
||||
| `build.yml`, `ci.yml` | platform CI test step (runner runs `go test` pre-build) |
|
||||
| `build-linux-binaries.yml` | `Dockerfile` builder stage (luxd binary) |
|
||||
| `build-ubuntu-amd64-release.yml`, `build-ubuntu-arm64-release.yml` | `Dockerfile` + buildx multi-arch |
|
||||
| `build-macos-release.yml`, `build-win-release.yml`, `build-and-test-mac-windows.yml` | arcd `lux-build-{macos,windows}-*` pools (matrix in `hanzo.yml` when desired) |
|
||||
| `build-deb-pkg.sh`, `build-tgz-pkg.sh` (under `.github/workflows/`) | packaging step on the arcd runner (post-build), not GitHub Actions |
|
||||
| `codeql-analysis.yml`, `fuzz.yml`, `fuzz_merkledb.yml`, `test-database-replay.yml` | scheduled jobs on arcd / DOKS (not a build dependency) |
|
||||
| `buf-lint.yml`, `buf-push.yml`, `labels.yml`, `stale.yml` | repo-hygiene; migrate to arcd cron or drop |
|
||||
|
||||
The same retirement applies to the equivalent build/release workflows in the
|
||||
plugin-source repos (`luxfi/evm`, `luxfi/chains`, `luxfi/dex`): their artifacts
|
||||
are built from source by THIS repo's `Dockerfile` at the pinned refs, so those
|
||||
repos need no independent image/release CI — only their tags. Migrate each by
|
||||
adding a `hanzo.yml` (if it ships its own image) or deleting its build CI (if it
|
||||
is consumed only as a Go module / plugin source here).
|
||||
+1
-1
@@ -4762,7 +4762,7 @@ This was the version used for the first public Lux Network deployment in Decembe
|
||||
### Network Support
|
||||
|
||||
- **Primary Network**: LUX mainnet and testnet
|
||||
- **Net Support**: Full chain capabilities
|
||||
- **Net Support**: Full L2/chain capabilities
|
||||
- **Cross-Chain**: Warp messaging and atomic swaps
|
||||
- **Validator Management**: Staking and delegation
|
||||
|
||||
|
||||
+4
-4
@@ -10,10 +10,10 @@ import (
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/luxfi/filesystem/perms"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/node/node"
|
||||
"github.com/luxfi/node/utils"
|
||||
"github.com/luxfi/filesystem/perms"
|
||||
"github.com/luxfi/sys/ulimit"
|
||||
|
||||
nodeconfig "github.com/luxfi/node/config/node"
|
||||
@@ -58,9 +58,9 @@ func New(config nodeconfig.Config) (App, error) {
|
||||
infoLevel, _ := log.ToLevel("info")
|
||||
logFactory := log.NewFactoryWithConfig(log.Config{
|
||||
RotatingWriterConfig: log.RotatingWriterConfig{
|
||||
MaxSize: 8, // 8MB per log file (reasonable for standard profile)
|
||||
MaxFiles: 5, // Keep 5 rotated files
|
||||
MaxAge: 7, // 7 days retention
|
||||
MaxSize: 8, // 8MB per log file (reasonable for standard profile)
|
||||
MaxFiles: 5, // Keep 5 rotated files
|
||||
MaxAge: 7, // 7 days retention
|
||||
Directory: config.DatabaseConfig.Path,
|
||||
},
|
||||
DisplayLevel: infoLevel,
|
||||
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
validators "github.com/luxfi/validators"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
metrics "github.com/luxfi/metric"
|
||||
validators "github.com/luxfi/validators"
|
||||
)
|
||||
|
||||
// NewManager creates a new benchlist manager
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/crypto/hash"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/crypto/hash"
|
||||
)
|
||||
|
||||
// BenchmarkHashingComputeHash256 benchmarks SHA256 hashing performance
|
||||
|
||||
@@ -7,11 +7,11 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/crypto/hash"
|
||||
"github.com/luxfi/database/memdb"
|
||||
"github.com/luxfi/database/prefixdb"
|
||||
"github.com/luxfi/database/versiondb"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/crypto/hash"
|
||||
)
|
||||
|
||||
// BenchmarkMemoryDatabase benchmarks in-memory database operations
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
compression "github.com/luxfi/compress"
|
||||
"github.com/luxfi/ids"
|
||||
compression "github.com/luxfi/compress"
|
||||
)
|
||||
|
||||
// BenchmarkMessageCompression benchmarks message compression
|
||||
|
||||
Vendored
+1
-1
@@ -6,9 +6,9 @@ package lru
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/luxfi/container/linked"
|
||||
"github.com/luxfi/node/cache"
|
||||
"github.com/luxfi/utils"
|
||||
"github.com/luxfi/container/linked"
|
||||
)
|
||||
|
||||
var _ cache.Cacher[struct{}, struct{}] = (*Cache[struct{}, struct{}])(nil)
|
||||
|
||||
Vendored
+1
-1
@@ -6,9 +6,9 @@ package lru
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/luxfi/container/linked"
|
||||
"github.com/luxfi/node/cache"
|
||||
"github.com/luxfi/utils"
|
||||
"github.com/luxfi/container/linked"
|
||||
)
|
||||
|
||||
var _ cache.Cacher[struct{}, any] = (*SizedCache[struct{}, any])(nil)
|
||||
|
||||
Vendored
+1
-1
@@ -6,8 +6,8 @@ package cache
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/luxfi/container/linked"
|
||||
"github.com/luxfi/utils"
|
||||
"github.com/luxfi/container/linked"
|
||||
)
|
||||
|
||||
var _ Cacher[struct{}, struct{}] = (*LRU[struct{}, struct{}])(nil)
|
||||
|
||||
Vendored
+1
-1
@@ -6,8 +6,8 @@ package cache
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/luxfi/container/linked"
|
||||
"github.com/luxfi/utils"
|
||||
"github.com/luxfi/container/linked"
|
||||
)
|
||||
|
||||
var _ Cacher[struct{}, any] = (*sizedLRU[struct{}, any])(nil)
|
||||
|
||||
@@ -8,13 +8,13 @@ import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/luxfi/vm/chain"
|
||||
consensusvertex "github.com/luxfi/consensus/engine/vertex"
|
||||
"github.com/luxfi/database"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/node/vms/platformvm/fx"
|
||||
"github.com/luxfi/runtime"
|
||||
vmcore "github.com/luxfi/vm"
|
||||
"github.com/luxfi/vm/chain"
|
||||
"github.com/luxfi/warp"
|
||||
)
|
||||
|
||||
|
||||
+34
-329
@@ -9,9 +9,9 @@ import (
|
||||
"context"
|
||||
"crypto"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/go-json-experiment/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -305,19 +305,6 @@ func getValidatorState(state validators.State) validators.State {
|
||||
return &noopValidatorState{}
|
||||
}
|
||||
|
||||
// quorumValidatorStateLive reports whether a HEIGHT-INDEXED validator state has
|
||||
// actually been published for a K>1 quorum chain. A nil state means the P-Chain
|
||||
// never published its validators.State into the manager — getValidatorState would
|
||||
// then supply the no-op State, whose GetValidatorSet is empty at every height, so
|
||||
// the ⅔-by-stake tally is 0 and VerifyWeighted fails closed FOREVER (a silent
|
||||
// permanent finality stall). This predicate is the fail-closed guard (CRITICAL-1
|
||||
// (c)): a K>1 chain whose state is not live must REFUSE TO START loudly rather
|
||||
// than run with the no-op State. It is a pure function so the guard is unit-
|
||||
// testable without building a whole chain (quorum_guard_node_test.go).
|
||||
func quorumValidatorStateLive(state validators.State) bool {
|
||||
return state != nil
|
||||
}
|
||||
|
||||
// createWarpSigner creates a warp.Signer from a bls.Signer
|
||||
func createWarpSigner(sk bls.Signer, networkID uint32, chainID ids.ID) warp.Signer {
|
||||
if sk == nil {
|
||||
@@ -358,41 +345,17 @@ type ManagerConfig struct {
|
||||
PartialSyncPrimaryNetwork bool
|
||||
Server server.Server // Handles HTTP API calls
|
||||
AtomicMemory *atomic.Memory
|
||||
UTXOAssetID ids.ID
|
||||
UTXOAssetID ids.ID
|
||||
SkipBootstrap bool // Skip bootstrapping and start processing immediately
|
||||
EnableAutomining bool // Enable automining in POA mode
|
||||
XChainID ids.ID // ID of the X-Chain,
|
||||
CChainID ids.ID // ID of the C-Chain,
|
||||
DChainID ids.ID // ID of the D-Chain (DEX),
|
||||
CriticalChains set.Set[ids.ID] // Chains that can't exit gracefully
|
||||
|
||||
// ChainAuthorizations gates per-validator activation of specific chains on
|
||||
// X-Chain NFT ownership: a validator may track/validate a listed chain
|
||||
// only if its staking X-address (StakingXAddress) holds the required
|
||||
// nftfx NFT. A nil/empty map means no chain is NFT-gated, so every chain
|
||||
// behaves exactly as before. Critical chains are never gated regardless of
|
||||
// this map. Node wiring (e.g. DChainID -> dex-validator collection) is set
|
||||
// in node.go; see authorizeChainActivation.
|
||||
ChainAuthorizations map[ids.ID]NFTAuthorization
|
||||
// StakingXAddress is this node's X-Chain address derived from its staking
|
||||
// keys; it is the owner whose UTXOs the NFT-authorization gate inspects.
|
||||
// Empty when no chain is gated; an empty address with a gated chain causes
|
||||
// that chain to be opted out (fail closed).
|
||||
StakingXAddress ids.ShortID
|
||||
// DexValidator is the operator's opt-in to PARTICIPATE in the D-Chain DEX
|
||||
// (track/validate it + dial the venue matcher). It is a NECESSARY condition
|
||||
// for D-Chain activation that composes AND-wise with the NFT gate: the
|
||||
// D-Chain (DChainID) activates only if DexValidator is true AND the NFT
|
||||
// requirement (if any) is satisfied. Default false means a node does NOT
|
||||
// activate the D-Chain even when it is queued by --track-all-chains; the
|
||||
// activation authority is authorizeChainActivation, not the platformvm
|
||||
// tracking set. Only the D-Chain consults this flag; every other chain is
|
||||
// untouched. Sourced from node Config.DexValidator (see node.go).
|
||||
DexValidator bool
|
||||
TimeoutManager timeout.Manager // Manages request timeouts when sending messages to other validators
|
||||
Health health.Registerer
|
||||
NetConfigs map[ids.ID]nets.Config // ID -> NetConfig
|
||||
ChainConfigs map[string]ChainConfig // alias -> ChainConfig
|
||||
TimeoutManager timeout.Manager // Manages request timeouts when sending messages to other validators
|
||||
Health health.Registerer
|
||||
NetConfigs map[ids.ID]nets.Config // ID -> NetConfig
|
||||
ChainConfigs map[string]ChainConfig // alias -> ChainConfig
|
||||
// ShutdownNodeFunc allows the chain manager to issue a request to shutdown the node
|
||||
ShutdownNodeFunc func(exitCode int)
|
||||
MeterVMEnabled bool // Should each VM be wrapped with a MeterVM
|
||||
@@ -462,16 +425,6 @@ type manager struct {
|
||||
pendingVMChainsLock sync.RWMutex
|
||||
pendingVMChains map[ids.ID][]ChainParameters
|
||||
|
||||
// gatedChainsLock guards the NFT-authorization gate's defer state.
|
||||
// pendingGatedChains holds chains parked because the X-Chain was not yet
|
||||
// bootstrapped when their activation was attempted; they are re-queued once
|
||||
// the X-Chain is created (retryPendingGatedChains). gatedAttempts caps
|
||||
// re-attempts per chain so a never-bootstrapping X-Chain cannot park a
|
||||
// chain forever. See manager_authz.go.
|
||||
gatedChainsLock sync.Mutex
|
||||
pendingGatedChains []ChainParameters
|
||||
gatedAttempts map[ids.ID]int
|
||||
|
||||
chainsLock sync.Mutex
|
||||
// Key: Chain's ID
|
||||
// Value: The chain
|
||||
@@ -549,7 +502,6 @@ func New(config *ManagerConfig) (Manager, error) {
|
||||
unblockChainCreatorCh: make(chan struct{}),
|
||||
chainCreatorShutdownCh: make(chan struct{}),
|
||||
pendingVMChains: make(map[ids.ID][]ChainParameters),
|
||||
gatedAttempts: make(map[ids.ID]int),
|
||||
|
||||
luxGatherer: luxGatherer,
|
||||
handlerGatherer: handlerGatherer,
|
||||
@@ -611,44 +563,6 @@ func (m *manager) createChain(chainParams ChainParameters) {
|
||||
|
||||
sb, _ := m.Nets.GetOrCreate(chainParams.ChainID)
|
||||
|
||||
// NFT-authorization gate: a chain listed in ChainAuthorizations may only be
|
||||
// activated if this validator's staking X-address holds the required
|
||||
// X-Chain NFT. Ungated and critical chains short-circuit to authorized, so
|
||||
// this is a no-op for P/C/X/Q/… and whenever ChainAuthorizations is empty.
|
||||
if authorized, ready := m.authorizeChainActivation(chainParams.ID); !ready {
|
||||
// The X-Chain is not bootstrapped yet, so the gate cannot decide.
|
||||
// Park the chain out of the queue and retry once the X-Chain is
|
||||
// created; do not skip permanently. The attempt cap bounds this.
|
||||
if m.deferGatedChain(chainParams) {
|
||||
m.Log.Info("deferring gated chain activation until X-Chain bootstrapped",
|
||||
log.Stringer("chainID", chainParams.ID),
|
||||
log.Stringer("vmID", chainParams.VMID),
|
||||
)
|
||||
return
|
||||
}
|
||||
// Cap exhausted: the X-Chain never became queryable. Opt out cleanly,
|
||||
// exactly like an unauthorized chain — mark bootstrapped, no failing
|
||||
// health check.
|
||||
m.Log.Warn("opting out of gated chain: X-Chain did not bootstrap in time",
|
||||
log.Stringer("chainID", chainParams.ID),
|
||||
log.Stringer("vmID", chainParams.VMID),
|
||||
)
|
||||
sb.Bootstrapped(chainParams.ID)
|
||||
return
|
||||
} else if !authorized {
|
||||
// Clean opt-out: this validator does not hold the required NFT. Mirror
|
||||
// the VM-plugin-not-loaded skip exactly — mark the slot bootstrapped
|
||||
// and return WITHOUT a failing health check, so the node stays healthy
|
||||
// while declining to validate a chain it is not authorized for.
|
||||
m.Log.Info("chain activation not authorized — validator X-address does not hold the required NFT",
|
||||
log.Stringer("chainID", chainParams.ChainID),
|
||||
log.Stringer("chainID", chainParams.ID),
|
||||
log.Stringer("vmID", chainParams.VMID),
|
||||
)
|
||||
sb.Bootstrapped(chainParams.ID)
|
||||
return
|
||||
}
|
||||
|
||||
// Note: buildChain builds all chain's relevant objects (notably engine and handler)
|
||||
// but does not start their operations. Starting of the handler (which could potentially
|
||||
// issue some internal messages), is delayed until chain dispatching is started and
|
||||
@@ -784,13 +698,6 @@ func (m *manager) createChain(chainParams ChainParameters) {
|
||||
m.chains[chainParams.ID] = chain
|
||||
m.chainsLock.Unlock()
|
||||
|
||||
// The X-Chain is now tracked, so the NFT-authorization gate can query it.
|
||||
// Re-queue any gated chains that were parked waiting for it (no-op when
|
||||
// nothing is parked or this is not the X-Chain).
|
||||
if chainParams.ID == m.XChainID {
|
||||
m.retryPendingGatedChains()
|
||||
}
|
||||
|
||||
// Associate the newly created chain with its default alias
|
||||
if err := m.Alias(chainParams.ID, chainParams.ID.String()); err != nil {
|
||||
m.Log.Error("failed to alias the new chain with itself",
|
||||
@@ -1008,7 +915,7 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
|
||||
|
||||
XChainID: m.XChainID,
|
||||
CChainID: m.CChainID,
|
||||
UTXOAssetID: m.UTXOAssetID,
|
||||
UTXOAssetID: m.UTXOAssetID,
|
||||
ChainDataDir: chainDataDir,
|
||||
|
||||
BCLookup: m,
|
||||
@@ -1156,57 +1063,6 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
|
||||
}
|
||||
m.Log.Info("VM initialized successfully", log.Stringer("chainID", chainParams.ID))
|
||||
|
||||
// CRITICAL-1(a): publish the P-Chain's HEIGHT-INDEXED validators.State so
|
||||
// every K>1 chain built AFTER it (C-Chain, L1s, …) resolves the weighted
|
||||
// validator set / per-voter pubkeys / stake at the block's P-chain epoch
|
||||
// height. The platformvm VM instance IS a validators.State (it embeds the
|
||||
// height-indexed pvalidators.Manager built from its own state DB, with the
|
||||
// real GetValidatorSet(ctx, height, netID)). It only EXISTS after the
|
||||
// P-Chain VM is initialized — which is here — and the P-Chain is created
|
||||
// before any other primary-network chain (single serial chain-creator
|
||||
// goroutine), so this assignment happens-before every later chain reads
|
||||
// m.validatorState in the K>1 wiring block below. Without this the field
|
||||
// stays nil → getValidatorState returns the no-op → empty set at every
|
||||
// height → ⅔-stake tally is 0 → finality stalls forever on every K>1 chain
|
||||
// (the bug the fail-closed guard now also catches). Plain assignment is
|
||||
// race-free: chain creation is serialized (dispatchChainCreator).
|
||||
if chainParams.ID == constants.PlatformChainID {
|
||||
if vdrState, ok := vmImpl.(validators.State); ok {
|
||||
m.validatorState = vdrState
|
||||
m.Log.Info("published P-Chain height-indexed validator state to chain manager (MEDIUM-1/CRITICAL-1)",
|
||||
log.Stringer("chainID", chainParams.ID))
|
||||
} else {
|
||||
// The P-Chain MUST be a validators.State; if it is not, every K>1
|
||||
// chain (including the P-Chain itself) would stall. Fail loud.
|
||||
return nil, fmt.Errorf(
|
||||
"P-Chain VM %T does not implement validators.State — cannot publish the "+
|
||||
"height-indexed validator set; every K>1 quorum chain would stall finality",
|
||||
vmImpl)
|
||||
}
|
||||
}
|
||||
|
||||
// X-Chain (and any DAG-native VM) must be linearized into linear block mode
|
||||
// BEFORE it goes Ready, so its embedded block builder is wired to the real
|
||||
// toEngine channel the cert runtime reads from. Interface-gated: only VMs that
|
||||
// implement Linearize (X-Chain) take this path; C/D/P/Q are untouched. This is
|
||||
// what lets X-Chain finalize through the same 2/3-stake cert path as every other
|
||||
// chain instead of the (now-removed) undriven DAG engine.
|
||||
if linearVM, ok := vmTyped.(interface {
|
||||
Linearize(context.Context, ids.ID, chan<- vm.Message) error
|
||||
}); ok {
|
||||
m.Log.Info("linearizing DAG-native VM into linear block mode",
|
||||
log.Stringer("chainID", chainParams.ID))
|
||||
linCtx, linCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
if err := linearVM.Linearize(linCtx, ids.Empty, toEngine); err != nil {
|
||||
linCancel()
|
||||
m.Log.Error("failed to linearize VM",
|
||||
log.Stringer("chainID", chainParams.ID),
|
||||
log.Err(err))
|
||||
return nil, fmt.Errorf("failed to linearize VM into linear block mode: %w", err)
|
||||
}
|
||||
linCancel()
|
||||
}
|
||||
|
||||
// Transition VM to normal operation after initialization
|
||||
// For genesis-based networks with pre-configured validators, this is required
|
||||
// to make the VM APIs available immediately
|
||||
@@ -1266,109 +1122,39 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
|
||||
)
|
||||
|
||||
// Choose consensus parameters based on mode:
|
||||
// - Single-node (--dev, sybil protection disabled): K=1, self-voting
|
||||
// (the sole validator's accept is the 1-of-1 quorum).
|
||||
// - Multi-node (sybil-protected): a BYZANTINE-fault-tolerant param set
|
||||
// selected by network (Mainnet K=21 / Testnet K=11 / Default K=20).
|
||||
//
|
||||
// CRITICAL-2: the prior code used LocalParams() (K=3, α=2 → f=0, CFT) for
|
||||
// ALL sybil-protected nets — a SINGLE Byzantine validator forks K=3/α=2.
|
||||
// We now select a real BFT set and FAIL CLOSED if it is not value-safe.
|
||||
consensusParams := selectConsensusParams(m.SybilProtectionEnabled, m.NetworkID)
|
||||
if m.SybilProtectionEnabled {
|
||||
if err := consensusParams.ValidateForValueNetwork(m.NetworkID); err != nil {
|
||||
return nil, fmt.Errorf("refusing to start multi-node chain %s with non-BFT consensus params: %w", chainParams.ID, err)
|
||||
// - Single-node (--dev, sybil protection disabled): K=1, self-voting
|
||||
// - Multi-node (normal): K=3, 2/3 threshold
|
||||
var consensusParams consensusconfig.Parameters
|
||||
if !m.SybilProtectionEnabled {
|
||||
consensusParams = consensusconfig.Parameters{
|
||||
K: 1,
|
||||
Alpha: 1.0,
|
||||
AlphaPreference: 1,
|
||||
AlphaConfidence: 1,
|
||||
Beta: 1,
|
||||
ConcurrentPolls: 1,
|
||||
OptimalProcessing: 1,
|
||||
MaxOutstandingItems: 256,
|
||||
MaxItemProcessingTime: 30 * time.Second,
|
||||
}
|
||||
} else {
|
||||
consensusParams = consensusconfig.LocalParams()
|
||||
}
|
||||
|
||||
// HIGH-4: wire the α-of-K vote/cert topology for multi-validator (K>1)
|
||||
// chains so finality is LIVE (votes broadcast to all, certs gossiped,
|
||||
// followers finalize on the cert). Without a verifier a K>1 engine refuses
|
||||
// to Start; without the gossiper Mode() reports degraded and value-DEX is
|
||||
// refused. For K==1 these stay nil (no quorum, no signatures).
|
||||
gossiper := &networkGossiper{net: m.Net, msgCreator: m.MsgCreator, networkID: networkID}
|
||||
netCfg := consensuschain.NetworkConfig{
|
||||
consensusEngine := consensuschain.NewRuntime(consensuschain.NetworkConfig{
|
||||
ChainID: chainParams.ID,
|
||||
NetworkID: networkID,
|
||||
NodeID: m.NodeID,
|
||||
Validators: m.Validators, // CRITICAL: Pass validator sampler for k-peer polling
|
||||
Logger: m.Log,
|
||||
Gossiper: gossiper,
|
||||
Gossiper: &networkGossiper{net: m.Net, msgCreator: m.MsgCreator, networkID: networkID},
|
||||
VM: blockBuilder,
|
||||
Params: &consensusParams,
|
||||
}
|
||||
if consensusParams.K > 1 {
|
||||
// CRITICAL-1 FAIL-CLOSED GUARD (c): a K>1 quorum chain finalizes ONLY
|
||||
// on a ⅔-by-stake supermajority read from the HEIGHT-INDEXED validator
|
||||
// state. If that state was never published (m.validatorState == nil →
|
||||
// getValidatorState returns the no-op, whose GetValidatorSet is empty at
|
||||
// every height), the stake tally is 0 → VerifyWeighted fails closed →
|
||||
// NO block EVER finalizes. That is a SILENT permanent finality stall.
|
||||
// Refuse to build the chain LOUDLY instead. The P-Chain publishes its
|
||||
// own height-indexed State into m.validatorState the moment it is
|
||||
// created (it is built before any other K>1 chain), so the P-Chain and
|
||||
// every chain after it sees a live state here; only a genuine wiring
|
||||
// regression trips this.
|
||||
if !quorumValidatorStateLive(m.validatorState) {
|
||||
return nil, fmt.Errorf(
|
||||
"refusing to start K>1 quorum chain %s: height-indexed validator state is not wired "+
|
||||
"(getValidatorState would supply the no-op State → zero stake at every height → "+
|
||||
"VerifyWeighted fails closed → finality would stall permanently). The P-Chain must be "+
|
||||
"created first and publish its validators.State; this is a wiring bug, not a runtime condition",
|
||||
chainParams.ID)
|
||||
}
|
||||
// Height-indexed validator state is the SINGLE source of epoch truth for
|
||||
// ALL FOUR epoch-pinned reads (MEDIUM-1 / CRITICAL-1 / RESIDUAL-B):
|
||||
// membership, per-voter PUBKEY (the verifier), the ⅔-by-stake tally, and
|
||||
// the set-root commitment. They are all read at the block's P-CHAIN epoch
|
||||
// height so every node computes the IDENTICAL set/pubkeys/weights/root for
|
||||
// a given block — independent of async current-map skew during a P-chain /
|
||||
// L1-staking change. (Reading the CURRENT map made the signer and the
|
||||
// assembler disagree on the set-root, and dropped the votes of validators
|
||||
// that had left the current map but were members at the epoch — stalling
|
||||
// finality at every staking change.)
|
||||
vdrState := getValidatorState(m.validatorState)
|
||||
// Vote verifier resolves the voter's pubkey from set@epoch (RESIDUAL-B),
|
||||
// the SAME height-pinned source as the stake + set-root — NOT m.Validators
|
||||
// (the current map).
|
||||
netCfg.VoteVerifier = newBLSVoteVerifier(vdrState, networkID)
|
||||
netCfg.VoteSigner = newBLSVoteSigner(m.StakingBLSKey)
|
||||
// Stake-weighted finality (HIGH-3): require a ⅔-of-stake supermajority,
|
||||
// not just the α-of-K count, so a low-stake coalition cannot finalize.
|
||||
netCfg.StakeSource = newValidatorStakeSource(vdrState, networkID)
|
||||
// Epoch binding (MEDIUM-1): pin every vote/cert to the weighted validator
|
||||
// set IN FORCE AT the block's P-chain epoch height so the ⅔-by-stake
|
||||
// predicate is enforced at that epoch — a cert gathered under one set
|
||||
// cannot be re-verified against another (its signatures were over this
|
||||
// height-pinned root).
|
||||
netCfg.ValidatorSetRoot = newValidatorSetRootSource(vdrState, networkID)
|
||||
// b2: deliver the REAL P-chain epoch height to the engine. The four reads
|
||||
// above are height-pinned but the engine reads that height off the VM
|
||||
// block (pChainHeightOf) — and a bare plugin block exposes none, so the
|
||||
// engine would resolve the set at P-chain height 0 (the GENESIS set),
|
||||
// freezing the epoch and dropping every post-genesis validator's vote.
|
||||
// Wrap the BlockBuilder so the block the engine builds/parses carries the
|
||||
// proposer's live P-chain height (max(GetCurrentHeight, parentH)), stamped
|
||||
// into the gossiped bytes so every follower adopts the IDENTICAL height —
|
||||
// the set-root/stake/pubkey reads then track the LIVE set at that height.
|
||||
// Installed ONLY here (K>1): a K==1 chain has no cert and no epoch, so the
|
||||
// stamp is inert and the inner VM is used directly.
|
||||
if blockBuilder != nil {
|
||||
blockBuilder = newPChainHeightVM(blockBuilder, vdrState, networkID)
|
||||
netCfg.VM = blockBuilder
|
||||
m.Log.Info("wired P-chain epoch height into consensus block builder (b2)",
|
||||
log.Stringer("chainID", chainParams.ID),
|
||||
log.Stringer("networkID", networkID))
|
||||
}
|
||||
}
|
||||
consensusEngine := consensuschain.NewRuntime(netCfg)
|
||||
})
|
||||
|
||||
// Start the consensus engine with a LIFETIME context (not a timeout):
|
||||
// engine.Start parents all four long-running loops (poll, vote, pipeline,
|
||||
// re-poll) to this ctx, so a WithTimeout here kills them ~30s after the
|
||||
// chain starts and the quorum cert never assembles — the finality wedge
|
||||
// fixed in v1.30.55 (ba3561778e). Do not reintroduce a timeout here.
|
||||
if err := consensusEngine.Start(context.Background(), true); err != nil {
|
||||
// Start the consensus engine
|
||||
engineStartCtx, engineStartCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer engineStartCancel()
|
||||
if err := consensusEngine.Start(engineStartCtx, true); err != nil {
|
||||
m.Log.Error("failed to start consensus engine",
|
||||
log.Stringer("chainID", chainParams.ID),
|
||||
log.Err(err))
|
||||
@@ -1441,11 +1227,7 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
|
||||
Runtime: chainRuntime,
|
||||
VM: vmTyped, // Use the real VM directly
|
||||
Engine: consensusEngine, // Use real consensus engine directly
|
||||
// Handler parses inbound blocks through the SAME builder the engine emits
|
||||
// through (blockBuilder == the P-chain-height wrapper on K>1, the inner VM
|
||||
// on K==1), so the container bytes it parses match the bytes the engine
|
||||
// framed — one codec, no raw-vs-wrapped split.
|
||||
Handler: newBlockHandler(blockBuilder, m.Log, consensusEngine, m.Net, m.MsgCreator, chainParams.ID, networkID),
|
||||
Handler: newBlockHandler(vmTyped, m.Log, consensusEngine, m.Net, m.MsgCreator, chainParams.ID, networkID),
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported VM type: %T", vmImpl)
|
||||
@@ -2304,15 +2086,7 @@ func (e *emptyValidatorManager) GetCurrentValidators(ctx context.Context, height
|
||||
// blockHandler implements handler.Handler interface and processes incoming blocks
|
||||
// This enables block propagation between validators
|
||||
type blockHandler struct {
|
||||
// vm is the SAME BlockBuilder the consensus engine builds/parses through — the
|
||||
// P-chain-height-stamping wrapper on K>1 chains (so ParseBlock unwraps the
|
||||
// transport envelope the engine emits and recovers the proposer's epoch
|
||||
// height), the inner VM directly on K==1. The handler only needs the
|
||||
// BlockBuilder subset (GetBlock / ParseBlock / LastAccepted); typing it as the
|
||||
// builder — not chain.ChainVM — keeps the engine and the handler on ONE block
|
||||
// codec, so inbound P2P container bytes are parsed by the same code that framed
|
||||
// them (no raw-vs-wrapped mismatch).
|
||||
vm consensuschain.BlockBuilder
|
||||
vm chain.ChainVM
|
||||
logger log.Logger
|
||||
engine *consensuschain.Runtime // Consensus engine for proper block handling
|
||||
net network.Network // Network for sending Qbit responses
|
||||
@@ -2351,7 +2125,7 @@ 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) *blockHandler {
|
||||
func newBlockHandler(vm chain.ChainVM, logger log.Logger, engine *consensuschain.Runtime, net network.Network, msgCreator message.OutboundMsgBuilder, chainID ids.ID, networkID ids.ID) *blockHandler {
|
||||
return &blockHandler{
|
||||
vm: vm,
|
||||
logger: logger,
|
||||
@@ -2982,27 +2756,7 @@ func (b *blockHandler) Response(ctx context.Context, nodeID ids.NodeID, requestI
|
||||
return nil
|
||||
}
|
||||
func (b *blockHandler) Gossip(ctx context.Context, nodeID ids.NodeID, msg []byte) error {
|
||||
// HIGH-4 receive demux: a quorum envelope (signed vote / finality cert) is
|
||||
// routed to the engine's α-of-K topology; anything else is a plain block
|
||||
// gossip handled by Put (backward-compatible — decode fails soft).
|
||||
//
|
||||
// The quorum path is consulted ONLY when the engine is in quorum-finality
|
||||
// mode (K>1, live topology). A single-validator engine never emits vote/cert
|
||||
// envelopes, so it always treats gossip as a block — this makes a chance
|
||||
// collision of the envelope magic with block bytes impossible to misroute on
|
||||
// the K==1 path.
|
||||
if b.engine != nil && b.engine.Mode() == consensuschain.ModeQuorumFinality {
|
||||
if kind, blockID, payload, err := decodeQuorumGossip(msg); err == nil {
|
||||
switch kind {
|
||||
case quorumKindVote:
|
||||
b.engine.HandleIncomingVote(blockID, payload)
|
||||
case quorumKindCert:
|
||||
b.engine.HandleIncomingCert(payload)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
// Plain block gossip.
|
||||
// Handle Gossip - try to process as block
|
||||
return b.Put(ctx, nodeID, 0, msg)
|
||||
}
|
||||
func (b *blockHandler) GetStateSummaryFrontier(ctx context.Context, nodeID ids.NodeID, requestID uint32, deadline time.Time) error {
|
||||
@@ -3103,14 +2857,6 @@ func (b *blockHandler) HandleInbound(ctx context.Context, msg handler.Message) e
|
||||
case handler.Context:
|
||||
// Context contains prerequisite blocks - process each one via Put
|
||||
return b.handleContext(ctx, msg.NodeID, msg.RequestID, msg.Message)
|
||||
case handler.Gossip:
|
||||
// App-gossip envelope carrying an α-of-K quorum vote or finality cert.
|
||||
// Route to Gossip, which demuxes the quorum envelope into
|
||||
// engine.HandleIncomingVote / HandleIncomingCert (the vote TRANSPORT that
|
||||
// drives finality). Without this case the router delivers the message here
|
||||
// but the switch drops it, so no vote is ever counted and the chain wedges.
|
||||
// Non-quorum gossip falls through inside Gossip to a plain block Put.
|
||||
return b.Gossip(ctx, msg.NodeID, msg.Message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -3234,47 +2980,6 @@ type networkGossiper struct {
|
||||
// Compile-time check that networkGossiper implements Gossiper
|
||||
var _ consensuschain.Gossiper = (*networkGossiper)(nil)
|
||||
|
||||
// Compile-time check that networkGossiper implements QuorumGossiper — the
|
||||
// vote/cert distribution topology that makes α-of-K finality LIVE (HIGH-4).
|
||||
// Without this the consensus engine runs degraded (Mode() != ModeQuorumFinality)
|
||||
// and value-DEX is refused.
|
||||
var _ consensuschain.QuorumGossiper = (*networkGossiper)(nil)
|
||||
|
||||
// BroadcastVote sends this node's SIGNED accept vote for blockID to ALL
|
||||
// validators on the network (not just the proposer). The signed vote rides on
|
||||
// app-gossip framed in a quorum envelope (decoded by blockHandler.Gossip into
|
||||
// engine.HandleIncomingVote). Broadcasting to ALL — rather than SendVote-to-
|
||||
// proposer-only — is the structural fix for the proposer-freeze: any node that
|
||||
// collects α distinct signed votes can assemble + gossip the cert, so finality
|
||||
// no longer hinges on one node's inbound Chits.
|
||||
func (g *networkGossiper) BroadcastVote(chainID ids.ID, networkID ids.ID, blockID ids.ID, voteBytes []byte) int {
|
||||
if g.net == nil || g.msgCreator == nil {
|
||||
return 0
|
||||
}
|
||||
envelope := encodeQuorumGossip(quorumKindVote, blockID, voteBytes)
|
||||
msg, err := g.msgCreator.Gossip(chainID, envelope)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return g.net.Gossip(msg, nil, g.networkID, -1, 0, 0).Len()
|
||||
}
|
||||
|
||||
// GossipCert broadcasts an assembled α-of-K finality cert to ALL validators so
|
||||
// followers finalize blockID on a verifiable proof (HandleIncomingCert), not a
|
||||
// fast-follow guess. Best effort: the gossiping node's own finality is already
|
||||
// established by the verified cert.
|
||||
func (g *networkGossiper) GossipCert(chainID ids.ID, networkID ids.ID, blockID ids.ID, certBytes []byte) int {
|
||||
if g.net == nil || g.msgCreator == nil {
|
||||
return 0
|
||||
}
|
||||
envelope := encodeQuorumGossip(quorumKindCert, blockID, certBytes)
|
||||
msg, err := g.msgCreator.Gossip(chainID, envelope)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return g.net.Gossip(msg, nil, g.networkID, -1, 0, 0).Len()
|
||||
}
|
||||
|
||||
// GossipPut broadcasts a Put message with block data to validators.
|
||||
func (g *networkGossiper) GossipPut(chainID ids.ID, networkID ids.ID, blockData []byte) int {
|
||||
if g.net == nil || g.msgCreator == nil {
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package chains
|
||||
|
||||
import (
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/math/set"
|
||||
lux "github.com/luxfi/utxo"
|
||||
"github.com/luxfi/utxo/nftfx"
|
||||
)
|
||||
|
||||
// maxGatedActivationAttempts bounds how many times a gated chain may be
|
||||
// deferred waiting for the X-Chain to bootstrap before it is opted out. Each
|
||||
// X-Chain creation re-queues parked chains exactly once, so this also bounds
|
||||
// total re-queues per chain. It exists only as defense-in-depth against an
|
||||
// X-Chain that never bootstraps; the normal path defers zero or one time.
|
||||
const maxGatedActivationAttempts = 8
|
||||
|
||||
// NFTAuthorization identifies the X-Chain NFT a validator's staking address
|
||||
// must hold to activate (track/validate) a gated chain.
|
||||
//
|
||||
// A chain is "gated" when ManagerConfig.ChainAuthorizations has an entry for
|
||||
// its blockchain ID. Chains with no entry are ungated and always authorized —
|
||||
// so a nil/empty ChainAuthorizations map preserves today's behavior exactly
|
||||
// for every chain (P/C/X/Q/D/…).
|
||||
type NFTAuthorization struct {
|
||||
AssetID ids.ID // X-Chain nftfx asset (the collection)
|
||||
GroupID uint32 // nftfx group within the collection; 0 = any group
|
||||
}
|
||||
|
||||
// xChainUTXOReader is the narrow in-process accessor the gate needs from the
|
||||
// X-Chain VM: list the UTXOs owned by an address set. *xvm.VM satisfies it via
|
||||
// its GetUTXOs method (vms/xvm/vm.go), which wraps lux.GetAllUTXOs over the
|
||||
// chain's committed state. Keeping the interface to this one method avoids
|
||||
// importing the concrete xvm VM type into the chain manager.
|
||||
type xChainUTXOReader interface {
|
||||
GetUTXOs(addrs set.Set[ids.ShortID]) ([]*lux.UTXO, error)
|
||||
}
|
||||
|
||||
// holdsAuthorizationNFT is the pure authorization policy: it reports whether
|
||||
// [utxos] contains an nftfx transfer output that satisfies [auth]. A UTXO
|
||||
// matches when its output is an *nftfx.TransferOutput for auth.AssetID and,
|
||||
// when auth.GroupID is non-zero, its GroupID equals auth.GroupID (GroupID 0
|
||||
// means "any group in the collection").
|
||||
//
|
||||
// This is the one place the NFT-ownership rule lives. It takes plain values
|
||||
// (the fetched UTXOs and the requirement) and returns a decision, so it is
|
||||
// unit-testable without standing up a manager or an X-Chain.
|
||||
func holdsAuthorizationNFT(utxos []*lux.UTXO, auth NFTAuthorization) bool {
|
||||
for _, utxo := range utxos {
|
||||
out, ok := utxo.Out.(*nftfx.TransferOutput)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if utxo.AssetID() != auth.AssetID {
|
||||
continue
|
||||
}
|
||||
if auth.GroupID != 0 && out.GroupID != auth.GroupID {
|
||||
continue
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// authorizeChainActivation reports whether this node may activate
|
||||
// (track/validate) [chainID].
|
||||
//
|
||||
// Returns (authorized, ready):
|
||||
// - ready=false means the gate cannot decide yet because the X-Chain is not
|
||||
// bootstrapped and therefore not queryable in-process. The caller MUST
|
||||
// defer (re-attempt later), not skip — see createChain.
|
||||
// - ready=true, authorized=true: proceed normally.
|
||||
// - ready=true, authorized=false: clean opt-out — this validator's staking
|
||||
// X-address does not hold the required NFT.
|
||||
//
|
||||
// Decision order:
|
||||
// 1. Critical chains (P/C/X/…) are never gated — always (true, true).
|
||||
// 2. Ungated chains (no ChainAuthorizations entry) are always (true, true),
|
||||
// so the zero/nil map is a no-op for every chain.
|
||||
// 3. Gated chain, X-Chain not bootstrapped → (false, false): defer.
|
||||
// 4. Gated chain, X-Chain bootstrapped, but no usable in-process UTXO reader
|
||||
// or no resolvable staking X-address → (false, true): clean opt-out. The
|
||||
// node refuses to activate a gated chain it cannot prove authorization
|
||||
// for, rather than fail open.
|
||||
// 5. Otherwise query the X-Chain for the staking X-address's UTXOs and apply
|
||||
// holdsAuthorizationNFT.
|
||||
func (m *manager) authorizeChainActivation(chainID ids.ID) (authorized bool, ready bool) {
|
||||
// Critical chains are foundational; the gate must never skip or defer them.
|
||||
if m.CriticalChains.Contains(chainID) {
|
||||
return true, true
|
||||
}
|
||||
|
||||
// D-Chain participation is an operator opt-in (dex-validator), and it is a
|
||||
// NECESSARY condition that composes AND-wise with the NFT gate below: the
|
||||
// D-Chain activates only if the operator opted in AND (when configured) the
|
||||
// staking X-address holds the dex-operator NFT. Checked here — before the
|
||||
// ungated short-circuit — so the opt-in is required even when no operator
|
||||
// collection is configured for this network (the otherwise-ungated case).
|
||||
// A node without dex-validator therefore cleanly opts out of the D-Chain
|
||||
// even when --track-all-chains queued it: this is the activation authority,
|
||||
// not the platformvm tracking set. The opt-out is decided (ready=true), so
|
||||
// the caller takes the same clean skip as a non-held NFT / absent plugin.
|
||||
if m.DChainID != ids.Empty && chainID == m.DChainID && !m.DexValidator {
|
||||
m.Log.Info("D-Chain activation declined: dex-validator opt-in is disabled",
|
||||
log.Stringer("chainID", chainID),
|
||||
)
|
||||
return false, true
|
||||
}
|
||||
|
||||
auth, gated := m.ChainAuthorizations[chainID]
|
||||
if !gated {
|
||||
return true, true
|
||||
}
|
||||
|
||||
// TODO(phase-2): proof-of-possession. holdsAuthorizationNFT below checks
|
||||
// ownership-OF-RECORD (the staking X-address appears in an nftfx output's
|
||||
// owners) — it does NOT prove the node controls the key for that address.
|
||||
// Phase-2 must derive StakingXAddress from the node's staking keys and bind
|
||||
// the NFT check to key-control (a signed challenge), so a node cannot claim
|
||||
// authorization from an NFT held at an address it does not control.
|
||||
|
||||
// The gate consults the X-Chain UTXO set, so the X-Chain must already be
|
||||
// bootstrapped (created and tracked) on this node. If not, we cannot decide
|
||||
// yet — signal the caller to defer.
|
||||
if !m.IsBootstrapped(m.XChainID) {
|
||||
return false, false
|
||||
}
|
||||
|
||||
// The staking X-address must be wired for any gated chain. If it is empty,
|
||||
// the node has no identity to check NFT ownership against; refuse to
|
||||
// activate the gated chain (fail closed) rather than guess.
|
||||
if m.StakingXAddress == ids.ShortEmpty {
|
||||
m.Log.Warn("gated chain activation blocked: staking X-address not configured",
|
||||
log.Stringer("chainID", chainID),
|
||||
log.Stringer("assetID", auth.AssetID),
|
||||
)
|
||||
return false, true
|
||||
}
|
||||
|
||||
reader := m.xChainUTXOReader()
|
||||
if reader == nil {
|
||||
m.Log.Warn("gated chain activation blocked: X-Chain UTXO reader unavailable",
|
||||
log.Stringer("chainID", chainID),
|
||||
log.Stringer("assetID", auth.AssetID),
|
||||
)
|
||||
return false, true
|
||||
}
|
||||
|
||||
owners := set.Of(m.StakingXAddress)
|
||||
utxos, err := reader.GetUTXOs(owners)
|
||||
if err != nil {
|
||||
// A query error is not a "decline" — the X-Chain claims to be
|
||||
// bootstrapped but cannot answer. Defer and retry rather than
|
||||
// permanently opt out on a transient read failure.
|
||||
m.Log.Warn("gated chain activation deferred: X-Chain UTXO query failed",
|
||||
log.Stringer("chainID", chainID),
|
||||
log.Stringer("assetID", auth.AssetID),
|
||||
log.Err(err),
|
||||
)
|
||||
return false, false
|
||||
}
|
||||
|
||||
return holdsAuthorizationNFT(utxos, auth), true
|
||||
}
|
||||
|
||||
// xChainUTXOReader returns the in-process UTXO reader for the X-Chain, or nil
|
||||
// if the X-Chain VM is not present or does not expose the accessor. The
|
||||
// X-Chain VM (*xvm.VM) implements xChainUTXOReader; this asserts against the
|
||||
// narrow interface only.
|
||||
func (m *manager) xChainUTXOReader() xChainUTXOReader {
|
||||
m.chainsLock.Lock()
|
||||
info, ok := m.chains[m.XChainID]
|
||||
m.chainsLock.Unlock()
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
reader, _ := info.VM.(xChainUTXOReader)
|
||||
return reader
|
||||
}
|
||||
|
||||
// deferGatedChain parks [chainParams] out of the creation queue because the
|
||||
// X-Chain is not yet bootstrapped, and reports whether the chain may still be
|
||||
// retried. It returns false once the per-chain attempt cap is exhausted, in
|
||||
// which case the caller must opt the chain out (a never-bootstrapping X-Chain
|
||||
// must not park a chain forever). Parking out of the queue — rather than
|
||||
// re-pushing — is what keeps the single-threaded dispatch loop from
|
||||
// busy-spinning; retryPendingGatedChains re-pushes when the X-Chain is created.
|
||||
func (m *manager) deferGatedChain(chainParams ChainParameters) (retry bool) {
|
||||
m.gatedChainsLock.Lock()
|
||||
defer m.gatedChainsLock.Unlock()
|
||||
|
||||
m.gatedAttempts[chainParams.ID]++
|
||||
if m.gatedAttempts[chainParams.ID] > maxGatedActivationAttempts {
|
||||
return false
|
||||
}
|
||||
m.pendingGatedChains = append(m.pendingGatedChains, chainParams)
|
||||
return true
|
||||
}
|
||||
|
||||
// retryPendingGatedChains re-queues every chain parked by deferGatedChain. It
|
||||
// is called once right after the X-Chain finishes createChain, so the gate can
|
||||
// now reach the X-Chain UTXO set. Chains that still cannot decide will park
|
||||
// again (bounded by maxGatedActivationAttempts).
|
||||
func (m *manager) retryPendingGatedChains() {
|
||||
m.gatedChainsLock.Lock()
|
||||
parked := m.pendingGatedChains
|
||||
m.pendingGatedChains = nil
|
||||
m.gatedChainsLock.Unlock()
|
||||
|
||||
for _, chainParams := range parked {
|
||||
m.Log.Info("re-queuing gated chain after X-Chain bootstrap",
|
||||
log.Stringer("chainID", chainParams.ID),
|
||||
log.Stringer("vmID", chainParams.VMID),
|
||||
)
|
||||
m.chainsQueue.PushRight(chainParams)
|
||||
}
|
||||
}
|
||||
@@ -1,560 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package chains
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/luxfi/container/buffer"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/math/set"
|
||||
lux "github.com/luxfi/utxo"
|
||||
"github.com/luxfi/utxo/nftfx"
|
||||
"github.com/luxfi/utxo/secp256k1fx"
|
||||
)
|
||||
|
||||
// fakeUTXOReader is a stand-in for the X-Chain VM in unit tests. It returns a
|
||||
// fixed UTXO set (or an error) regardless of the requested addresses, which is
|
||||
// all the gate's address-set query needs to be driven deterministically.
|
||||
type fakeUTXOReader struct {
|
||||
utxos []*lux.UTXO
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *fakeUTXOReader) GetUTXOs(set.Set[ids.ShortID]) ([]*lux.UTXO, error) {
|
||||
return f.utxos, f.err
|
||||
}
|
||||
|
||||
// ownerScopedUTXOReader is a faithful X-Chain stand-in: it returns ONLY the
|
||||
// UTXOs whose nftfx/secp owners intersect the queried address set, exactly as
|
||||
// lux.GetAllUTXOs(vm.state, addrs) does over committed state. This is what the
|
||||
// fixed-list fakeUTXOReader cannot model — and it is required to test the
|
||||
// ownership-scoping invariant: a node may activate a gated chain only on an NFT
|
||||
// held at ITS OWN staking X-address, never one held at someone else's. The gate
|
||||
// queries set.Of(StakingXAddress), so any UTXO owned by a different address is
|
||||
// invisible here, just as it would be on a real X-Chain.
|
||||
type ownerScopedUTXOReader struct {
|
||||
utxos []*lux.UTXO
|
||||
}
|
||||
|
||||
func (r *ownerScopedUTXOReader) GetUTXOs(addrs set.Set[ids.ShortID]) ([]*lux.UTXO, error) {
|
||||
var out []*lux.UTXO
|
||||
for _, utxo := range r.utxos {
|
||||
owned, ok := utxo.Out.(lux.Addressable)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, raw := range owned.Addresses() {
|
||||
addr, err := ids.ToShortID(raw)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if addrs.Contains(addr) {
|
||||
out = append(out, utxo)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// nftUTXO builds a UTXO whose output is an nftfx transfer output for the given
|
||||
// asset and group. owner is the address recorded as the holder.
|
||||
func nftUTXO(assetID ids.ID, groupID uint32, owner ids.ShortID) *lux.UTXO {
|
||||
return &lux.UTXO{
|
||||
UTXOID: lux.UTXOID{TxID: ids.GenerateTestID()},
|
||||
Asset: lux.Asset{ID: assetID},
|
||||
Out: &nftfx.TransferOutput{
|
||||
GroupID: groupID,
|
||||
OutputOwners: secp256k1fx.OutputOwners{
|
||||
Threshold: 1,
|
||||
Addrs: []ids.ShortID{owner},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// secpUTXO builds a non-NFT (secp256k1 transfer) UTXO for the given asset, used
|
||||
// to prove the gate ignores outputs that are not nftfx transfers.
|
||||
func secpUTXO(assetID ids.ID, owner ids.ShortID) *lux.UTXO {
|
||||
return &lux.UTXO{
|
||||
UTXOID: lux.UTXOID{TxID: ids.GenerateTestID()},
|
||||
Asset: lux.Asset{ID: assetID},
|
||||
Out: &secp256k1fx.TransferOutput{
|
||||
Amt: 1,
|
||||
OutputOwners: secp256k1fx.OutputOwners{
|
||||
Threshold: 1,
|
||||
Addrs: []ids.ShortID{owner},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestHoldsAuthorizationNFT(t *testing.T) {
|
||||
asset := ids.GenerateTestID()
|
||||
other := ids.GenerateTestID()
|
||||
owner := ids.GenerateTestShortID()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
utxos []*lux.UTXO
|
||||
auth NFTAuthorization
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "empty set",
|
||||
utxos: nil,
|
||||
auth: NFTAuthorization{AssetID: asset},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "matching asset, any group",
|
||||
utxos: []*lux.UTXO{nftUTXO(asset, 7, owner)},
|
||||
auth: NFTAuthorization{AssetID: asset, GroupID: 0},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "matching asset and group",
|
||||
utxos: []*lux.UTXO{nftUTXO(asset, 7, owner)},
|
||||
auth: NFTAuthorization{AssetID: asset, GroupID: 7},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "matching asset, wrong group",
|
||||
utxos: []*lux.UTXO{nftUTXO(asset, 7, owner)},
|
||||
auth: NFTAuthorization{AssetID: asset, GroupID: 8},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "wrong asset",
|
||||
utxos: []*lux.UTXO{nftUTXO(other, 7, owner)},
|
||||
auth: NFTAuthorization{AssetID: asset},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "non-nft output for asset is ignored",
|
||||
utxos: []*lux.UTXO{secpUTXO(asset, owner)},
|
||||
auth: NFTAuthorization{AssetID: asset},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "match found among mixed outputs",
|
||||
utxos: []*lux.UTXO{
|
||||
secpUTXO(asset, owner),
|
||||
nftUTXO(other, 1, owner),
|
||||
nftUTXO(asset, 3, owner),
|
||||
},
|
||||
auth: NFTAuthorization{AssetID: asset, GroupID: 3},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
require.Equal(t, tt.want, holdsAuthorizationNFT(tt.utxos, tt.auth))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// newGateManager builds the minimal manager needed to exercise the gate:
|
||||
// only the fields authorizeChainActivation reads. xChainVM, when non-nil, is
|
||||
// installed as the X-Chain's tracked VM so IsBootstrapped(XChainID) is true and
|
||||
// xChainUTXOReader() resolves to it.
|
||||
func newGateManager(
|
||||
xChainID ids.ID,
|
||||
stakingAddr ids.ShortID,
|
||||
authz map[ids.ID]NFTAuthorization,
|
||||
critical set.Set[ids.ID],
|
||||
xChainVM xChainUTXOReader,
|
||||
) *manager {
|
||||
m := &manager{
|
||||
chains: make(map[ids.ID]*chainInfo),
|
||||
gatedAttempts: make(map[ids.ID]int),
|
||||
}
|
||||
m.Log = log.NewNoOpLogger()
|
||||
m.XChainID = xChainID
|
||||
m.StakingXAddress = stakingAddr
|
||||
m.ChainAuthorizations = authz
|
||||
m.CriticalChains = critical
|
||||
if xChainVM != nil {
|
||||
m.chains[xChainID] = &chainInfo{Name: "X-Chain", VM: xChainVM}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func TestAuthorizeChainActivation(t *testing.T) {
|
||||
xChainID := ids.GenerateTestID()
|
||||
gatedChain := ids.GenerateTestID()
|
||||
asset := ids.GenerateTestID()
|
||||
addr := ids.GenerateTestShortID()
|
||||
|
||||
collection := map[ids.ID]NFTAuthorization{
|
||||
gatedChain: {AssetID: asset, GroupID: 0},
|
||||
}
|
||||
|
||||
t.Run("ungated chain is authorized and ready", func(t *testing.T) {
|
||||
// No ChainAuthorizations entry: behaves exactly as today, even with no
|
||||
// X-Chain present. Covers P/C/X-like IDs via a table.
|
||||
m := newGateManager(xChainID, addr, nil, nil, nil)
|
||||
for _, id := range []ids.ID{ids.GenerateTestID(), xChainID, gatedChain} {
|
||||
authorized, ready := m.authorizeChainActivation(id)
|
||||
require.True(t, authorized, "id %s", id)
|
||||
require.True(t, ready, "id %s", id)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("gated chain, X-Chain not bootstrapped, defers", func(t *testing.T) {
|
||||
// xChainVM nil => XChainID not in m.chains => IsBootstrapped false.
|
||||
m := newGateManager(xChainID, addr, collection, nil, nil)
|
||||
authorized, ready := m.authorizeChainActivation(gatedChain)
|
||||
require.False(t, ready, "must signal defer")
|
||||
require.False(t, authorized)
|
||||
})
|
||||
|
||||
t.Run("gated chain, holder, authorized", func(t *testing.T) {
|
||||
reader := &fakeUTXOReader{utxos: []*lux.UTXO{nftUTXO(asset, 0, addr)}}
|
||||
m := newGateManager(xChainID, addr, collection, nil, reader)
|
||||
authorized, ready := m.authorizeChainActivation(gatedChain)
|
||||
require.True(t, ready)
|
||||
require.True(t, authorized)
|
||||
})
|
||||
|
||||
t.Run("gated chain, non-holder, clean opt-out", func(t *testing.T) {
|
||||
reader := &fakeUTXOReader{utxos: nil}
|
||||
m := newGateManager(xChainID, addr, collection, nil, reader)
|
||||
authorized, ready := m.authorizeChainActivation(gatedChain)
|
||||
require.True(t, ready, "decided, not deferred")
|
||||
require.False(t, authorized, "no NFT => opt out")
|
||||
})
|
||||
|
||||
t.Run("group match vs mismatch", func(t *testing.T) {
|
||||
groupCollection := map[ids.ID]NFTAuthorization{
|
||||
gatedChain: {AssetID: asset, GroupID: 5},
|
||||
}
|
||||
hold5 := &fakeUTXOReader{utxos: []*lux.UTXO{nftUTXO(asset, 5, addr)}}
|
||||
hold9 := &fakeUTXOReader{utxos: []*lux.UTXO{nftUTXO(asset, 9, addr)}}
|
||||
|
||||
authorized, ready := newGateManager(xChainID, addr, groupCollection, nil, hold5).
|
||||
authorizeChainActivation(gatedChain)
|
||||
require.True(t, ready)
|
||||
require.True(t, authorized, "group 5 held => authorized")
|
||||
|
||||
authorized, ready = newGateManager(xChainID, addr, groupCollection, nil, hold9).
|
||||
authorizeChainActivation(gatedChain)
|
||||
require.True(t, ready)
|
||||
require.False(t, authorized, "wrong group => opt out")
|
||||
})
|
||||
|
||||
t.Run("critical chain never skipped even if gated", func(t *testing.T) {
|
||||
// gatedChain is in BOTH ChainAuthorizations and CriticalChains, the
|
||||
// holder holds nothing, and the X-Chain is absent. A non-critical chain
|
||||
// here would defer (not ready); a critical chain must be authorized.
|
||||
critical := set.Of(gatedChain)
|
||||
m := newGateManager(xChainID, addr, collection, critical, nil)
|
||||
authorized, ready := m.authorizeChainActivation(gatedChain)
|
||||
require.True(t, authorized, "critical chain must never be gated")
|
||||
require.True(t, ready, "critical chain must never defer")
|
||||
})
|
||||
|
||||
t.Run("gated chain, empty staking address, fails closed", func(t *testing.T) {
|
||||
reader := &fakeUTXOReader{utxos: []*lux.UTXO{nftUTXO(asset, 0, addr)}}
|
||||
m := newGateManager(xChainID, ids.ShortEmpty, collection, nil, reader)
|
||||
authorized, ready := m.authorizeChainActivation(gatedChain)
|
||||
require.True(t, ready, "decided")
|
||||
require.False(t, authorized, "no staking identity => opt out")
|
||||
})
|
||||
|
||||
t.Run("gated chain, X-Chain query error, defers", func(t *testing.T) {
|
||||
reader := &fakeUTXOReader{err: errors.New("state not ready")}
|
||||
m := newGateManager(xChainID, addr, collection, nil, reader)
|
||||
authorized, ready := m.authorizeChainActivation(gatedChain)
|
||||
require.False(t, ready, "transient query error => retry, not opt out")
|
||||
require.False(t, authorized)
|
||||
})
|
||||
|
||||
t.Run("gated chain, X-Chain tracked but VM lacks reader, opts out", func(t *testing.T) {
|
||||
// X-Chain is in m.chains (IsBootstrapped true) but its VM does not
|
||||
// satisfy xChainUTXOReader. The gate must fail closed (ready, !authz),
|
||||
// not panic.
|
||||
m := newGateManager(xChainID, addr, collection, nil, nil)
|
||||
m.chains[xChainID] = &chainInfo{Name: "X-Chain", VM: struct{}{}}
|
||||
authorized, ready := m.authorizeChainActivation(gatedChain)
|
||||
require.True(t, ready)
|
||||
require.False(t, authorized)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeferGatedChainCap(t *testing.T) {
|
||||
xChainID := ids.GenerateTestID()
|
||||
gatedChain := ids.GenerateTestID()
|
||||
m := newGateManager(xChainID, ids.GenerateTestShortID(), nil, nil, nil)
|
||||
|
||||
params := ChainParameters{ID: gatedChain, VMID: ids.GenerateTestID()}
|
||||
|
||||
// The first maxGatedActivationAttempts calls park the chain and allow retry.
|
||||
for i := 0; i < maxGatedActivationAttempts; i++ {
|
||||
require.True(t, m.deferGatedChain(params), "attempt %d should allow retry", i+1)
|
||||
}
|
||||
// One past the cap opts out instead of parking again.
|
||||
require.False(t, m.deferGatedChain(params), "cap exhausted")
|
||||
|
||||
// Every allowed attempt parked exactly one entry; the over-cap attempt did not.
|
||||
m.gatedChainsLock.Lock()
|
||||
require.Len(t, m.pendingGatedChains, maxGatedActivationAttempts)
|
||||
m.gatedChainsLock.Unlock()
|
||||
}
|
||||
|
||||
func TestRetryPendingGatedChainsRequeues(t *testing.T) {
|
||||
xChainID := ids.GenerateTestID()
|
||||
m := newGateManager(xChainID, ids.GenerateTestShortID(), nil, nil, nil)
|
||||
m.chainsQueue = buffer.NewUnboundedBlockingDeque[ChainParameters](initialQueueSize)
|
||||
|
||||
parked := ChainParameters{ID: ids.GenerateTestID(), VMID: ids.GenerateTestID()}
|
||||
require.True(t, m.deferGatedChain(parked))
|
||||
|
||||
m.retryPendingGatedChains()
|
||||
|
||||
// Parked list is drained and the chain is re-pushed onto the creation queue.
|
||||
m.gatedChainsLock.Lock()
|
||||
require.Empty(t, m.pendingGatedChains)
|
||||
m.gatedChainsLock.Unlock()
|
||||
|
||||
got, ok := m.chainsQueue.PopLeft()
|
||||
require.True(t, ok)
|
||||
require.Equal(t, parked.ID, got.ID)
|
||||
}
|
||||
|
||||
// dexGatedManager builds a manager whose D-Chain (dChainID) is NFT-gated on a
|
||||
// synthetic dex-operator collection — exactly the ChainAuthorizations shape
|
||||
// node.chainAuthorizationsFor produces for dexvm: dChainID -> {dexAsset,
|
||||
// group 0}. reader is installed as the bootstrapped X-Chain so the gate can
|
||||
// query the staking address's UTXOs. This is the chain-manager-side mirror of
|
||||
// the OptionalVMs[DexVMID].RequiredNFT policy under a synthetic per-network
|
||||
// asset.
|
||||
func dexGatedManager(dexAsset ids.ID, staking ids.ShortID, reader xChainUTXOReader) (*manager, ids.ID) {
|
||||
xChainID := ids.GenerateTestID()
|
||||
dChainID := ids.GenerateTestID()
|
||||
authz := map[ids.ID]NFTAuthorization{
|
||||
dChainID: {AssetID: dexAsset, GroupID: 0}, // group 0 = any group in the collection
|
||||
}
|
||||
m := newGateManager(xChainID, staking, authz, nil, reader)
|
||||
// Wire the D-Chain identity and turn the operator opt-in ON, so these NFT
|
||||
// tests exercise the REAL dex-validator path: dex-validator=true, then the
|
||||
// NFT gate decides. (The dex-validator=false short-circuit is proven
|
||||
// separately in TestDexValidatorFlagGatesDChain.)
|
||||
m.DChainID = dChainID
|
||||
m.DexValidator = true
|
||||
return m, dChainID
|
||||
}
|
||||
|
||||
// TestDexVMFailsClosedWithoutXNFT (#4). A node whose staking X-address holds NO
|
||||
// dex-operator NFT is NOT authorized to activate the D-Chain — a clean
|
||||
// fail-closed opt-out (ready=true, authorized=false), not a fail-open and not a
|
||||
// deferral. The X-Chain is bootstrapped and answers; the address simply holds
|
||||
// nothing. Also covers holding the WRONG asset (some other NFT) → still opt-out.
|
||||
func TestDexVMFailsClosedWithoutXNFT(t *testing.T) {
|
||||
dexAsset := ids.GenerateTestID()
|
||||
otherAsset := ids.GenerateTestID()
|
||||
staking := ids.GenerateTestShortID()
|
||||
|
||||
t.Run("no NFT held => opt out", func(t *testing.T) {
|
||||
reader := &fakeUTXOReader{utxos: nil}
|
||||
m, dChainID := dexGatedManager(dexAsset, staking, reader)
|
||||
authorized, ready := m.authorizeChainActivation(dChainID)
|
||||
require.True(t, ready, "X-Chain answered => decided, not deferred")
|
||||
require.False(t, authorized, "no dex-operator NFT => D-Chain activation fails closed")
|
||||
})
|
||||
|
||||
t.Run("wrong collection held => opt out", func(t *testing.T) {
|
||||
reader := &fakeUTXOReader{utxos: []*lux.UTXO{nftUTXO(otherAsset, 0, staking)}}
|
||||
m, dChainID := dexGatedManager(dexAsset, staking, reader)
|
||||
authorized, ready := m.authorizeChainActivation(dChainID)
|
||||
require.True(t, ready)
|
||||
require.False(t, authorized, "holding a different collection's NFT must not authorize the D-Chain")
|
||||
})
|
||||
|
||||
t.Run("no staking identity => fail closed", func(t *testing.T) {
|
||||
// Even with a matching NFT in the set, an empty staking X-address has no
|
||||
// identity to check ownership against → fail closed.
|
||||
reader := &fakeUTXOReader{utxos: []*lux.UTXO{nftUTXO(dexAsset, 0, staking)}}
|
||||
m, dChainID := dexGatedManager(dexAsset, ids.ShortEmpty, reader)
|
||||
authorized, ready := m.authorizeChainActivation(dChainID)
|
||||
require.True(t, ready)
|
||||
require.False(t, authorized, "no staking identity => fail closed")
|
||||
})
|
||||
}
|
||||
|
||||
// TestDexVMActivatesWithDexOperatorNFT (#5). The same gated D-Chain, but the
|
||||
// staking X-address holds the dex-operator NFT → authorized to activate.
|
||||
// Proves the positive path of the NFT-governed activation.
|
||||
func TestDexVMActivatesWithDexOperatorNFT(t *testing.T) {
|
||||
dexAsset := ids.GenerateTestID()
|
||||
staking := ids.GenerateTestShortID()
|
||||
|
||||
t.Run("dex-operator NFT held => authorized", func(t *testing.T) {
|
||||
reader := &fakeUTXOReader{utxos: []*lux.UTXO{nftUTXO(dexAsset, 0, staking)}}
|
||||
m, dChainID := dexGatedManager(dexAsset, staking, reader)
|
||||
authorized, ready := m.authorizeChainActivation(dChainID)
|
||||
require.True(t, ready)
|
||||
require.True(t, authorized, "holding the dex-operator NFT authorizes D-Chain activation")
|
||||
})
|
||||
|
||||
t.Run("dex-operator NFT among mixed UTXOs => authorized", func(t *testing.T) {
|
||||
other := ids.GenerateTestID()
|
||||
reader := &fakeUTXOReader{utxos: []*lux.UTXO{
|
||||
secpUTXO(dexAsset, staking), // non-NFT output for the asset: ignored
|
||||
nftUTXO(other, 1, staking), // wrong collection: ignored
|
||||
nftUTXO(dexAsset, 4, staking), // the dex-operator NFT (group 4, any-group gate)
|
||||
}}
|
||||
m, dChainID := dexGatedManager(dexAsset, staking, reader)
|
||||
authorized, ready := m.authorizeChainActivation(dChainID)
|
||||
require.True(t, ready)
|
||||
require.True(t, authorized, "dex-operator NFT present among other UTXOs => authorized")
|
||||
})
|
||||
}
|
||||
|
||||
// TestDexVMOwnershipScoping (H1 gap). The activation NFT must be held at the
|
||||
// node's OWN staking X-address — not merely exist somewhere on the X-Chain. The
|
||||
// gate queries set.Of(StakingXAddress), so an ownership-honoring reader returns
|
||||
// nothing for an NFT minted to a DIFFERENT address, and the gate fails closed.
|
||||
// The fixed-list fakeUTXOReader could not catch this (it returns its list for
|
||||
// any query); ownerScopedUTXOReader models real per-address X-Chain state.
|
||||
//
|
||||
// NOTE: this proves ownership-OF-RECORD scoping (the NFT is owned by this
|
||||
// address). It does NOT prove the node controls that address's key — that is
|
||||
// the // TODO(phase-2): proof-of-possession seam in authorizeChainActivation,
|
||||
// out of scope this pass.
|
||||
func TestDexVMOwnershipScoping(t *testing.T) {
|
||||
dexAsset := ids.GenerateTestID()
|
||||
nodeAddr := ids.GenerateTestShortID() // this node's staking X-address
|
||||
strangerAddr := ids.GenerateTestShortID() // a DIFFERENT operator's address
|
||||
|
||||
t.Run("dex-operator NFT held by a DIFFERENT address => not authorized", func(t *testing.T) {
|
||||
// The collection's NFT exists, but it is owned by strangerAddr. An
|
||||
// ownership-honoring X-Chain returns no UTXOs for nodeAddr's query.
|
||||
reader := &ownerScopedUTXOReader{utxos: []*lux.UTXO{nftUTXO(dexAsset, 0, strangerAddr)}}
|
||||
m, dChainID := dexGatedManager(dexAsset, nodeAddr, reader)
|
||||
authorized, ready := m.authorizeChainActivation(dChainID)
|
||||
require.True(t, ready, "X-Chain answered => decided, not deferred")
|
||||
require.False(t, authorized, "an NFT held at another address must NOT authorize this node")
|
||||
})
|
||||
|
||||
t.Run("dex-operator NFT held at the node's OWN address => authorized", func(t *testing.T) {
|
||||
// Same collection, but the NFT (alongside the stranger's) is also held by
|
||||
// nodeAddr. Only the node's own holding authorizes it.
|
||||
reader := &ownerScopedUTXOReader{utxos: []*lux.UTXO{
|
||||
nftUTXO(dexAsset, 0, strangerAddr), // not ours: invisible to our query
|
||||
nftUTXO(dexAsset, 0, nodeAddr), // ours: authorizes
|
||||
}}
|
||||
m, dChainID := dexGatedManager(dexAsset, nodeAddr, reader)
|
||||
authorized, ready := m.authorizeChainActivation(dChainID)
|
||||
require.True(t, ready)
|
||||
require.True(t, authorized, "an NFT held at the node's own staking X-address authorizes it")
|
||||
})
|
||||
|
||||
t.Run("only the stranger's NFT exists, queried by stranger => authorized for stranger only", func(t *testing.T) {
|
||||
// Sanity: the same reader DOES authorize the address that actually holds
|
||||
// the NFT, proving the scoping is by-address and not a blanket deny.
|
||||
reader := &ownerScopedUTXOReader{utxos: []*lux.UTXO{nftUTXO(dexAsset, 0, strangerAddr)}}
|
||||
m, dChainID := dexGatedManager(dexAsset, strangerAddr, reader)
|
||||
authorized, ready := m.authorizeChainActivation(dChainID)
|
||||
require.True(t, ready)
|
||||
require.True(t, authorized, "the holder address is authorized; scoping is per-address, not blanket")
|
||||
})
|
||||
}
|
||||
|
||||
// TestDexValidatorFlagGatesDChain (M1). The dex-validator opt-in is a genuine,
|
||||
// NECESSARY activation condition for the D-Chain — not just a log line — and it
|
||||
// composes AND-wise with the NFT gate. This proves the full truth table at the
|
||||
// activation authority (authorizeChainActivation), which is downstream of how a
|
||||
// chain gets QUEUED: --track-all-chains queues the D-Chain in platformvm
|
||||
// (CreateChain), but the manager's gate is what ACTIVATES it. So a manager with
|
||||
// DexValidator=false standing in for a --track-all-chains node MUST decline the
|
||||
// D-Chain regardless of NFT state.
|
||||
func TestDexValidatorFlagGatesDChain(t *testing.T) {
|
||||
xChainID := ids.GenerateTestID()
|
||||
dChainID := ids.GenerateTestID()
|
||||
dexAsset := ids.GenerateTestID()
|
||||
staking := ids.GenerateTestShortID()
|
||||
|
||||
// withFlag builds a D-Chain manager with the dex-validator flag set to
|
||||
// [dexValidator]. When [gated] the D-Chain is NFT-gated on dexAsset and the
|
||||
// reader is the bootstrapped X-Chain; when not gated, ChainAuthorizations is
|
||||
// empty (the --track-all-chains, no-operator-collection case).
|
||||
withFlag := func(dexValidator, gated bool, reader xChainUTXOReader) *manager {
|
||||
var authz map[ids.ID]NFTAuthorization
|
||||
if gated {
|
||||
authz = map[ids.ID]NFTAuthorization{dChainID: {AssetID: dexAsset, GroupID: 0}}
|
||||
}
|
||||
m := newGateManager(xChainID, staking, authz, nil, reader)
|
||||
m.DChainID = dChainID
|
||||
m.DexValidator = dexValidator
|
||||
return m
|
||||
}
|
||||
holdsNFT := func() xChainUTXOReader {
|
||||
return &fakeUTXOReader{utxos: []*lux.UTXO{nftUTXO(dexAsset, 0, staking)}}
|
||||
}
|
||||
|
||||
t.Run("flag=false + ungated (track-all-chains) => D NOT activated", func(t *testing.T) {
|
||||
// The headline M1 case: even with no operator collection configured (so
|
||||
// the chain would otherwise be ungated and activate under --track-all),
|
||||
// dex-validator=false declines the D-Chain. Decided opt-out, not defer.
|
||||
m := withFlag(false, false, nil)
|
||||
authorized, ready := m.authorizeChainActivation(dChainID)
|
||||
require.True(t, ready, "decided opt-out, not a deferral")
|
||||
require.False(t, authorized, "dex-validator=false must block the D-Chain even under --track-all-chains")
|
||||
})
|
||||
|
||||
t.Run("flag=false + NFT held => still NOT activated (flag is necessary)", func(t *testing.T) {
|
||||
// Holding the dex-operator NFT does not rescue activation when the flag
|
||||
// is off: the flag is an independent necessary condition (the AND).
|
||||
m := withFlag(false, true, holdsNFT())
|
||||
authorized, ready := m.authorizeChainActivation(dChainID)
|
||||
require.True(t, ready)
|
||||
require.False(t, authorized, "flag=false blocks D even with the NFT held")
|
||||
})
|
||||
|
||||
t.Run("flag=true + NFT held => activated", func(t *testing.T) {
|
||||
// Both conditions met: opted in AND holds the NFT.
|
||||
m := withFlag(true, true, holdsNFT())
|
||||
authorized, ready := m.authorizeChainActivation(dChainID)
|
||||
require.True(t, ready)
|
||||
require.True(t, authorized, "dex-validator=true AND NFT held => D-Chain activates")
|
||||
})
|
||||
|
||||
t.Run("flag=true + gated but NFT NOT held => NOT activated (NFT is necessary)", func(t *testing.T) {
|
||||
// The other half of the AND: opting in does not bypass the NFT gate.
|
||||
m := withFlag(true, true, &fakeUTXOReader{utxos: nil})
|
||||
authorized, ready := m.authorizeChainActivation(dChainID)
|
||||
require.True(t, ready)
|
||||
require.False(t, authorized, "flag=true but no NFT => still blocked")
|
||||
})
|
||||
|
||||
t.Run("flag=true + ungated (no operator collection) => activated", func(t *testing.T) {
|
||||
// Opted in and no NFT requirement configured for this network: the flag
|
||||
// alone governs and the D-Chain activates (preserves today's opt-in
|
||||
// behavior for the in-flag operator).
|
||||
m := withFlag(true, false, nil)
|
||||
authorized, ready := m.authorizeChainActivation(dChainID)
|
||||
require.True(t, ready)
|
||||
require.True(t, authorized, "dex-validator=true with no NFT configured => activates")
|
||||
})
|
||||
|
||||
t.Run("flag=false does not affect a non-D gated chain", func(t *testing.T) {
|
||||
// The flag is D-specific: another gated chain (e.g. bridgevm) is governed
|
||||
// solely by its NFT gate, untouched by dex-validator.
|
||||
otherChain := ids.GenerateTestID()
|
||||
authz := map[ids.ID]NFTAuthorization{otherChain: {AssetID: dexAsset, GroupID: 0}}
|
||||
m := newGateManager(xChainID, staking, authz, nil, holdsNFT())
|
||||
m.DChainID = dChainID
|
||||
m.DexValidator = false // off, but irrelevant to otherChain
|
||||
authorized, ready := m.authorizeChainActivation(otherChain)
|
||||
require.True(t, ready)
|
||||
require.True(t, authorized, "dex-validator must not gate a non-D chain")
|
||||
})
|
||||
}
|
||||
@@ -10,13 +10,13 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/luxfi/vm"
|
||||
"github.com/luxfi/constants"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/metric"
|
||||
"github.com/luxfi/node/nets"
|
||||
"github.com/luxfi/node/vms"
|
||||
"github.com/luxfi/vm"
|
||||
)
|
||||
|
||||
// TestNew tests creating a new manager
|
||||
@@ -106,9 +106,9 @@ func TestQueueChainCreation(t *testing.T) {
|
||||
chainID := ids.GenerateTestID()
|
||||
netID := ids.GenerateTestID()
|
||||
chainParams := ChainParameters{
|
||||
ID: chainID,
|
||||
ID: chainID,
|
||||
ChainID: netID,
|
||||
VMID: ids.GenerateTestID(),
|
||||
VMID: ids.GenerateTestID(),
|
||||
}
|
||||
|
||||
// Queue the chain
|
||||
|
||||
@@ -1,591 +0,0 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// pchain_height_finality_test.go — the b2 load-bearing proof the prior round
|
||||
// lacked: that the node delivers the REAL P-chain epoch height to the chain
|
||||
// engine, so a K>1 quorum chain finalizes against the LIVE validator set — not
|
||||
// the frozen genesis set.
|
||||
//
|
||||
// The consensus-layer TestPChainEpochFinality_RealWiring proved the engine reads
|
||||
// the RIGHT height GIVEN a block that already exposes one; it fed a synthetic
|
||||
// block carrying PChainHeight directly, BYPASSING the boundary. The boundary —
|
||||
// where a bare plugin block yields pChainHeightOf==0 — was exactly what shipped
|
||||
// broken (set@0 = genesis). These tests drive the REAL boundary:
|
||||
//
|
||||
// inner VM (bare block, no PChainHeight)
|
||||
// └─ pChainHeightVM (the b2 wrapper, backed by a real validators.State)
|
||||
// └─ consensus engine (real α-of-K cert finality, node BLS sources)
|
||||
//
|
||||
// and prove three properties end to end:
|
||||
//
|
||||
// (1) pChainHeightOf(realBlock) returns the wrapper's stamped P-chain height
|
||||
// (NOT 0) — at BuildBlock AND after a ParseBlock round-trip of the gossiped
|
||||
// bytes (the determinism guarantee: every node recovers the same height).
|
||||
// (2) K>1 FINALIZES at genesis (set@H0).
|
||||
// (3) K>1 FINALIZES AFTER a staking change — validators that JOINED post-genesis
|
||||
// cast the deciding votes+stake. This is the case that STALLS on the set@0
|
||||
// path (the joiners are absent from the genesis set), so finalizing proves
|
||||
// the real height is load-bearing.
|
||||
//
|
||||
// CGO-free: the node BLS sources use the pure-Go BLS path under CGO_ENABLED=0, so
|
||||
// this runs the ACTUAL production quorum sources (blsVoteVerifier / blsVoteSigner
|
||||
// / validatorStakeSource / validatorSetRootSource) — not an ed25519 stand-in.
|
||||
package chains
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
consensusconfig "github.com/luxfi/consensus/config"
|
||||
consensuschain "github.com/luxfi/consensus/engine/chain"
|
||||
"github.com/luxfi/consensus/engine/chain/block"
|
||||
"github.com/luxfi/crypto/bls"
|
||||
"github.com/luxfi/ids"
|
||||
validators "github.com/luxfi/validators"
|
||||
"github.com/luxfi/validators/validatorstest"
|
||||
)
|
||||
|
||||
// --- a bare inner VM whose blocks carry NO P-chain height --------------------
|
||||
|
||||
// fakeInnerBlock is a minimal chain block: it satisfies block.Block but does NOT
|
||||
// expose PChainHeight() — exactly like the plugin VM blocks (C-Chain EVM, dexvm)
|
||||
// the node runs. Its Bytes() is its own opaque encoding; the wrapper frames a
|
||||
// P-chain height AROUND these bytes for transport.
|
||||
type fakeInnerBlock struct {
|
||||
id ids.ID
|
||||
parentID ids.ID
|
||||
height uint64
|
||||
bytes []byte
|
||||
timestamp time.Time
|
||||
|
||||
mu sync.Mutex
|
||||
acceptCalled int
|
||||
}
|
||||
|
||||
func (b *fakeInnerBlock) ID() ids.ID { return b.id }
|
||||
func (b *fakeInnerBlock) Parent() ids.ID { return b.parentID }
|
||||
func (b *fakeInnerBlock) ParentID() ids.ID { return b.parentID }
|
||||
func (b *fakeInnerBlock) Height() uint64 { return b.height }
|
||||
func (b *fakeInnerBlock) Timestamp() time.Time { return b.timestamp }
|
||||
func (b *fakeInnerBlock) Status() uint8 { return 0 }
|
||||
func (b *fakeInnerBlock) Bytes() []byte { return b.bytes }
|
||||
func (b *fakeInnerBlock) Verify(context.Context) error { return nil }
|
||||
func (b *fakeInnerBlock) Reject(context.Context) error { return nil }
|
||||
func (b *fakeInnerBlock) Accept(context.Context) error {
|
||||
b.mu.Lock()
|
||||
b.acceptCalled++
|
||||
b.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
func (b *fakeInnerBlock) accepted() int {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.acceptCalled
|
||||
}
|
||||
|
||||
// fakeInnerVM is a BlockBuilder over fakeInnerBlocks, keyed by id and by bytes so
|
||||
// ParseBlock(bytes) reconstructs the SAME inner block on a follower. It builds one
|
||||
// block on demand (set via stage) so the test controls the proposed block.
|
||||
type fakeInnerVM struct {
|
||||
mu sync.Mutex
|
||||
byID map[ids.ID]*fakeInnerBlock
|
||||
byBytes map[string]*fakeInnerBlock
|
||||
staged *fakeInnerBlock // returned by the next BuildBlock
|
||||
lastAcc ids.ID
|
||||
}
|
||||
|
||||
func newFakeInnerVM() *fakeInnerVM {
|
||||
return &fakeInnerVM{
|
||||
byID: make(map[ids.ID]*fakeInnerBlock),
|
||||
byBytes: make(map[string]*fakeInnerBlock),
|
||||
}
|
||||
}
|
||||
|
||||
func (vm *fakeInnerVM) register(b *fakeInnerBlock) {
|
||||
vm.mu.Lock()
|
||||
vm.byID[b.id] = b
|
||||
vm.byBytes[string(b.bytes)] = b
|
||||
vm.mu.Unlock()
|
||||
}
|
||||
|
||||
// stage sets the block the next BuildBlock returns (and registers it for Get/Parse).
|
||||
func (vm *fakeInnerVM) stage(b *fakeInnerBlock) {
|
||||
vm.register(b)
|
||||
vm.mu.Lock()
|
||||
vm.staged = b
|
||||
vm.mu.Unlock()
|
||||
}
|
||||
|
||||
func (vm *fakeInnerVM) BuildBlock(context.Context) (block.Block, error) {
|
||||
vm.mu.Lock()
|
||||
defer vm.mu.Unlock()
|
||||
return vm.staged, nil
|
||||
}
|
||||
|
||||
func (vm *fakeInnerVM) ParseBlock(_ context.Context, b []byte) (block.Block, error) {
|
||||
vm.mu.Lock()
|
||||
defer vm.mu.Unlock()
|
||||
if blk, ok := vm.byBytes[string(b)]; ok {
|
||||
return blk, nil
|
||||
}
|
||||
// Unknown bytes: synthesize a block so a follower can still parse. Real VMs
|
||||
// decode deterministically; the test pre-registers every block it gossips, so
|
||||
// this path is only a safety net.
|
||||
return nil, errUnknownInnerBytes
|
||||
}
|
||||
|
||||
func (vm *fakeInnerVM) GetBlock(_ context.Context, id ids.ID) (block.Block, error) {
|
||||
vm.mu.Lock()
|
||||
defer vm.mu.Unlock()
|
||||
if blk, ok := vm.byID[id]; ok {
|
||||
return blk, nil
|
||||
}
|
||||
return nil, errUnknownInnerBytes
|
||||
}
|
||||
|
||||
func (vm *fakeInnerVM) LastAccepted(context.Context) (ids.ID, error) {
|
||||
vm.mu.Lock()
|
||||
defer vm.mu.Unlock()
|
||||
return vm.lastAcc, nil
|
||||
}
|
||||
|
||||
func (vm *fakeInnerVM) SetPreference(_ context.Context, id ids.ID) error {
|
||||
vm.mu.Lock()
|
||||
vm.lastAcc = id
|
||||
vm.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
var errUnknownInnerBytes = errInnerBytes{}
|
||||
|
||||
type errInnerBytes struct{}
|
||||
|
||||
func (errInnerBytes) Error() string { return "fakeInnerVM: unknown block bytes" }
|
||||
|
||||
// --- BLS validator material (pure-Go BLS under CGO_ENABLED=0) ----------------
|
||||
|
||||
type blsValidator struct {
|
||||
nodeID ids.NodeID
|
||||
sk *bls.SecretKey
|
||||
pkComp []byte
|
||||
light uint64
|
||||
}
|
||||
|
||||
func newBLSValidator(t *testing.T, weight uint64) blsValidator {
|
||||
t.Helper()
|
||||
sk, err := bls.NewSecretKey()
|
||||
if err != nil {
|
||||
t.Fatalf("bls.NewSecretKey: %v", err)
|
||||
}
|
||||
return blsValidator{
|
||||
nodeID: ids.GenerateTestNodeID(),
|
||||
sk: sk,
|
||||
pkComp: bls.PublicKeyToCompressedBytes(sk.PublicKey()),
|
||||
light: weight,
|
||||
}
|
||||
}
|
||||
|
||||
func (v blsValidator) out() *validators.GetValidatorOutput {
|
||||
return &validators.GetValidatorOutput{
|
||||
NodeID: v.nodeID,
|
||||
PublicKey: v.pkComp,
|
||||
Light: v.light,
|
||||
Weight: v.light,
|
||||
}
|
||||
}
|
||||
|
||||
// stateByHeight builds a height-indexed validators.State reporting the given sets
|
||||
// per height (empty for unknown heights / wrong net), with GetCurrentHeight fixed
|
||||
// to `current` so the wrapper stamps that height onto built blocks.
|
||||
func stateByHeight(netID ids.ID, current uint64, byHeight map[uint64][]blsValidator) *validatorstest.TestState {
|
||||
s := validatorstest.NewTestState()
|
||||
s.GetCurrentHeightF = func(context.Context) (uint64, error) { return current, nil }
|
||||
s.GetValidatorSetF = func(_ context.Context, height uint64, gotNet ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
|
||||
if gotNet != netID {
|
||||
return map[ids.NodeID]*validators.GetValidatorOutput{}, nil
|
||||
}
|
||||
out := make(map[ids.NodeID]*validators.GetValidatorOutput)
|
||||
for _, v := range byHeight[height] {
|
||||
out[v.nodeID] = v.out()
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// --- a recording cert gossiper (engine CertGossiper) -------------------------
|
||||
|
||||
type recordingCertGossiper struct {
|
||||
mu sync.Mutex
|
||||
certs [][]byte
|
||||
}
|
||||
|
||||
func (g *recordingCertGossiper) GossipCert(_ ids.ID, _ ids.ID, certBytes []byte) error {
|
||||
g.mu.Lock()
|
||||
g.certs = append(g.certs, append([]byte(nil), certBytes...))
|
||||
g.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *recordingCertGossiper) count() int {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
return len(g.certs)
|
||||
}
|
||||
|
||||
var _ consensuschain.CertGossiper = (*recordingCertGossiper)(nil)
|
||||
|
||||
// --- helpers -----------------------------------------------------------------
|
||||
|
||||
func params5() consensusconfig.Parameters {
|
||||
return consensusconfig.Parameters{K: 5, AlphaPreference: 3, AlphaConfidence: 3, Beta: 2}
|
||||
}
|
||||
|
||||
func waitForCond(d time.Duration, cond func() bool) bool {
|
||||
deadline := time.Now().Add(d)
|
||||
for time.Now().Before(deadline) {
|
||||
if cond() {
|
||||
return true
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
return cond()
|
||||
}
|
||||
|
||||
// signBLSVote produces validator v's signed accept Vote for a position (the same
|
||||
// canonical message the node's blsVoteSigner/Verifier use).
|
||||
func signBLSVote(t *testing.T, v blsValidator, pos consensuschain.VotePosition) consensuschain.Vote {
|
||||
t.Helper()
|
||||
sig, err := v.sk.Sign(consensuschain.CanonicalVoteMessage(pos))
|
||||
if err != nil {
|
||||
t.Fatalf("sign vote: %v", err)
|
||||
}
|
||||
return consensuschain.Vote{
|
||||
BlockID: pos.BlockID,
|
||||
NodeID: v.nodeID,
|
||||
Accept: true,
|
||||
SignedAt: time.Now(),
|
||||
Signature: bls.SignatureToBytes(sig),
|
||||
ParentID: pos.ParentID,
|
||||
Round: pos.Round,
|
||||
}
|
||||
}
|
||||
|
||||
// quorumEngineFixture wires a real consensus engine with the node's production
|
||||
// BLS quorum sources, all height-pinned to a height-indexed validators.State,
|
||||
// driving blocks through the b2 pChainHeightVM wrapper over a bare inner VM.
|
||||
type quorumEngineFixture struct {
|
||||
engine *consensuschain.Transitive
|
||||
wrapper *pChainHeightVM
|
||||
inner *fakeInnerVM
|
||||
chainID ids.ID
|
||||
netID ids.ID
|
||||
proposer blsValidator
|
||||
certs *recordingCertGossiper
|
||||
}
|
||||
|
||||
func newQuorumEngineFixture(t *testing.T, netID ids.ID, state validators.State, proposer blsValidator, byHeight map[uint64][]blsValidator) *quorumEngineFixture {
|
||||
t.Helper()
|
||||
inner := newFakeInnerVM()
|
||||
wrapper := newPChainHeightVM(inner, state, netID)
|
||||
|
||||
chainID := ids.GenerateTestID()
|
||||
certs := &recordingCertGossiper{}
|
||||
|
||||
vdrState := state
|
||||
engine := consensuschain.NewWithConfig(
|
||||
consensuschain.Config{Params: params5(), VM: wrapper},
|
||||
consensuschain.WithQuorumCert(chainID, proposer.nodeID, newBLSVoteVerifier(vdrState, netID), certs, newBLSVoteSigner(proposer.sk)),
|
||||
consensuschain.WithStakeWeighting(newValidatorStakeSource(vdrState, netID)),
|
||||
consensuschain.WithValidatorSetRoot(newValidatorSetRootSource(vdrState, netID)),
|
||||
)
|
||||
if err := engine.Start(context.Background(), true); err != nil {
|
||||
t.Fatalf("engine.Start: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = engine.Stop(context.Background()) })
|
||||
|
||||
return &quorumEngineFixture{
|
||||
engine: engine,
|
||||
wrapper: wrapper,
|
||||
inner: inner,
|
||||
chainID: chainID,
|
||||
netID: netID,
|
||||
proposer: proposer,
|
||||
certs: certs,
|
||||
}
|
||||
}
|
||||
|
||||
// proposeViaWrapper builds the staged inner block THROUGH the wrapper (stamping
|
||||
// the live P-chain height), tracks it as the engine's own verified proposal with
|
||||
// the wrapper-delivered epoch, records the proposer's self-vote, and returns the
|
||||
// wrapped block + the canonical vote position followers must sign.
|
||||
func (f *quorumEngineFixture) proposeViaWrapper(t *testing.T, inner *fakeInnerBlock) (block.Block, consensuschain.VotePosition) {
|
||||
t.Helper()
|
||||
f.inner.stage(inner)
|
||||
wrapped, err := f.wrapper.BuildBlock(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("wrapper.BuildBlock: %v", err)
|
||||
}
|
||||
pos := f.engine.TrackOwnProposalForTest(context.Background(), wrapped, 0)
|
||||
return wrapped, pos
|
||||
}
|
||||
|
||||
// newFakeInner is a small constructor for an inner block at value height h with a
|
||||
// unique opaque encoding (tag-derived, so byBytes keys never collide).
|
||||
func newFakeInner(h uint64, parent ids.ID, tag string) *fakeInnerBlock {
|
||||
return &fakeInnerBlock{
|
||||
id: ids.GenerateTestID(),
|
||||
parentID: parent,
|
||||
height: h,
|
||||
bytes: []byte("inner:" + tag),
|
||||
timestamp: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// --- (1) the boundary delivers the REAL height (not 0), build + parse ---------
|
||||
|
||||
// TestPChainHeightVM_DeliversRealHeight is the direct b2 boundary proof: the
|
||||
// wrapper stamps the proposer's live P-chain height onto the block the engine
|
||||
// sees, so pChainHeightOf(realBlock) returns that height — NOT 0 — at BuildBlock,
|
||||
// AND a follower recovers the IDENTICAL height by parsing the gossiped bytes (the
|
||||
// determinism guarantee H rides the bytes, never recomputed from a skewing view).
|
||||
//
|
||||
// This is the assertion the whole fix turns on. On the broken path
|
||||
// pChainHeightOf(any real plugin block)==0 → set@0 (genesis) forever.
|
||||
func TestPChainHeightVM_DeliversRealHeight(t *testing.T) {
|
||||
const epoch = uint64(7) // the live P-chain height the proposer stamps
|
||||
netID := ids.GenerateTestID()
|
||||
v := newBLSValidator(t, 20)
|
||||
// A state whose GetCurrentHeight is 7 (so BuildBlock stamps 7); the set content
|
||||
// is irrelevant to the stamping itself, but we register it at 7 for symmetry.
|
||||
state := stateByHeight(netID, epoch, map[uint64][]blsValidator{epoch: {v}})
|
||||
wrapper := newPChainHeightVM(newFakeInnerVM(), state, netID)
|
||||
|
||||
inner := newFakeInner(10_000_000, ids.Empty, "real-height") // value height races ahead
|
||||
wrapper.inner.(*fakeInnerVM).stage(inner)
|
||||
|
||||
wrapped, err := wrapper.BuildBlock(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("BuildBlock: %v", err)
|
||||
}
|
||||
|
||||
// (1a) the engine's boundary read on the BUILT block returns the stamped height.
|
||||
if got := consensuschain.PChainHeightOfForTest(wrapped); got != epoch {
|
||||
t.Fatalf("pChainHeightOf(built block) = %d, want %d — the boundary still delivers the wrong height "+
|
||||
"(0 would mean the genesis-set freeze the b2 fix removes)", got, epoch)
|
||||
}
|
||||
// Sanity: the inner block itself exposes no PChainHeight, so without the wrapper
|
||||
// the engine would read 0. This pins WHY the wrapper is load-bearing.
|
||||
if got := consensuschain.PChainHeightOfForTest(inner); got != 0 {
|
||||
t.Fatalf("bare inner block must expose no P-chain height (pChainHeightOf=0), got %d", got)
|
||||
}
|
||||
|
||||
// (1b) determinism: a follower parsing the GOSSIPED bytes recovers the same H.
|
||||
parsed, err := wrapper.ParseBlock(context.Background(), wrapped.Bytes())
|
||||
if err != nil {
|
||||
t.Fatalf("ParseBlock(gossiped bytes): %v", err)
|
||||
}
|
||||
if got := consensuschain.PChainHeightOfForTest(parsed); got != epoch {
|
||||
t.Fatalf("pChainHeightOf(parsed block) = %d, want %d — a follower recomputed/lost the height; "+
|
||||
"finality would stall on a dynamic set (worse than the genesis fallback)", got, epoch)
|
||||
}
|
||||
if parsed.ID() != wrapped.ID() {
|
||||
t.Fatalf("parsed block ID %s != built block ID %s — the inner identity must be preserved across the envelope", parsed.ID(), wrapped.ID())
|
||||
}
|
||||
}
|
||||
|
||||
// --- (2) K>1 finalizes at genesis (set@H0) -----------------------------------
|
||||
|
||||
// TestPChainHeightVM_FinalizesAtGenesis proves the fix UNBRICKS finality in the
|
||||
// base case: a K>1 quorum chain whose validator set is the genesis set (current
|
||||
// P-chain height 0) finalizes through the real BLS quorum sources. This is the
|
||||
// safe floor the genesis fallback guarantees — even here the height is delivered
|
||||
// honestly (0), and the set@0 is non-empty so a ⅔ quorum finalizes.
|
||||
func TestPChainHeightVM_FinalizesAtGenesis(t *testing.T) {
|
||||
const genesis = uint64(0)
|
||||
netID := constantsPrimaryNetworkID()
|
||||
|
||||
g := make([]blsValidator, 5)
|
||||
for i := range g {
|
||||
g[i] = newBLSValidator(t, 20) // equal stake, total 100
|
||||
}
|
||||
state := stateByHeight(netID, genesis, map[uint64][]blsValidator{genesis: g})
|
||||
|
||||
f := newQuorumEngineFixture(t, netID, state, g[0], map[uint64][]blsValidator{genesis: g})
|
||||
|
||||
inner := newFakeInner(42, ids.Empty, "genesis-finalize") // value height advanced past genesis
|
||||
wrapped, pos := f.proposeViaWrapper(t, inner)
|
||||
|
||||
// The block was stamped at the genesis height (0); the position binds set@0.
|
||||
if got := consensuschain.PChainHeightOfForTest(wrapped); got != genesis {
|
||||
t.Fatalf("expected genesis stamp %d, got %d", genesis, got)
|
||||
}
|
||||
if pos.ValidatorSetRoot == ids.Empty {
|
||||
t.Fatal("genesis set-root must be non-Empty (the genesis set is non-empty)")
|
||||
}
|
||||
|
||||
// proposer g[0] self-voted; drive 3 more signed accepts → 4/5 = 80/100 stake > ⅔.
|
||||
f.engine.ReceiveVote(signBLSVote(t, g[1], pos))
|
||||
f.engine.ReceiveVote(signBLSVote(t, g[2], pos))
|
||||
f.engine.ReceiveVote(signBLSVote(t, g[3], pos))
|
||||
|
||||
if !waitForCond(2*time.Second, func() bool { return inner.accepted() == 1 }) {
|
||||
t.Fatalf("UNBRICK: a K>1 block must finalize against the genesis set (VM.Accept=%d)", inner.accepted())
|
||||
}
|
||||
if f.certs.count() == 0 {
|
||||
t.Fatal("a verified quorum cert must be assembled + gossiped at finality")
|
||||
}
|
||||
}
|
||||
|
||||
// --- (3) K>1 finalizes AFTER a staking change (THE b2 proof) ------------------
|
||||
|
||||
// TestPChainHeightVM_FinalizesAfterStakingChange is the load-bearing b2 proof:
|
||||
// validators that JOINED after genesis cast the deciding votes + stake, and the
|
||||
// block finalizes — which is IMPOSSIBLE on the broken set@0 path, where the
|
||||
// joiners are absent from the genesis set so their votes are dropped and their
|
||||
// stake is uncounted.
|
||||
//
|
||||
// The set@epoch (height 7) holds {g0, j1, j2, j3, j4}: only g0 overlaps genesis;
|
||||
// j1..j4 JOINED at 7. The genesis set@0 holds five DIFFERENT validators with only
|
||||
// g0 in common. A 4-voter cert {g0, j1, j2, j3} = 80/100 stake-at-7 > ⅔.
|
||||
//
|
||||
// - With the b2 fix: the block is stamped 7, the verifier resolves j1..j3 at
|
||||
// set@7 (present) and the ⅔ tally is measured at 7 → the cert verifies →
|
||||
// FINALIZES.
|
||||
// - On the broken set@0 path: j1..j3 are unknown at height 0 → their signed
|
||||
// votes are dropped → only g0 (20/100) verifies < ⅔ → STALLS FOREVER.
|
||||
//
|
||||
// The test asserts BOTH directly: (a) the block finalizes, and (b) the production
|
||||
// verifier itself rejects a joiner's vote at height 0 but accepts it at height 7 —
|
||||
// pinning the exact mechanism that would stall the frozen-set path.
|
||||
func TestPChainHeightVM_FinalizesAfterStakingChange(t *testing.T) {
|
||||
const (
|
||||
genesis = uint64(0)
|
||||
epoch = uint64(7) // staking change landed here; current P-chain height = 7
|
||||
)
|
||||
netID := constantsPrimaryNetworkID()
|
||||
|
||||
// g0 is a validator present at BOTH epochs (so the fixture proposer key resolves
|
||||
// at the stamped epoch). j1..j4 JOINED at epoch 7. gen1..gen4 are the OTHER four
|
||||
// genesis validators (present only at 0) — they make set@0 a genuinely different
|
||||
// set so "the joiners decide" is unambiguous.
|
||||
g0 := newBLSValidator(t, 20)
|
||||
j1 := newBLSValidator(t, 20)
|
||||
j2 := newBLSValidator(t, 20)
|
||||
j3 := newBLSValidator(t, 20)
|
||||
j4 := newBLSValidator(t, 20)
|
||||
gen1 := newBLSValidator(t, 20)
|
||||
gen2 := newBLSValidator(t, 20)
|
||||
gen3 := newBLSValidator(t, 20)
|
||||
gen4 := newBLSValidator(t, 20)
|
||||
|
||||
genesisSet := []blsValidator{g0, gen1, gen2, gen3, gen4} // total 100 at height 0
|
||||
epochSet := []blsValidator{g0, j1, j2, j3, j4} // total 100 at height 7
|
||||
|
||||
state := stateByHeight(netID, epoch, map[uint64][]blsValidator{
|
||||
genesis: genesisSet,
|
||||
epoch: epochSet,
|
||||
})
|
||||
|
||||
// (b) PIN THE MECHANISM that makes the frozen path stall: the production verifier
|
||||
// rejects a joiner at height 0 (frozen/genesis) and accepts it at height 7.
|
||||
verifier := newBLSVoteVerifier(state, netID)
|
||||
// Build a position to sign (any height-7-bound position works for this probe).
|
||||
probeBlk := newFakeInner(123, ids.Empty, "probe")
|
||||
probeWrapper := newPChainHeightVM(newFakeInnerVM(), state, netID)
|
||||
probeWrapper.inner.(*fakeInnerVM).stage(probeBlk)
|
||||
probeWrapped, _ := probeWrapper.BuildBlock(context.Background())
|
||||
probePos := consensuschain.VotePosition{
|
||||
ChainID: ids.GenerateTestID(),
|
||||
Height: probeWrapped.Height(),
|
||||
BlockID: probeWrapped.ID(),
|
||||
ParentID: ids.Empty,
|
||||
ValidatorSetRoot: newValidatorSetRootSource(state, netID).ValidatorSetRoot(epoch),
|
||||
}
|
||||
probeMsg := consensuschain.CanonicalVoteMessage(probePos)
|
||||
j1Sig, err := j1.sk.Sign(probeMsg)
|
||||
if err != nil {
|
||||
t.Fatalf("sign probe: %v", err)
|
||||
}
|
||||
j1SigBytes := bls.SignatureToBytes(j1Sig)
|
||||
if verifier.VerifyVote(j1.nodeID, probeMsg, j1SigBytes, genesis) {
|
||||
t.Fatal("frozen-path mechanism broken: a post-genesis joiner must NOT verify at height 0 " +
|
||||
"(if it does, the genesis-set path would not actually stall and the test is vacuous)")
|
||||
}
|
||||
if !verifier.VerifyVote(j1.nodeID, probeMsg, j1SigBytes, epoch) {
|
||||
t.Fatal("a joiner present at the epoch MUST verify at the epoch height — the b2 read is broken")
|
||||
}
|
||||
|
||||
// (a) THE END-TO-END FINALIZATION: drive the real engine with the joiners.
|
||||
f := newQuorumEngineFixture(t, netID, state, g0, map[uint64][]blsValidator{
|
||||
genesis: genesisSet,
|
||||
epoch: epochSet,
|
||||
})
|
||||
|
||||
inner := newFakeInner(10_000_000, ids.Empty, "post-staking-change") // value height far ahead
|
||||
wrapped, pos := f.proposeViaWrapper(t, inner)
|
||||
|
||||
// The block carries the LIVE epoch height (7), and the position binds set@7.
|
||||
if got := consensuschain.PChainHeightOfForTest(wrapped); got != epoch {
|
||||
t.Fatalf("block must carry the live epoch height %d, got %d", epoch, got)
|
||||
}
|
||||
if pos.ValidatorSetRoot != newValidatorSetRootSource(state, netID).ValidatorSetRoot(epoch) {
|
||||
t.Fatal("position must bind the set-root at the epoch height (set@7), not another height")
|
||||
}
|
||||
if pos.ValidatorSetRoot == newValidatorSetRootSource(state, netID).ValidatorSetRoot(genesis) {
|
||||
t.Fatal("test vacuous: set@7 root must differ from set@0 root (the sets must genuinely differ)")
|
||||
}
|
||||
|
||||
// proposer g0 self-voted; the JOINERS j1,j2,j3 cast the deciding votes.
|
||||
// 4 distinct accepts {g0,j1,j2,j3} = 80/100 stake-at-7 > ⅔ → MUST finalize.
|
||||
f.engine.ReceiveVote(signBLSVote(t, j1, pos))
|
||||
f.engine.ReceiveVote(signBLSVote(t, j2, pos))
|
||||
f.engine.ReceiveVote(signBLSVote(t, j3, pos))
|
||||
|
||||
if !waitForCond(3*time.Second, func() bool { return inner.accepted() == 1 }) {
|
||||
t.Fatalf("b2: a block whose ⅔ quorum is post-genesis JOINERS must finalize against the LIVE set@%d "+
|
||||
"(VM.Accept=%d). On the broken set@0 path the joiners are unknown → votes dropped → permanent stall.",
|
||||
epoch, inner.accepted())
|
||||
}
|
||||
|
||||
// The gossiped cert must verify stake-weighted AT THE EPOCH, and must FAIL at
|
||||
// genesis (where the joiners are absent) — proving the height is load-bearing.
|
||||
if f.certs.count() == 0 {
|
||||
t.Fatal("a verified quorum cert must be assembled + gossiped at finality")
|
||||
}
|
||||
f.certs.mu.Lock()
|
||||
lastCert := f.certs.certs[len(f.certs.certs)-1]
|
||||
f.certs.mu.Unlock()
|
||||
cert, err := consensuschain.UnmarshalQuorumCert(lastCert)
|
||||
if err != nil {
|
||||
t.Fatalf("decode gossiped cert: %v", err)
|
||||
}
|
||||
stake := newValidatorStakeSource(state, netID)
|
||||
if err := cert.VerifyWeighted(verifier, stake, epoch); err != nil {
|
||||
t.Fatalf("cert must verify stake-weighted at the epoch height %d: %v", epoch, err)
|
||||
}
|
||||
if err := cert.VerifyWeighted(verifier, stake, genesis); err == nil {
|
||||
t.Fatal("b2: cert must NOT verify at the genesis height (joiners absent / below ⅔ there) — " +
|
||||
"if it does, the epoch height is not actually being used")
|
||||
}
|
||||
// The cert must contain at least one joiner — proving a post-genesis validator's
|
||||
// vote was counted (the exact case the frozen-set path drops).
|
||||
var hasJoiner bool
|
||||
for i := range cert.Votes {
|
||||
if cert.Votes[i].NodeID == j1.nodeID || cert.Votes[i].NodeID == j2.nodeID || cert.Votes[i].NodeID == j3.nodeID {
|
||||
hasJoiner = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasJoiner {
|
||||
t.Fatal("the finality cert must include a post-genesis joiner's vote (it was the deciding quorum)")
|
||||
}
|
||||
}
|
||||
|
||||
// constantsPrimaryNetworkID returns the primary-network ID the node uses for
|
||||
// native-chain validator lookups (ids.Empty). Declared here so the test reads the
|
||||
// SAME net the production wiring resolves native chains against, without importing
|
||||
// the constants package for one value.
|
||||
func constantsPrimaryNetworkID() ids.ID { return ids.Empty }
|
||||
@@ -1,327 +0,0 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// pchain_height_hardening_test.go — the receive-side hardening proofs for the b2
|
||||
// transport wrapper (Red round): the three node-layer fixes that close the
|
||||
// receive-side gaps the build-side stamping left open.
|
||||
//
|
||||
// (HIGH-1, predicate b — RECENCY) ParseBlock rejects a wrapped block whose
|
||||
// stamped P-chain epoch exceeds this node's live P-chain height by more than the
|
||||
// recency slack (an absurd-future epoch). Honest forward skew within the slack —
|
||||
// including a legitimate P-chain advance during a staking change — still parses.
|
||||
//
|
||||
// (MEDIUM-3 — MAP DoS) the heights map is bounded: it plateaus at the finalized
|
||||
// watermark under mass parse+accept, and a still-pending block's epoch is never
|
||||
// evicted by the watermark prune. A sustained unverified-parse flood is capped.
|
||||
//
|
||||
// (LOW-2 — FRAME DISCRIMINATOR) a raw inner block whose first bytes coincidentally
|
||||
// equal the 4-byte frame magic is NOT mis-parsed as framed: the inner re-parse of
|
||||
// the framed payload fails, so ParseBlock falls back to a raw whole-block parse.
|
||||
package chains
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/ids"
|
||||
)
|
||||
|
||||
// --- (HIGH-1 b) RECENCY: reject absurd-future epoch ---------------------------
|
||||
|
||||
// TestParseBlock_RejectsFarFutureEpoch is the receive-side upper-bound proof. A
|
||||
// node whose live P-chain height is 100 parses a wrapped block stamped at epoch
|
||||
// 10_000 (far beyond 100 + slack). ParseBlock REJECTS it fail-closed — the block
|
||||
// is dropped, never returned to be tracked. This stops a Byzantine proposer from
|
||||
// pinning a block to an epoch the network has not reached.
|
||||
func TestParseBlock_RejectsFarFutureEpoch(t *testing.T) {
|
||||
const localH = uint64(100)
|
||||
netID := ids.GenerateTestID()
|
||||
v := newBLSValidator(t, 20)
|
||||
state := stateByHeight(netID, localH, map[uint64][]blsValidator{localH: {v}})
|
||||
wrapper := newPChainHeightVM(newFakeInnerVM(), state, netID)
|
||||
|
||||
// Craft a frame stamped at an absurd-future epoch, with a registered inner block
|
||||
// so the ONLY reason to reject is the recency bound (not an inner parse failure).
|
||||
inner := newFakeInner(5_000_000, ids.Empty, "far-future-epoch")
|
||||
wrapper.inner.(*fakeInnerVM).register(inner)
|
||||
framed := wrapPChainHeight(inner.Bytes(), localH+pChainHeightRecencySlack+1) // just past the bound
|
||||
|
||||
if _, err := wrapper.ParseBlock(context.Background(), framed); err != errPChainHeightNotRecent {
|
||||
t.Fatalf("ParseBlock(far-future epoch) err = %v, want errPChainHeightNotRecent — an absurd-future "+
|
||||
"P-chain epoch must be rejected fail-closed so a follower never tracks a block at an unreachable epoch", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseBlock_AcceptsEpochWithinSlack proves the recency gate admits HONEST
|
||||
// forward skew: a follower at local P-chain height 100 parses a block stamped at
|
||||
// 100 + slack (the boundary — the largest legitimate skew, e.g. a proposer that
|
||||
// saw P-chain blocks this follower has not yet synced during a staking change).
|
||||
// The block parses and carries its real epoch.
|
||||
func TestParseBlock_AcceptsEpochWithinSlack(t *testing.T) {
|
||||
const localH = uint64(100)
|
||||
netID := ids.GenerateTestID()
|
||||
v := newBLSValidator(t, 20)
|
||||
state := stateByHeight(netID, localH, map[uint64][]blsValidator{localH: {v}})
|
||||
wrapper := newPChainHeightVM(newFakeInnerVM(), state, netID)
|
||||
|
||||
inner := newFakeInner(5_000_000, ids.Empty, "within-slack-epoch")
|
||||
wrapper.inner.(*fakeInnerVM).register(inner)
|
||||
atBound := localH + pChainHeightRecencySlack // exactly at the bound — must be admitted
|
||||
framed := wrapPChainHeight(inner.Bytes(), atBound)
|
||||
|
||||
parsed, err := wrapper.ParseBlock(context.Background(), framed)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseBlock(epoch within slack) err = %v, want nil — honest forward skew up to the slack "+
|
||||
"(a real P-chain advance during a staking change) must still parse", err)
|
||||
}
|
||||
if got := parsed.(*pChainHeightBlock).PChainHeight(); got != atBound {
|
||||
t.Fatalf("parsed epoch = %d, want %d — the wrapper must carry the real (recent) epoch unchanged", got, atBound)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseBlock_RecencyDisabledWithoutState proves the fail-soft: a wrapper with
|
||||
// no P-chain view (nil state, current height 0) cannot bound recency, so it admits
|
||||
// the block — the verifier still fails closed on an unresolvable future epoch at
|
||||
// set-resolution time. (Production installs the wrapper only on K>1 chains the
|
||||
// manager guards to have a live state, so this is a defensive path, not a mode.)
|
||||
func TestParseBlock_RecencyDisabledWithoutState(t *testing.T) {
|
||||
wrapper := newPChainHeightVM(newFakeInnerVM(), nil, ids.GenerateTestID())
|
||||
inner := newFakeInner(7, ids.Empty, "no-state-epoch")
|
||||
wrapper.inner.(*fakeInnerVM).register(inner)
|
||||
framed := wrapPChainHeight(inner.Bytes(), 9_999_999) // would be far-future if state existed
|
||||
|
||||
if _, err := wrapper.ParseBlock(context.Background(), framed); err != nil {
|
||||
t.Fatalf("ParseBlock with nil state must admit (recency unenforceable, verifier fails closed downstream): %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- (LOW-2) FRAME DISCRIMINATOR: a magic-colliding raw block is NOT mis-framed -
|
||||
|
||||
// TestParseBlock_MagicCollisionParsesRaw is the discriminator proof. A raw inner
|
||||
// block whose Bytes() coincidentally BEGIN with the 4-byte frame magic (the
|
||||
// 2^-32 collision the raw-hash-prefix VMs hit) must parse as a RAW block, not be
|
||||
// mis-classified as framed (which used to fail the inner decode → bootstrap stall).
|
||||
// The inner re-parse of the framed payload fails (the payload is the block minus
|
||||
// its first 12 bytes — not a valid inner block), so ParseBlock falls back to a raw
|
||||
// whole-block parse.
|
||||
func TestParseBlock_MagicCollisionParsesRaw(t *testing.T) {
|
||||
netID := ids.GenerateTestID()
|
||||
v := newBLSValidator(t, 20)
|
||||
state := stateByHeight(netID, 50, map[uint64][]blsValidator{50: {v}})
|
||||
inner := newFakeInnerVM()
|
||||
wrapper := newPChainHeightVM(inner, state, netID)
|
||||
|
||||
// A raw block whose bytes START with the exact magic prefix, then arbitrary
|
||||
// payload. Length is >= the header width so the prefix check would treat it as a
|
||||
// candidate frame. Crucially, bytes[12:] is NOT a registered inner block, so the
|
||||
// framed-payload re-parse fails and the whole-bytes parse must be used.
|
||||
raw := &fakeInnerBlock{
|
||||
id: ids.GenerateTestID(),
|
||||
parentID: ids.Empty,
|
||||
height: 1234,
|
||||
bytes: append(append([]byte{}, pChainHeightMagic[:]...),
|
||||
[]byte("RAW-BLOCK-COLLIDES-WITH-MAGIC-PREFIX-payload")...),
|
||||
}
|
||||
inner.register(raw) // registers byBytes[whole] = raw; bytes[12:] stays UNregistered
|
||||
|
||||
parsed, err := wrapper.ParseBlock(context.Background(), raw.bytes)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseBlock(magic-colliding raw block) err = %v, want nil — a raw block whose prefix collides "+
|
||||
"with the frame magic must parse as RAW (the old behavior mis-framed it → inner decode fail → bootstrap stall)", err)
|
||||
}
|
||||
if parsed.ID() != raw.id {
|
||||
t.Fatalf("parsed block ID %s != raw block ID %s — the colliding block must decode to the SAME raw inner block", parsed.ID(), raw.id)
|
||||
}
|
||||
// A raw block falls back to the genesis-set epoch (0): it was NOT framed, so no
|
||||
// proposer epoch was carried.
|
||||
if got := parsed.(*pChainHeightBlock).PChainHeight(); got != 0 {
|
||||
t.Fatalf("magic-colliding raw block epoch = %d, want 0 — it must NOT be read as a framed epoch", got)
|
||||
}
|
||||
// Its re-gossip bytes must be the raw passthrough (the inner bytes verbatim), not
|
||||
// re-framed — so a follower of THIS node also sees it raw.
|
||||
if string(parsed.Bytes()) != string(raw.bytes) {
|
||||
t.Fatal("a raw colliding block must re-gossip its inner bytes verbatim (passthrough), not a new frame")
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseBlock_GenuineFrameStillParses guards the discriminator's other side: a
|
||||
// GENUINE frame (whose payload IS a valid inner block) still parses as framed and
|
||||
// recovers the stamped epoch. This pins that the collision fix did not break the
|
||||
// normal framed path.
|
||||
func TestParseBlock_GenuineFrameStillParses(t *testing.T) {
|
||||
netID := ids.GenerateTestID()
|
||||
v := newBLSValidator(t, 20)
|
||||
const epoch = uint64(33)
|
||||
state := stateByHeight(netID, epoch, map[uint64][]blsValidator{epoch: {v}})
|
||||
inner := newFakeInnerVM()
|
||||
wrapper := newPChainHeightVM(inner, state, netID)
|
||||
|
||||
innerBlk := newFakeInner(9000, ids.Empty, "genuine-frame")
|
||||
inner.register(innerBlk)
|
||||
framed := wrapPChainHeight(innerBlk.Bytes(), epoch)
|
||||
|
||||
parsed, err := wrapper.ParseBlock(context.Background(), framed)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseBlock(genuine frame) err = %v, want nil", err)
|
||||
}
|
||||
if parsed.ID() != innerBlk.ID() {
|
||||
t.Fatalf("genuine frame parsed ID %s != inner ID %s", parsed.ID(), innerBlk.ID())
|
||||
}
|
||||
if got := parsed.(*pChainHeightBlock).PChainHeight(); got != epoch {
|
||||
t.Fatalf("genuine frame epoch = %d, want %d", got, epoch)
|
||||
}
|
||||
}
|
||||
|
||||
// --- (MEDIUM-3) MAP BOUND: plateau at watermark, pending never evicted --------
|
||||
|
||||
// TestHeightsMap_PlateausAtWatermark is the DoS bound proof. We parse a long run
|
||||
// of distinct framed blocks (each adds a heights entry) and ACCEPT them in order;
|
||||
// each Accept advances the finalized-height watermark and prunes entries at/below
|
||||
// it. The map plateaus — it does NOT grow without bound as the chain advances.
|
||||
func TestHeightsMap_PlateausAtWatermark(t *testing.T) {
|
||||
netID := ids.GenerateTestID()
|
||||
v := newBLSValidator(t, 20)
|
||||
const epoch = uint64(10)
|
||||
state := stateByHeight(netID, epoch, map[uint64][]blsValidator{epoch: {v}})
|
||||
inner := newFakeInnerVM()
|
||||
wrapper := newPChainHeightVM(inner, state, netID)
|
||||
|
||||
const n = 500
|
||||
var lastParsed *pChainHeightBlock
|
||||
for h := uint64(1); h <= n; h++ {
|
||||
blk := newFakeInner(h, ids.Empty, "plateau-"+itoa(h))
|
||||
inner.register(blk)
|
||||
framed := wrapPChainHeight(blk.Bytes(), epoch)
|
||||
parsed, err := wrapper.ParseBlock(context.Background(), framed)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseBlock #%d: %v", h, err)
|
||||
}
|
||||
// Accept the PREVIOUS block (so the most-recent one is still "pending").
|
||||
if lastParsed != nil {
|
||||
if err := lastParsed.Accept(context.Background()); err != nil {
|
||||
t.Fatalf("Accept #%d: %v", h-1, err)
|
||||
}
|
||||
}
|
||||
lastParsed = parsed.(*pChainHeightBlock)
|
||||
|
||||
// After each accept the map must be bounded: only entries strictly above the
|
||||
// watermark survive. We accept (h-1) blocks, so at most a tiny constant remain
|
||||
// (the just-parsed, not-yet-accepted block, plus any equal-height entries).
|
||||
wrapper.mu.Lock()
|
||||
size := len(wrapper.heights)
|
||||
wm := wrapper.finalizedHeight
|
||||
wrapper.mu.Unlock()
|
||||
if size > 4 { // generous constant: the pending working set, not O(n)
|
||||
t.Fatalf("heights map grew to %d entries at height %d (watermark %d) — it must plateau at the "+
|
||||
"finalized watermark, not accrete one permanent entry per parsed block (MEDIUM-3 DoS)", size, h, wm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestHeightsMap_PendingNeverEvictedByWatermark proves the watermark prune NEVER
|
||||
// drops a still-pending block's epoch. We track a pending block at height 1000
|
||||
// (epoch recalled), accept a LOWER-height block (watermark advances only to that
|
||||
// lower height), and assert the pending block's epoch is still recallable. A blind
|
||||
// LRU would have dropped it; the watermark prune must not.
|
||||
func TestHeightsMap_PendingNeverEvictedByWatermark(t *testing.T) {
|
||||
netID := ids.GenerateTestID()
|
||||
v := newBLSValidator(t, 20)
|
||||
const epoch = uint64(12)
|
||||
state := stateByHeight(netID, epoch, map[uint64][]blsValidator{epoch: {v}})
|
||||
inner := newFakeInnerVM()
|
||||
wrapper := newPChainHeightVM(inner, state, netID)
|
||||
|
||||
// Pending block at a HIGH value height (1000).
|
||||
pending := newFakeInner(1000, ids.Empty, "pending-high")
|
||||
inner.register(pending)
|
||||
pendingFrame := wrapPChainHeight(pending.Bytes(), epoch)
|
||||
if _, err := wrapper.ParseBlock(context.Background(), pendingFrame); err != nil {
|
||||
t.Fatalf("ParseBlock(pending): %v", err)
|
||||
}
|
||||
|
||||
// A different block at a LOW value height (5) finalizes → watermark = 5.
|
||||
low := newFakeInner(5, ids.Empty, "finalized-low")
|
||||
inner.register(low)
|
||||
lowFrame := wrapPChainHeight(low.Bytes(), epoch)
|
||||
lowParsed, err := wrapper.ParseBlock(context.Background(), lowFrame)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseBlock(low): %v", err)
|
||||
}
|
||||
if err := lowParsed.Accept(context.Background()); err != nil {
|
||||
t.Fatalf("Accept(low): %v", err)
|
||||
}
|
||||
|
||||
// The watermark advanced to 5 (the low block). The PENDING block at height 1000
|
||||
// is strictly above the watermark, so its epoch MUST survive the prune.
|
||||
if got, ok := wrapper.recall(pending.ID()); !ok || got != epoch {
|
||||
t.Fatalf("pending block's epoch recall = (%d,%v), want (%d,true) — the watermark prune (wm=5) must NEVER "+
|
||||
"evict a still-pending block at height 1000 (that would reset its epoch to 0 = re-freeze)", got, ok, epoch)
|
||||
}
|
||||
// And the finalized low block's entry WAS pruned (it is at/below the watermark).
|
||||
if _, ok := wrapper.recall(low.ID()); ok {
|
||||
t.Fatal("the finalized low block's entry should have been pruned at/below the watermark (loss-free: epoch already captured)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestHeightsMap_FloodCappedPreservingNearTip proves the hard-cap backstop: a
|
||||
// sustained UNVERIFIED-parse flood (blocks that never finalize, so the watermark
|
||||
// never advances) cannot grow the map past the cap, AND the cap evicts
|
||||
// highest-value-height-first so the near-tip pending band survives. We seed a
|
||||
// near-tip pending entry (low height), then flood with far-future-height frames;
|
||||
// the map stays at the cap and the near-tip entry is retained.
|
||||
func TestHeightsMap_FloodCappedPreservingNearTip(t *testing.T) {
|
||||
netID := ids.GenerateTestID()
|
||||
v := newBLSValidator(t, 20)
|
||||
const epoch = uint64(8)
|
||||
// Slack must admit the flood frames' epoch; we stamp them at the current epoch so
|
||||
// the recency gate is not what bounds them — the CAP is what we are testing.
|
||||
state := stateByHeight(netID, epoch, map[uint64][]blsValidator{epoch: {v}})
|
||||
inner := newFakeInnerVM()
|
||||
wrapper := newPChainHeightVM(inner, state, netID)
|
||||
|
||||
// Near-tip pending block at LOW value height 1.
|
||||
nearTip := newFakeInner(1, ids.Empty, "near-tip")
|
||||
inner.register(nearTip)
|
||||
if _, err := wrapper.ParseBlock(context.Background(), wrapPChainHeight(nearTip.Bytes(), epoch)); err != nil {
|
||||
t.Fatalf("ParseBlock(near-tip): %v", err)
|
||||
}
|
||||
|
||||
// Flood: cap + 200 distinct frames at FAR-future value heights (never accepted).
|
||||
for i := 0; i < pChainHeightMapCap+200; i++ {
|
||||
h := uint64(1_000_000 + i) // far above the near-tip height
|
||||
blk := newFakeInner(h, ids.Empty, "flood-"+itoa(h))
|
||||
inner.register(blk)
|
||||
if _, err := wrapper.ParseBlock(context.Background(), wrapPChainHeight(blk.Bytes(), epoch)); err != nil {
|
||||
t.Fatalf("ParseBlock(flood %d): %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
wrapper.mu.Lock()
|
||||
size := len(wrapper.heights)
|
||||
_, nearTipKept := wrapper.heights[nearTip.ID()]
|
||||
wrapper.mu.Unlock()
|
||||
|
||||
if size > pChainHeightMapCap {
|
||||
t.Fatalf("heights map = %d entries, must be capped at %d under flood (MEDIUM-3 backstop)", size, pChainHeightMapCap)
|
||||
}
|
||||
if !nearTipKept {
|
||||
t.Fatal("the near-tip pending entry (height 1) must SURVIVE the cap — eviction is highest-height-first, " +
|
||||
"so a flood of far-future-height spam is shed before any near-tip pending block (NOT a blind LRU)")
|
||||
}
|
||||
}
|
||||
|
||||
// itoa is a tiny, allocation-light uint64→string for unique test tags (avoids
|
||||
// importing strconv into a hot test loop and keeps byBytes keys distinct).
|
||||
func itoa(v uint64) string {
|
||||
if v == 0 {
|
||||
return "0"
|
||||
}
|
||||
var buf [20]byte
|
||||
i := len(buf)
|
||||
for v > 0 {
|
||||
i--
|
||||
buf[i] = byte('0' + v%10)
|
||||
v /= 10
|
||||
}
|
||||
return string(buf[i:])
|
||||
}
|
||||
@@ -1,480 +0,0 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// pchain_height_vm.go — the node-layer boundary that delivers the REAL P-CHAIN
|
||||
// EPOCH HEIGHT to the consensus chain engine (the b2 wiring; the LAST
|
||||
// QUORUM_FINALITY blocker).
|
||||
//
|
||||
// THE BUG IT FIXES. The chain engine pins a block's weighted validator set to a
|
||||
// P-CHAIN height (engine/chain: pChainHeightOf → epochHeightLocked → the SINGLE
|
||||
// height the set-root commitment, the ⅔-by-stake tally, AND the per-voter pubkey
|
||||
// resolution are read at). It reads that height by asserting the VM block to a
|
||||
// `PChainHeight() uint64` (consensus block.SignedBlock). A bare VM block — every
|
||||
// block the node's plugin VMs (C-Chain EVM, dexvm, …) produce — does NOT expose
|
||||
// one, so pChainHeightOf returns 0 and the engine resolves the set at P-chain
|
||||
// height 0: the GENESIS set. That is SAFE (non-empty, identical on every node,
|
||||
// ≤ current) and UNBRICKS finality, but it FREEZES the epoch at genesis: a
|
||||
// validator that JOINED after genesis is absent from set@0, so its vote is
|
||||
// dropped and its stake is not counted — finality cannot track a DYNAMIC
|
||||
// validator set (a departed genesis majority could even collude). This is the
|
||||
// frozen-set caveat Gate D must sign away.
|
||||
//
|
||||
// THE FIX. pChainHeightVM wraps the chain's BlockBuilder (the VM the engine
|
||||
// builds/parses blocks through) and makes the block the engine sees carry the
|
||||
// proposer's P-chain epoch height — WITHOUT changing the inner VM's block format,
|
||||
// IDs, or ledger state:
|
||||
//
|
||||
// - BuildBlock (proposer): build the inner block, then stamp the proposer's
|
||||
// live P-chain epoch height H = max(GetCurrentHeight, parentH) (the
|
||||
// proposervm selectChildPChainHeight rule — monotone, ≤ current). Return a
|
||||
// pChainHeightBlock whose Bytes() are [magic|H|innerBytes] and whose
|
||||
// PChainHeight() is H. Every other method (ID, ParentID, Height, Verify,
|
||||
// Accept, …) delegates to the inner block — so the block's IDENTITY and the
|
||||
// VM's state are the inner VM's, unchanged.
|
||||
// - ParseBlock (follower): split [magic|H|innerBytes], parse the inner bytes
|
||||
// through the inner VM, and re-attach H. Bytes WITHOUT the magic (a raw inner
|
||||
// block from a pre-wrapper peer, GetAncestors, or genesis) parse with H=0 →
|
||||
// the SAFE genesis-set fallback — never worse than today.
|
||||
//
|
||||
// DETERMINISM is the whole point and is why H must travel IN the gossiped bytes.
|
||||
// The engine gossips only blk.Bytes(); a follower recovers the block solely via
|
||||
// ParseBlock(those bytes). The proposer STAMPS H; every follower ADOPTS the
|
||||
// identical H from the envelope — it never recomputes H from its own (skewing)
|
||||
// current P-chain view. So every honest node derives the SAME epoch height from
|
||||
// the SAME signed block, which is the invariant the engine's cert verifier
|
||||
// requires (engine/chain HandleIncomingCert cross-checks the cert's set-root
|
||||
// against the set-root WE recompute at OUR epoch height for the block: equal H ⟹
|
||||
// equal root ⟹ the cert verifies; a post-genesis validator's vote+stake now
|
||||
// count). A build-time-only stamp would give the proposer a real H but leave
|
||||
// followers computing a DIFFERENT root from their own height → the cert is
|
||||
// dropped → finality STALLS on a dynamic set (strictly worse than the genesis
|
||||
// fallback). That is why the height is carried, not recomputed.
|
||||
//
|
||||
// This is a consensus-TRANSPORT framing (an 8-byte height + 4-byte magic prefix
|
||||
// on the gossiped bytes), NOT a chain/ledger fork: the inner VM's bytes, block
|
||||
// IDs, and execution state are byte-identical, so there is no re-genesis — only a
|
||||
// coordinated node upgrade (the whole validator set ships together).
|
||||
package chains
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
consensuschain "github.com/luxfi/consensus/engine/chain"
|
||||
"github.com/luxfi/consensus/engine/chain/block"
|
||||
"github.com/luxfi/ids"
|
||||
validators "github.com/luxfi/validators"
|
||||
)
|
||||
|
||||
// pChainHeightMagic tags a transport-wrapped block so ParseBlock can tell a
|
||||
// wrapped block ([magic|H:8|innerBytes]) from a raw inner block (no magic →
|
||||
// H=0, the genesis-set fallback). Distinct, fixed, version-pinned: a future
|
||||
// envelope change bumps the last byte and is non-malleable. "LXP" = Lux P-chain.
|
||||
var pChainHeightMagic = [4]byte{'L', 'X', 'P', 0x01}
|
||||
|
||||
// pChainHeightHeaderLen is the fixed transport-header width: magic(4) + height(8).
|
||||
const pChainHeightHeaderLen = 4 + 8
|
||||
|
||||
// pChainHeightRecencySlack is the receive-side UPPER bound on how far a gossiped
|
||||
// block's stamped P-chain epoch height H may exceed THIS node's live P-chain
|
||||
// height before the block is rejected as absurd-future (HIGH-1, predicate b). The
|
||||
// proposer stamps H = its own GetCurrentHeight (the proposervm selectChildPChainHeight
|
||||
// rule, ≤ its current); a follower lagging the P-chain sees a smaller current, so
|
||||
// some forward skew is LEGITIMATE during a staking change or a gossip burst. 256
|
||||
// P-chain heights swamps any honest inter-node skew (P-chain blocks are seconds
|
||||
// apart; gossip+verify is sub-second) while still rejecting a wildly-future H. The
|
||||
// bound is a LIVENESS/DoS sanity gate, NOT the safety bound (that is the monotone
|
||||
// gate in the engine): a future H always FAILS set resolution at the verifier
|
||||
// (errUnfinalizedHeight) and never finalizes against a bogus set regardless — this
|
||||
// just fails it fast and keeps the heights map from accreting unresolvable entries.
|
||||
const pChainHeightRecencySlack = uint64(256)
|
||||
|
||||
// pChainHeightMapCap is the hard backstop on the heights map size — the cap that
|
||||
// bounds the gossip-parse DoS (MEDIUM-3): ParseBlock runs before the engine's
|
||||
// dedup/Verify, so an attacker peer could stream distinct unverified blocks, each
|
||||
// adding a permanent entry. The map plateaus far below this in steady state (the
|
||||
// watermark prune evicts every finalized block's entry as finality advances); the
|
||||
// cap fires only under a sustained flood, and then evicts HIGHEST-chain-height
|
||||
// first — spam claims wild heights, real pending blocks cluster just above the
|
||||
// finalized watermark, so the near-tip pending band is preserved (NOT a blind LRU
|
||||
// that could drop a live block). Chosen >> any realistic pending working set.
|
||||
const pChainHeightMapCap = 4096
|
||||
|
||||
// errPChainHeightNotRecent is returned by ParseBlock when a wrapped block's stamped
|
||||
// P-chain epoch exceeds this node's live P-chain height by more than the recency
|
||||
// slack. Returning an error fails CLOSED: HandleIncomingBlock drops the block (it
|
||||
// is never tracked or voted), which is the correct response to an absurd-future
|
||||
// epoch a follower cannot resolve a set at.
|
||||
var errPChainHeightNotRecent = errors.New("chains: gossiped block P-chain epoch height exceeds local P-chain height + recency slack (absurd-future epoch, rejected fail-closed)")
|
||||
|
||||
// pChainHeightVM wraps a chain BlockBuilder so the block the consensus engine
|
||||
// sees carries the proposer's P-chain epoch height. It is the ONLY thing that
|
||||
// changes between the engine and the inner VM; everything else is the inner VM,
|
||||
// verbatim.
|
||||
//
|
||||
// It is installed ONLY on K>1 (quorum) chains: a K==1 chain finalizes on its
|
||||
// sole validator's self-vote with no cert and no validator-set epoch, so the
|
||||
// stamp is inert there and the wrapper is not installed (the inner VM is used
|
||||
// directly — one obvious path per mode).
|
||||
type pChainHeightVM struct {
|
||||
inner consensuschain.BlockBuilder
|
||||
state validators.State
|
||||
networkID ids.ID
|
||||
|
||||
// heights memoises blockID → (stamped P-chain epoch, value-chain height) for
|
||||
// blocks this node has built or parsed, so GetBlock can re-attach the epoch a
|
||||
// block was stamped with (the inner VM stores only the inner bytes, which carry
|
||||
// no epoch). Best-effort: a cache miss yields H=0 (the genesis-set fallback).
|
||||
// GetBlock is NOT on the finality-capture path — the engine records a block's
|
||||
// epoch the first time it BUILDS or PARSES the block (where the epoch is always
|
||||
// present) and reads it back from its own pending-block record, never by
|
||||
// re-fetching via GetBlock — so a miss here cannot drop a LIVE block's epoch
|
||||
// (the consensus PendingBlock holds it, not this map).
|
||||
//
|
||||
// BOUNDED (MEDIUM-3): the value-chain height tagged on each entry lets onAccepted
|
||||
// PRUNE every entry at or below the finalized-height watermark (a finalized block
|
||||
// never needs a GetBlock epoch re-attach), so under steady finality the map
|
||||
// plateaus at the watermark. finalizedHeight is that watermark, advanced as
|
||||
// blocks Accept. A hard cap (pChainHeightMapCap) backstops a sustained
|
||||
// unverified-parse flood, evicting highest-height-first to preserve the near-tip
|
||||
// pending band. A still-PENDING block (height above the watermark) is never
|
||||
// evicted by the watermark prune.
|
||||
mu sync.Mutex
|
||||
heights map[ids.ID]heightEntry
|
||||
finalizedHeight uint64
|
||||
}
|
||||
|
||||
// heightEntry records, for a remembered block, both the P-chain EPOCH it was
|
||||
// stamped with (re-attached by GetBlock) and its VALUE-CHAIN height (the prune key:
|
||||
// an entry at/below the finalized watermark is evictable).
|
||||
type heightEntry struct {
|
||||
epoch uint64
|
||||
chainHeight uint64
|
||||
}
|
||||
|
||||
// newPChainHeightVM wraps inner with P-chain-epoch-height stamping backed by the
|
||||
// height-indexed validators.State. networkID is the set the height is resolved
|
||||
// against (PrimaryNetworkID for native chains; the L1's set ID otherwise) — it is
|
||||
// recorded for symmetry with the engine's epoch reads, though GetCurrentHeight is
|
||||
// a P-chain-global height. A nil state disables stamping (BuildBlock falls back to
|
||||
// H=0): callers install this ONLY for K>1 chains, which the manager already
|
||||
// guards to have a live height-indexed state, so the nil path is a defensive
|
||||
// fail-soft, not a runtime mode.
|
||||
func newPChainHeightVM(inner consensuschain.BlockBuilder, state validators.State, networkID ids.ID) *pChainHeightVM {
|
||||
return &pChainHeightVM{
|
||||
inner: inner,
|
||||
state: state,
|
||||
networkID: networkID,
|
||||
heights: make(map[ids.ID]heightEntry),
|
||||
}
|
||||
}
|
||||
|
||||
var _ consensuschain.BlockBuilder = (*pChainHeightVM)(nil)
|
||||
|
||||
// remember records the epoch a block (at value-chain height chainHeight) was
|
||||
// stamped with so GetBlock can re-attach it. BOUNDED (MEDIUM-3): the map is pruned
|
||||
// against the finalized-height watermark by onAccepted, so it plateaus under steady
|
||||
// finality; remember additionally enforces the hard cap as a flood backstop. The
|
||||
// chainHeight is the prune key.
|
||||
func (vm *pChainHeightVM) remember(id ids.ID, epoch, chainHeight uint64) {
|
||||
vm.mu.Lock()
|
||||
defer vm.mu.Unlock()
|
||||
vm.heights[id] = heightEntry{epoch: epoch, chainHeight: chainHeight}
|
||||
vm.enforceCapLocked()
|
||||
}
|
||||
|
||||
func (vm *pChainHeightVM) recall(id ids.ID) (uint64, bool) {
|
||||
vm.mu.Lock()
|
||||
e, ok := vm.heights[id]
|
||||
vm.mu.Unlock()
|
||||
return e.epoch, ok
|
||||
}
|
||||
|
||||
// onAccepted advances the finalized-height watermark to acceptedHeight and PRUNES
|
||||
// every remembered entry at or below it (MEDIUM-3). A finalized block's epoch is
|
||||
// already captured in the engine's records and will never be re-attached via
|
||||
// GetBlock, so dropping its entry is loss-free — and a still-PENDING block (height
|
||||
// strictly above the watermark) is NEVER evicted by this prune, so its GetBlock
|
||||
// epoch survives. Called from the wrapped block's Accept (the block tells its VM
|
||||
// the height at which it finalized). Idempotent and monotone: a re-accept or an
|
||||
// out-of-order lower height never lowers the watermark.
|
||||
func (vm *pChainHeightVM) onAccepted(acceptedHeight uint64) {
|
||||
vm.mu.Lock()
|
||||
defer vm.mu.Unlock()
|
||||
if acceptedHeight > vm.finalizedHeight {
|
||||
vm.finalizedHeight = acceptedHeight
|
||||
}
|
||||
for id, e := range vm.heights {
|
||||
if e.chainHeight <= vm.finalizedHeight {
|
||||
delete(vm.heights, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// enforceCapLocked is the flood backstop: if the map exceeds the hard cap, evict
|
||||
// the entry with the HIGHEST value-chain height. Real pending blocks cluster just
|
||||
// above the finalized watermark (the next few heights); an unverified-parse flood
|
||||
// claims arbitrary, typically far-future heights — so evicting highest-first sheds
|
||||
// spam while preserving the near-tip pending band (NOT a blind LRU that could drop
|
||||
// a live block's epoch). The cap is reached only under attack: the watermark prune
|
||||
// keeps the steady-state map far below it. Caller holds vm.mu.
|
||||
func (vm *pChainHeightVM) enforceCapLocked() {
|
||||
for len(vm.heights) > pChainHeightMapCap {
|
||||
var victim ids.ID
|
||||
var maxHeight uint64
|
||||
first := true
|
||||
for id, e := range vm.heights {
|
||||
if first || e.chainHeight > maxHeight {
|
||||
victim, maxHeight, first = id, e.chainHeight, false
|
||||
}
|
||||
}
|
||||
delete(vm.heights, victim)
|
||||
}
|
||||
}
|
||||
|
||||
// currentPChainHeight returns the proposer's live P-chain height, the upper end
|
||||
// of the proposervm selectChildPChainHeight rule. A nil state or a read error
|
||||
// yields 0 (the genesis-set fallback): a height the proposer cannot resolve must
|
||||
// not be stamped onto a block, since a follower could not resolve the set there
|
||||
// either. A read error is symmetric across nodes for a committed P-chain, so the
|
||||
// fallback is uniform.
|
||||
func (vm *pChainHeightVM) currentPChainHeight(ctx context.Context) uint64 {
|
||||
if vm.state == nil {
|
||||
return 0
|
||||
}
|
||||
h, err := vm.state.GetCurrentHeight(ctx)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// parentHeight returns the P-chain epoch height stamped on parentID, or 0 if it
|
||||
// is unknown — used to keep a child's stamped height monotone ≥ its parent's
|
||||
// (selectChildPChainHeight). An unknown parent (0) cannot LOWER the child below
|
||||
// the current height (the max below), so monotonicity is preserved fail-soft.
|
||||
func (vm *pChainHeightVM) parentHeight(parentID ids.ID) uint64 {
|
||||
h, _ := vm.recall(parentID)
|
||||
return h
|
||||
}
|
||||
|
||||
// BuildBlock builds the inner block and stamps it with the proposer's P-chain
|
||||
// epoch height H = max(currentPChainHeight, parentH). This is the proposer side
|
||||
// of the epoch: the one node building this block chooses H from its own live
|
||||
// P-chain view, and that H rides the gossiped bytes to every follower (which
|
||||
// adopt it, never recompute it). Monotone ≥ parent so a chain's epoch never goes
|
||||
// backwards across blocks (a cert at a lower epoch than its parent would bind a
|
||||
// stale set).
|
||||
func (vm *pChainHeightVM) BuildBlock(ctx context.Context) (block.Block, error) {
|
||||
inner, err := vm.inner.BuildBlock(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h := vm.currentPChainHeight(ctx)
|
||||
if ph := vm.parentHeight(inner.ParentID()); ph > h {
|
||||
h = ph
|
||||
}
|
||||
vm.remember(inner.ID(), h, inner.Height())
|
||||
return newPChainHeightBlock(vm, inner, h), nil
|
||||
}
|
||||
|
||||
// ParseBlock recovers a block from transport bytes. Wrapped bytes
|
||||
// ([magic|H|innerBytes]) yield a block whose PChainHeight() is the EXACT H the
|
||||
// proposer stamped — the determinism guarantee: every node parsing the same
|
||||
// gossiped bytes recovers the identical epoch height. Bytes without the magic are
|
||||
// a raw inner block (a pre-wrapper peer, GetAncestors, genesis): parsed straight
|
||||
// through with H=0, the SAFE genesis-set fallback.
|
||||
//
|
||||
// FRAME DISCRIMINATOR (LOW-2). The 4-byte magic collides at 2^-32 with the raw
|
||||
// hash prefix some VMs use as their Bytes() (dexvm/quantumvm/dchain serialize
|
||||
// parentID[0:32]||…). The magic match alone is NOT sufficient: a frame is real
|
||||
// only if the inner re-parse of the framed payload ALSO succeeds. So when the
|
||||
// prefix matches, attempt to parse b[header:] as an inner block; if that FAILS,
|
||||
// fall back to parsing the WHOLE b as a raw inner block (the colliding raw block,
|
||||
// whose bytes minus the 12-byte prefix are not a valid inner block). This removes
|
||||
// the bootstrap stall a magic collision used to cause (mis-framed → inner decode
|
||||
// fails closed → stall on that chain).
|
||||
//
|
||||
// RECENCY GATE (HIGH-1, predicate b). A real frame's stamped epoch H must be
|
||||
// recent relative to THIS node's live P-chain height: H ≤ localCurrentH + slack.
|
||||
// An absurd-future H (a Byzantine proposer claiming an epoch the network has not
|
||||
// reached) is rejected fail-closed (the block is dropped, never tracked). This is
|
||||
// the upper half of the epoch bound; the engine's monotone gate is the lower half
|
||||
// (≥ parent's recorded epoch), so the epoch is pinned to [parentEpoch, localH+slack].
|
||||
func (vm *pChainHeightVM) ParseBlock(ctx context.Context, b []byte) (block.Block, error) {
|
||||
candidateInner, h, magicMatched := unwrapPChainHeight(b)
|
||||
if magicMatched {
|
||||
// The prefix looks like a frame. It IS a frame only if the framed payload
|
||||
// parses as an inner block AND the stamped epoch is recent.
|
||||
if inner, err := vm.inner.ParseBlock(ctx, candidateInner); err == nil {
|
||||
if !vm.epochRecent(ctx, h) {
|
||||
return nil, errPChainHeightNotRecent
|
||||
}
|
||||
vm.remember(inner.ID(), h, inner.Height())
|
||||
return newPChainHeightBlock(vm, inner, h), nil
|
||||
}
|
||||
// Magic matched but the framed payload did NOT parse → this is a RAW block
|
||||
// whose first bytes coincidentally equal the magic. Parse the whole b raw.
|
||||
}
|
||||
// Raw inner block: no proposer epoch available → genesis-set fallback. Still
|
||||
// wrap (uniform block type to the engine) but with H=0 and a passthrough
|
||||
// Bytes() so re-gossip of a raw block stays raw.
|
||||
inner, err := vm.inner.ParseBlock(ctx, b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newRawPChainHeightBlock(vm, inner), nil
|
||||
}
|
||||
|
||||
// epochRecent reports whether a stamped P-chain epoch height is within the recency
|
||||
// slack of this node's live P-chain height (HIGH-1, predicate b). A node with no
|
||||
// resolvable current height (state nil / read error → 0) cannot bound recency, so
|
||||
// it admits the block (the verifier still fails closed on an unresolvable future
|
||||
// epoch at set-resolution time — recency here is a fast-fail/DoS sanity gate, not
|
||||
// the safety bound). Heights at or below local are always recent.
|
||||
func (vm *pChainHeightVM) epochRecent(ctx context.Context, h uint64) bool {
|
||||
localH := vm.currentPChainHeight(ctx)
|
||||
if localH == 0 {
|
||||
return true
|
||||
}
|
||||
return h <= localH+pChainHeightRecencySlack
|
||||
}
|
||||
|
||||
// GetBlock fetches a block from the inner VM and re-attaches the P-chain height
|
||||
// it was stamped with (recalled from build/parse). A miss yields H=0 — acceptable
|
||||
// because GetBlock is not on the engine's finality-capture path (see the heights
|
||||
// field doc). The returned block's Bytes() is the inner bytes (the inner VM's
|
||||
// canonical at-rest form); a stamped height is re-attached for the engine's
|
||||
// in-memory use only.
|
||||
func (vm *pChainHeightVM) GetBlock(ctx context.Context, id ids.ID) (block.Block, error) {
|
||||
inner, err := vm.inner.GetBlock(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if h, ok := vm.recall(id); ok {
|
||||
return newRawPChainHeightBlockWithHeight(vm, inner, h), nil
|
||||
}
|
||||
return newRawPChainHeightBlock(vm, inner), nil
|
||||
}
|
||||
|
||||
// LastAccepted delegates to the inner VM.
|
||||
func (vm *pChainHeightVM) LastAccepted(ctx context.Context) (ids.ID, error) {
|
||||
return vm.inner.LastAccepted(ctx)
|
||||
}
|
||||
|
||||
// SetPreference delegates to the inner VM.
|
||||
func (vm *pChainHeightVM) SetPreference(ctx context.Context, id ids.ID) error {
|
||||
return vm.inner.SetPreference(ctx, id)
|
||||
}
|
||||
|
||||
// wrapPChainHeight frames inner bytes with the proposer's P-chain height:
|
||||
// magic(4) || height(8,BE) || innerBytes. The header is fixed-width so unwrap is
|
||||
// a constant-time prefix read.
|
||||
func wrapPChainHeight(innerBytes []byte, height uint64) []byte {
|
||||
out := make([]byte, pChainHeightHeaderLen+len(innerBytes))
|
||||
copy(out[0:4], pChainHeightMagic[:])
|
||||
binary.BigEndian.PutUint64(out[4:12], height)
|
||||
copy(out[pChainHeightHeaderLen:], innerBytes)
|
||||
return out
|
||||
}
|
||||
|
||||
// unwrapPChainHeight is a PURE prefix splitter: it reports whether b carries the
|
||||
// frame magic at the header width and, if so, returns the candidate payload and
|
||||
// the encoded height. magicMatched==true means ONLY that the prefix looks like a
|
||||
// frame — NOT that b is definitely framed. ParseBlock makes the real decision by
|
||||
// re-parsing the candidate payload (LOW-2): a raw block whose first bytes collide
|
||||
// with the magic (2^-32) yields magicMatched==true here but its payload fails the
|
||||
// inner re-parse, so ParseBlock falls back to a raw whole-b parse. Keeping this a
|
||||
// pure split (no VM dependency) localizes the collision handling to ParseBlock.
|
||||
func unwrapPChainHeight(b []byte) (payload []byte, height uint64, magicMatched bool) {
|
||||
if len(b) < pChainHeightHeaderLen ||
|
||||
b[0] != pChainHeightMagic[0] || b[1] != pChainHeightMagic[1] ||
|
||||
b[2] != pChainHeightMagic[2] || b[3] != pChainHeightMagic[3] {
|
||||
return b, 0, false
|
||||
}
|
||||
height = binary.BigEndian.Uint64(b[4:12])
|
||||
return b[pChainHeightHeaderLen:], height, true
|
||||
}
|
||||
|
||||
// --- the wrapped block -------------------------------------------------------
|
||||
|
||||
// pChainHeightBlock is a chain block that carries a P-chain epoch height for the
|
||||
// engine while delegating identity, verification, and acceptance to the inner VM
|
||||
// block. It satisfies consensus block.SignedBlock's PChainHeight() (the subset
|
||||
// the engine reads via pChainHeightOf), so the engine records the real epoch
|
||||
// height — every other behaviour is the inner VM's.
|
||||
//
|
||||
// `bytes` is what the engine gossips. For a proposer-built / wrapped-parsed block
|
||||
// it is the framed [magic|H|innerBytes] so a follower recovers H; for a raw block
|
||||
// (genesis-set fallback) it is the inner bytes verbatim (passthrough) so re-gossip
|
||||
// stays raw.
|
||||
type pChainHeightBlock struct {
|
||||
vm *pChainHeightVM
|
||||
inner block.Block
|
||||
pChainHeight uint64
|
||||
bytes []byte
|
||||
}
|
||||
|
||||
// newPChainHeightBlock wraps inner with height h and a FRAMED Bytes()
|
||||
// ([magic|h|inner]) — the proposer/parse path, where H must travel to followers.
|
||||
func newPChainHeightBlock(vm *pChainHeightVM, inner block.Block, h uint64) *pChainHeightBlock {
|
||||
return &pChainHeightBlock{
|
||||
vm: vm,
|
||||
inner: inner,
|
||||
pChainHeight: h,
|
||||
bytes: wrapPChainHeight(inner.Bytes(), h),
|
||||
}
|
||||
}
|
||||
|
||||
// newRawPChainHeightBlock wraps inner with H=0 and a PASSTHROUGH Bytes() (the
|
||||
// inner bytes verbatim) — the genesis-set fallback for a raw inner block.
|
||||
func newRawPChainHeightBlock(vm *pChainHeightVM, inner block.Block) *pChainHeightBlock {
|
||||
return &pChainHeightBlock{vm: vm, inner: inner, pChainHeight: 0, bytes: inner.Bytes()}
|
||||
}
|
||||
|
||||
// newRawPChainHeightBlockWithHeight re-attaches a recalled height to an inner
|
||||
// block fetched via GetBlock while keeping a PASSTHROUGH Bytes() (the inner VM's
|
||||
// at-rest bytes). Used off the finality-capture path; the height is for the
|
||||
// engine's in-memory epoch read, not for re-gossip framing.
|
||||
func newRawPChainHeightBlockWithHeight(vm *pChainHeightVM, inner block.Block, h uint64) *pChainHeightBlock {
|
||||
return &pChainHeightBlock{vm: vm, inner: inner, pChainHeight: h, bytes: inner.Bytes()}
|
||||
}
|
||||
|
||||
func (b *pChainHeightBlock) ID() ids.ID { return b.inner.ID() }
|
||||
func (b *pChainHeightBlock) Parent() ids.ID { return b.inner.Parent() }
|
||||
func (b *pChainHeightBlock) ParentID() ids.ID { return b.inner.ParentID() }
|
||||
func (b *pChainHeightBlock) Height() uint64 { return b.inner.Height() }
|
||||
func (b *pChainHeightBlock) Timestamp() time.Time { return b.inner.Timestamp() }
|
||||
func (b *pChainHeightBlock) Status() uint8 { return b.inner.Status() }
|
||||
func (b *pChainHeightBlock) Bytes() []byte { return b.bytes }
|
||||
|
||||
// PChainHeight is the method the engine reads via pChainHeightOf — the SOLE
|
||||
// reason this wrapper exists. The inner block does not expose it; this does.
|
||||
func (b *pChainHeightBlock) PChainHeight() uint64 { return b.pChainHeight }
|
||||
|
||||
// Verify/Reject delegate to the inner VM block: the wrapper changes the epoch the
|
||||
// engine SEES, never the block's validity or state transition.
|
||||
func (b *pChainHeightBlock) Verify(ctx context.Context) error { return b.inner.Verify(ctx) }
|
||||
func (b *pChainHeightBlock) Reject(ctx context.Context) error { return b.inner.Reject(ctx) }
|
||||
|
||||
// Accept finalizes the inner block, then advances the VM's finalized-height
|
||||
// watermark and prunes the heights map at/below it (MEDIUM-3). The inner Accept
|
||||
// runs FIRST: finalization must not depend on the bookkeeping prune, and the prune
|
||||
// is a pure memory-management side effect that can never fail. The block carries
|
||||
// its own height, so the VM learns the exact finalized height with no extra read.
|
||||
func (b *pChainHeightBlock) Accept(ctx context.Context) error {
|
||||
if err := b.inner.Accept(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if b.vm != nil {
|
||||
b.vm.onAccepted(b.inner.Height())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unwrap exposes the inner block for code that must reach the underlying VM block
|
||||
// (e.g. a missing-context reparse). Kept narrow on purpose.
|
||||
func (b *pChainHeightBlock) Unwrap() block.Block { return b.inner }
|
||||
@@ -1,375 +0,0 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// quorum.go — the node-layer wiring that makes the consensus engine's α-of-K
|
||||
// quorum-cert finality LIVE (HIGH-4). The consensus engine (luxfi/consensus
|
||||
// engine/chain) defines the quorum RULE and the vote/cert topology interfaces;
|
||||
// THIS file supplies the concrete node implementations the engine needs:
|
||||
//
|
||||
// - blsVoteVerifier — verifies a validator's BLS signature over the canonical
|
||||
// vote message against the chain's validator set (the engine is scheme-
|
||||
// agnostic; the node injects BLS).
|
||||
// - blsVoteSigner — signs THIS node's accept votes with its staking BLS key
|
||||
// so its signature can be collected into a cert.
|
||||
// - validatorStakeSource — supplies per-validator stake so finality is a
|
||||
// ⅔-by-STAKE supermajority (HIGH-3), not a raw voter count.
|
||||
// - networkGossiper.BroadcastVote / GossipCert — the QuorumGossiper transport:
|
||||
// a follower broadcasts its signed vote to ALL validators and any node that
|
||||
// collects α distinct signed votes gossips the assembled cert, so finality
|
||||
// never hinges on one node's inbound Chits (liveness; no proposer-freeze).
|
||||
//
|
||||
// Inbound votes/certs arrive as app-gossip and are demuxed in blockHandler.Gossip
|
||||
// (see manager.go) into engine.HandleIncomingVote / HandleIncomingCert.
|
||||
package chains
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"sort"
|
||||
|
||||
consensusconfig "github.com/luxfi/consensus/config"
|
||||
consensuschain "github.com/luxfi/consensus/engine/chain"
|
||||
"github.com/luxfi/constants"
|
||||
"github.com/luxfi/crypto/bls"
|
||||
"github.com/luxfi/ids"
|
||||
validators "github.com/luxfi/validators"
|
||||
)
|
||||
|
||||
// isLocalDevNetwork reports whether networkID is an explicitly-local developer
|
||||
// network: devnet (3) or localnet (1337). These are EXACT IDs (per luxfi/constants
|
||||
// convention), NOT a range — a custom value L1 with a high networkID (e.g. an
|
||||
// L1 whose chainID == networkID) is a VALUE network and must NOT match here.
|
||||
// Local dev networks are the only IDs that run the minimal-BFT committee
|
||||
// (LocalBFTParams, K=4) instead of the large-committee Default (K=20), because a
|
||||
// handful of localhost validators cannot reach an α=14-of-K=20 quorum.
|
||||
func isLocalDevNetwork(networkID uint32) bool {
|
||||
return networkID == constants.DevnetID || networkID == constants.LocalID
|
||||
}
|
||||
|
||||
// selectConsensusParams picks the consensus parameters for a chain.
|
||||
//
|
||||
// - sybilProtection == false (--dev / single-node): K=1, the sole validator's
|
||||
// accept is the 1-of-1 quorum (no peer signatures).
|
||||
// - sybilProtection == true, VALUE network: a large BYZANTINE-fault-tolerant
|
||||
// param set — NEVER LocalParams() (K=3/α=2, f=0, which a single Byzantine
|
||||
// validator forks; this was CRITICAL-2). Mainnet→K=21, Testnet→K=11, every
|
||||
// other value net→Default K=20.
|
||||
// - sybilProtection == true, LOCAL DEV network (devnet 3 / localnet 1337):
|
||||
// LocalBFTParams() (K=4/α=3, f=1) — the MINIMAL real-BFT committee. Default
|
||||
// K=20 is unsatisfiable on a few localhost validators: α=14 affirmative votes
|
||||
// are unreachable with 3-4 validators, so no block ever finalizes and the
|
||||
// P-Chain freezes at height 0 (no C-Chain is ever created). K=4 makes quorum
|
||||
// reachable (3 of 4) while staying genuinely BFT — it still clears
|
||||
// ValidateForValueNetwork (K≥4, f≥1) and the CRITICAL-2 multi-node-is-BFT
|
||||
// regression, so production safety is untouched. (A local devnet should run
|
||||
// ≥4 validators to realise f=1; with 3 it degrades to near-unanimous f=0,
|
||||
// which is safe though not live under a fault.)
|
||||
//
|
||||
// All branches satisfy the 2α−K ≥ f+1 overlap bound; the manager call site also
|
||||
// asserts ValidateForValueNetwork as a fail-closed backstop (K=4 passes it).
|
||||
func selectConsensusParams(sybilProtection bool, networkID uint32) consensusconfig.Parameters {
|
||||
if !sybilProtection {
|
||||
return consensusconfig.SingleValidatorParams()
|
||||
}
|
||||
switch networkID {
|
||||
case constants.MainnetID:
|
||||
return consensusconfig.MainnetParams()
|
||||
case constants.TestnetID:
|
||||
return consensusconfig.TestnetParams()
|
||||
default:
|
||||
if isLocalDevNetwork(networkID) {
|
||||
return consensusconfig.LocalBFTParams()
|
||||
}
|
||||
return consensusconfig.DefaultParams()
|
||||
}
|
||||
}
|
||||
|
||||
// --- BLS vote verifier -------------------------------------------------------
|
||||
|
||||
// blsVoteVerifier verifies a validator's BLS signature over the canonical vote
|
||||
// message. The validator's BLS public key is resolved FROM THE HEIGHT-INDEXED
|
||||
// validators.State AT THE BLOCK'S P-CHAIN EPOCH HEIGHT (RESIDUAL-B) — the SAME
|
||||
// height-pinned source the set-root and the ⅔-by-stake tally read from
|
||||
// (validatorSetAtHeight). It is NOT resolved from the CURRENT validator map.
|
||||
//
|
||||
// Why the epoch, not the current map: during an async validator-set change a
|
||||
// validator can be present in the set@H (it legitimately signed the block being
|
||||
// voted on at height H) yet ALREADY GONE from the current map. Resolving its
|
||||
// pubkey from the current map drops its valid vote, and if that validator holds
|
||||
// >⅓ of the stake-at-H the stable set falls below ⅔ and block H NEVER finalizes
|
||||
// (this is not self-healing for that block — the skew is permanent for H). Pinning
|
||||
// pubkey resolution to set@H — alongside membership, set-root, and stake — makes
|
||||
// the cert internally consistent at exactly one epoch.
|
||||
//
|
||||
// An unknown validator at the epoch, a validator with no BLS key, or a bad
|
||||
// signature all yield false (never an error/panic) — a cert with such a voter is
|
||||
// simply invalid, the fail-closed contract the engine requires. A nil state (the
|
||||
// no-op on a non-quorum node) yields false for every voter; a K>1 chain is
|
||||
// guarded against ever reaching here with a nil/no-op state (manager.go).
|
||||
type blsVoteVerifier struct {
|
||||
state validators.State
|
||||
networkID ids.ID
|
||||
}
|
||||
|
||||
func newBLSVoteVerifier(state validators.State, networkID ids.ID) *blsVoteVerifier {
|
||||
return &blsVoteVerifier{state: state, networkID: networkID}
|
||||
}
|
||||
|
||||
// VerifyVote implements consensuschain.VoteVerifier. epochHeight is the block's
|
||||
// P-chain height; the voter's pubkey is read from the set IN FORCE AT that height.
|
||||
func (v *blsVoteVerifier) VerifyVote(nodeID ids.NodeID, message []byte, sig []byte, epochHeight uint64) bool {
|
||||
out, ok := validatorSetAtHeight(v.state, v.networkID, epochHeight)[nodeID]
|
||||
if !ok || out == nil || len(out.PublicKey) == 0 {
|
||||
return false
|
||||
}
|
||||
pk, err := bls.PublicKeyFromCompressedBytes(out.PublicKey)
|
||||
if err != nil || pk == nil {
|
||||
return false
|
||||
}
|
||||
signature, err := bls.SignatureFromBytes(sig)
|
||||
if err != nil || signature == nil {
|
||||
return false
|
||||
}
|
||||
return bls.Verify(pk, signature, message)
|
||||
}
|
||||
|
||||
var _ consensuschain.VoteVerifier = (*blsVoteVerifier)(nil)
|
||||
|
||||
// --- BLS vote signer ---------------------------------------------------------
|
||||
|
||||
// blsVoteSigner signs this node's accept votes with its staking BLS key.
|
||||
type blsVoteSigner struct {
|
||||
signer bls.Signer
|
||||
}
|
||||
|
||||
func newBLSVoteSigner(signer bls.Signer) *blsVoteSigner {
|
||||
if signer == nil {
|
||||
return nil
|
||||
}
|
||||
return &blsVoteSigner{signer: signer}
|
||||
}
|
||||
|
||||
// SignVote implements consensuschain.VoteSigner.
|
||||
func (s *blsVoteSigner) SignVote(message []byte) ([]byte, error) {
|
||||
sig, err := s.signer.Sign(message)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bls.SignatureToBytes(sig), nil
|
||||
}
|
||||
|
||||
var _ consensuschain.VoteSigner = (*blsVoteSigner)(nil)
|
||||
|
||||
// --- height-pinned epoch read (MEDIUM-1) -------------------------------------
|
||||
|
||||
// validatorSetAtHeight reads the validator set IN FORCE AT a value-chain height
|
||||
// from the height-indexed validators.State. This is the SINGLE source of epoch
|
||||
// truth shared by the stake source and the set-root source: both membership and
|
||||
// weights are read at the SAME height H so a cert's signed set-root and its
|
||||
// ⅔-by-stake tally are measured against the identical set.
|
||||
//
|
||||
// Determinism across nodes is the whole point. validators.State.GetValidatorSet
|
||||
// returns the set the network already agreed on at height H (P-chain / L1-staking
|
||||
// consensus), so every honest node computes the same set — and therefore the
|
||||
// same set-root and the same tally — for a given H, INDEPENDENT of async
|
||||
// current-map skew during a validator-set change. (The previous Manager.GetMap()
|
||||
// read hashed the CURRENT map, which diverges between the signer and the
|
||||
// assembler across that skew window → mismatched canonical messages → dropped
|
||||
// votes → finality stall at every staking change. That was MEDIUM-1.)
|
||||
//
|
||||
// A nil state, a lookup error, or an empty set yields a nil map, which the
|
||||
// callers fold into their fail-soft answers (0 weight / Empty root). An error is
|
||||
// SYMMETRIC across nodes (a committed height H reads the same on every node, or
|
||||
// fails the same way), so the degraded answer is uniform — it never makes one
|
||||
// node's view disagree with another's, which is the property that matters here.
|
||||
func validatorSetAtHeight(state validators.State, networkID ids.ID, height uint64) map[ids.NodeID]*validators.GetValidatorOutput {
|
||||
if state == nil {
|
||||
return nil
|
||||
}
|
||||
set, err := state.GetValidatorSet(context.Background(), height, networkID)
|
||||
if err != nil || len(set) == 0 {
|
||||
return nil
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
// --- stake source (HIGH-3, height-pinned by MEDIUM-1) ------------------------
|
||||
|
||||
// validatorStakeSource supplies validator voting weights so the engine can
|
||||
// require a ⅔-by-stake supermajority for finality (HIGH-3). Weights are read
|
||||
// from the HEIGHT-INDEXED validators.State at the cert-position height, the same
|
||||
// height the set-root commits to (MEDIUM-1). Reading the tally at the same epoch
|
||||
// as the signed membership means a validator whose vote is in the cert (its
|
||||
// signature verifies against the height-H set-root) also contributes its height-H
|
||||
// weight to the tally — eliminating the second skew (a current-map weight read
|
||||
// could drop a legitimately-signed quorum when membership changed between sign
|
||||
// and tally).
|
||||
type validatorStakeSource struct {
|
||||
state validators.State
|
||||
networkID ids.ID
|
||||
}
|
||||
|
||||
func newValidatorStakeSource(state validators.State, networkID ids.ID) *validatorStakeSource {
|
||||
return &validatorStakeSource{state: state, networkID: networkID}
|
||||
}
|
||||
|
||||
// Weight implements consensuschain.StakeSource. Returns the validator's stake in
|
||||
// the set IN FORCE AT height — deterministic across nodes for a given height. An
|
||||
// unknown validator (or a fail-soft empty read) yields 0, which cannot inflate
|
||||
// the numerator.
|
||||
func (s *validatorStakeSource) Weight(nodeID ids.NodeID, height uint64) uint64 {
|
||||
out, ok := validatorSetAtHeight(s.state, s.networkID, height)[nodeID]
|
||||
if !ok || out == nil {
|
||||
return 0
|
||||
}
|
||||
return out.Light
|
||||
}
|
||||
|
||||
// TotalStake implements consensuschain.StakeSource. Total active stake of the set
|
||||
// IN FORCE AT height (the denominator of the ⅔ predicate), measured at the same
|
||||
// epoch as Weight and the set-root.
|
||||
func (s *validatorStakeSource) TotalStake(height uint64) uint64 {
|
||||
var total uint64
|
||||
for _, out := range validatorSetAtHeight(s.state, s.networkID, height) {
|
||||
if out != nil {
|
||||
total += out.Light
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
var _ consensuschain.StakeSource = (*validatorStakeSource)(nil)
|
||||
|
||||
// --- validator-set-root source (MEDIUM: epoch binding) -----------------------
|
||||
|
||||
// validatorSetRootSource computes the deterministic commitment to the validator
|
||||
// set IN FORCE AT a value-chain height — the value the engine stamps into every
|
||||
// vote's VotePosition.ValidatorSetRoot so a cert is pinned to the exact set it
|
||||
// was certified under. It is the node side of the MEDIUM fix: it turns the
|
||||
// "⅔-by-stake measured at the cert-position epoch" property into an ENFORCED
|
||||
// invariant (a cross-epoch cert fails verification because every signature was
|
||||
// over this root).
|
||||
//
|
||||
// HEIGHT-PINNED (MEDIUM-1). The root is read from the HEIGHT-INDEXED
|
||||
// validators.State at the value-chain block height, NOT from the Manager's
|
||||
// CURRENT map. At a given height H, GetValidatorSet returns the set the network
|
||||
// already agreed on, so every honest node — signer and assembler alike — computes
|
||||
// the IDENTICAL root for H, independent of async current-map skew during a
|
||||
// validator-set change. Reading the current map (the prior bug) let the signer
|
||||
// and the assembler hold different maps across that skew window → different roots
|
||||
// → the canonical signed message differed → signatures failed verification →
|
||||
// votes dropped → finality stalled at every staking change.
|
||||
//
|
||||
// The commitment is a SHA-256 over the set serialized in a canonical order
|
||||
// (validators sorted by NodeID, each as nodeID || light || len(pubkey) ||
|
||||
// pubkey) — see hashValidatorSet. Sorting by NodeID + length-prefixing the
|
||||
// pubkey makes the encoding canonical and unambiguous; the byte layout is
|
||||
// UNCHANGED from the prior implementation, so the wire format and the engine's
|
||||
// epoch-binding contract are preserved (only the SOURCE of the set changed from
|
||||
// the current map to the height-indexed set).
|
||||
type validatorSetRootSource struct {
|
||||
state validators.State
|
||||
networkID ids.ID
|
||||
}
|
||||
|
||||
func newValidatorSetRootSource(state validators.State, networkID ids.ID) *validatorSetRootSource {
|
||||
return &validatorSetRootSource{state: state, networkID: networkID}
|
||||
}
|
||||
|
||||
// ValidatorSetRoot implements consensuschain.ValidatorSetRootSource. Returns the
|
||||
// commitment to the weighted set IN FORCE AT height (deterministic across nodes).
|
||||
// Returns ids.Empty when the state is absent or the set is empty (the explicit
|
||||
// "unbound" answer, consistent with the engine default); a height-read error is
|
||||
// symmetric across nodes, so the Empty fallback is uniform and never creates a
|
||||
// cross-node root disagreement.
|
||||
func (s *validatorSetRootSource) ValidatorSetRoot(height uint64) ids.ID {
|
||||
return hashValidatorSet(validatorSetAtHeight(s.state, s.networkID, height))
|
||||
}
|
||||
|
||||
var _ consensuschain.ValidatorSetRootSource = (*validatorSetRootSource)(nil)
|
||||
|
||||
// hashValidatorSet computes the canonical SHA-256 commitment to a weighted
|
||||
// validator set: validators sorted by NodeID, each serialized as
|
||||
// nodeID || light(8,BE) || len(pubkey)(8,BE) || pubkey. An empty/nil set commits
|
||||
// to ids.Empty (the "unbound" answer). This is the SINGLE definition of the
|
||||
// set-root encoding (DRY) — both the live source and its tests hash through here,
|
||||
// so the wire format cannot drift between them.
|
||||
func hashValidatorSet(set map[ids.NodeID]*validators.GetValidatorOutput) ids.ID {
|
||||
if len(set) == 0 {
|
||||
return ids.Empty
|
||||
}
|
||||
nodeIDs := make([]ids.NodeID, 0, len(set))
|
||||
for nodeID := range set {
|
||||
nodeIDs = append(nodeIDs, nodeID)
|
||||
}
|
||||
sort.Slice(nodeIDs, func(i, j int) bool {
|
||||
return bytes.Compare(nodeIDs[i][:], nodeIDs[j][:]) < 0
|
||||
})
|
||||
h := sha256.New()
|
||||
var u64 [8]byte
|
||||
for _, nodeID := range nodeIDs {
|
||||
v := set[nodeID]
|
||||
h.Write(nodeID[:])
|
||||
binary.BigEndian.PutUint64(u64[:], v.Light)
|
||||
h.Write(u64[:])
|
||||
binary.BigEndian.PutUint64(u64[:], uint64(len(v.PublicKey)))
|
||||
h.Write(u64[:])
|
||||
h.Write(v.PublicKey)
|
||||
}
|
||||
var root ids.ID
|
||||
copy(root[:], h.Sum(nil))
|
||||
return root
|
||||
}
|
||||
|
||||
// --- app-gossip envelope for votes/certs -------------------------------------
|
||||
|
||||
// The QuorumGossiper transport rides on app-gossip. A single framed envelope
|
||||
// carries either a signed vote or an assembled cert. The receiver (blockHandler
|
||||
// .Gossip) demuxes on the magic + kind and routes to the engine. A payload
|
||||
// without the magic is a plain block gossip (legacy Put path), so the demux is
|
||||
// backward-compatible.
|
||||
//
|
||||
// Layout (big-endian):
|
||||
//
|
||||
// magic:4 ("LXQ\x01") kind:1 blockID:32 payload:...
|
||||
//
|
||||
// kind 1 = signed vote (payload = engine encodeSignedVote: nodeID+sig)
|
||||
// kind 2 = finality cert (payload = engine cert MarshalBinary)
|
||||
var quorumGossipMagic = [4]byte{'L', 'X', 'Q', 0x01}
|
||||
|
||||
const (
|
||||
quorumKindVote byte = 1
|
||||
quorumKindCert byte = 2
|
||||
)
|
||||
|
||||
// ErrNotQuorumGossip signals a payload is not a quorum envelope (so the caller
|
||||
// falls through to the block-gossip path).
|
||||
var ErrNotQuorumGossip = errors.New("chains: not a quorum gossip envelope")
|
||||
|
||||
// encodeQuorumGossip frames a vote/cert payload for app-gossip.
|
||||
func encodeQuorumGossip(kind byte, blockID ids.ID, payload []byte) []byte {
|
||||
buf := make([]byte, 0, 4+1+32+len(payload))
|
||||
buf = append(buf, quorumGossipMagic[:]...)
|
||||
buf = append(buf, kind)
|
||||
buf = append(buf, blockID[:]...)
|
||||
buf = append(buf, payload...)
|
||||
return buf
|
||||
}
|
||||
|
||||
// decodeQuorumGossip parses an envelope. Returns ErrNotQuorumGossip if the magic
|
||||
// is absent (a normal block gossip) — fail-soft so the legacy path is preserved.
|
||||
func decodeQuorumGossip(data []byte) (kind byte, blockID ids.ID, payload []byte, err error) {
|
||||
if len(data) < 4+1+32 || [4]byte{data[0], data[1], data[2], data[3]} != quorumGossipMagic {
|
||||
return 0, ids.Empty, nil, ErrNotQuorumGossip
|
||||
}
|
||||
kind = data[4]
|
||||
copy(blockID[:], data[5:5+32])
|
||||
payload = data[5+32:]
|
||||
if kind != quorumKindVote && kind != quorumKindCert {
|
||||
return 0, ids.Empty, nil, ErrNotQuorumGossip
|
||||
}
|
||||
return kind, blockID, payload, nil
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// quorum_guard_node_test.go — CRITICAL-1(c) fail-closed guard + the wiring proof
|
||||
// the MEDIUM-1 round lacked. The round-1 tests injected a validators.State into
|
||||
// the source constructors directly and never exercised the path where
|
||||
// m.validatorState is nil (the production default until the P-Chain publishes
|
||||
// its State). These tests pin:
|
||||
//
|
||||
// (1) the guard predicate: a K>1 chain with NO height-indexed state is refused
|
||||
// (would otherwise stall finality forever);
|
||||
// (2) the failure mechanism: getValidatorState(nil) → the no-op State, whose
|
||||
// GetValidatorSet is EMPTY at every height → the stake source totals 0 and
|
||||
// the set-root is Empty (exactly the inputs that make VerifyWeighted fail
|
||||
// closed). This is what the guard exists to prevent silently;
|
||||
// (3) the fix: once a REAL height-indexed state is published, getValidatorState
|
||||
// returns it and the same sources read a live set (non-zero stake / non-Empty
|
||||
// root) — finality can proceed.
|
||||
package chains
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/ids"
|
||||
validators "github.com/luxfi/validators"
|
||||
"github.com/luxfi/validators/validatorstest"
|
||||
)
|
||||
|
||||
// TestQuorumGuard_RefusesNoopState pins the guard predicate (CRITICAL-1(c)): a
|
||||
// nil (unpublished) validator state is NOT live, so a K>1 chain must refuse to
|
||||
// start; a published state IS live.
|
||||
func TestQuorumGuard_RefusesNoopState(t *testing.T) {
|
||||
if quorumValidatorStateLive(nil) {
|
||||
t.Fatal("CRITICAL-1(c): a nil validator state must NOT be considered live (would stall finality)")
|
||||
}
|
||||
live := validatorstest.NewTestState()
|
||||
if !quorumValidatorStateLive(live) {
|
||||
t.Fatal("a published height-indexed validator state must be considered live")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetValidatorState_NoopIsEmptyAtEveryHeight proves the failure mechanism the
|
||||
// guard prevents: with no state published, getValidatorState yields the no-op
|
||||
// State, and the height-pinned sources read an EMPTY set at every height — zero
|
||||
// total stake and an Empty set-root, the exact inputs that make the engine's
|
||||
// VerifyWeighted fail closed (ErrQCStakeBelowSupermajority) so NO block finalizes.
|
||||
func TestGetValidatorState_NoopIsEmptyAtEveryHeight(t *testing.T) {
|
||||
netID := ids.GenerateTestID()
|
||||
|
||||
// Production default: validatorState unset → getValidatorState(nil) = no-op.
|
||||
noop := getValidatorState(nil)
|
||||
if noop == nil {
|
||||
t.Fatal("getValidatorState(nil) must return the no-op State, not nil")
|
||||
}
|
||||
|
||||
stake := newValidatorStakeSource(noop, netID)
|
||||
root := newValidatorSetRootSource(noop, netID)
|
||||
|
||||
for _, h := range []uint64{0, 1, 7, 1_000, 10_000_000} {
|
||||
if total := stake.TotalStake(h); total != 0 {
|
||||
t.Fatalf("no-op State must report zero total stake at height %d, got %d", h, total)
|
||||
}
|
||||
if r := root.ValidatorSetRoot(h); r != ids.Empty {
|
||||
t.Fatalf("no-op State must commit to Empty set-root at height %d, got %s", h, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetValidatorState_LiveStateReadsSet proves the fix: once a real
|
||||
// height-indexed state is published (as the P-Chain does), getValidatorState
|
||||
// returns it and the SAME sources read a live set — non-zero stake and a
|
||||
// non-Empty set-root — so the ⅔-by-stake finality predicate can be satisfied.
|
||||
func TestGetValidatorState_LiveStateReadsSet(t *testing.T) {
|
||||
netID := ids.GenerateTestID()
|
||||
n0, n1, n2 := ids.GenerateTestNodeID(), ids.GenerateTestNodeID(), ids.GenerateTestNodeID()
|
||||
|
||||
const H = uint64(7)
|
||||
live := validatorstest.NewTestState()
|
||||
live.GetValidatorSetF = func(_ context.Context, height uint64, gotNet ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
|
||||
if gotNet != netID || height != H {
|
||||
return map[ids.NodeID]*validators.GetValidatorOutput{}, nil
|
||||
}
|
||||
return map[ids.NodeID]*validators.GetValidatorOutput{
|
||||
n0: {NodeID: n0, PublicKey: []byte("pk0"), Light: 30, Weight: 30},
|
||||
n1: {NodeID: n1, PublicKey: []byte("pk1"), Light: 30, Weight: 30},
|
||||
n2: {NodeID: n2, PublicKey: []byte("pk2"), Light: 40, Weight: 40},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getValidatorState passes a non-nil state through unchanged.
|
||||
got := getValidatorState(live)
|
||||
stake := newValidatorStakeSource(got, netID)
|
||||
root := newValidatorSetRootSource(got, netID)
|
||||
|
||||
if total := stake.TotalStake(H); total != 100 {
|
||||
t.Fatalf("live State must report total stake 100 at the epoch, got %d", total)
|
||||
}
|
||||
if w := stake.Weight(n2, H); w != 40 {
|
||||
t.Fatalf("live State must report n2 weight 40 at the epoch, got %d", w)
|
||||
}
|
||||
if r := root.ValidatorSetRoot(H); r == ids.Empty {
|
||||
t.Fatal("live State must commit to a NON-Empty set-root at the epoch")
|
||||
}
|
||||
// A different height (no set) is still Empty/zero — the read is height-pinned.
|
||||
if stake.TotalStake(H+1) != 0 || root.ValidatorSetRoot(H+1) != ids.Empty {
|
||||
t.Fatal("live State read must be height-pinned (empty at a height with no set)")
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// quorum_params_test.go — CRITICAL-2 node-layer regression: the node must NEVER
|
||||
// wire a non-BFT consensus param set for a multi-validator (sybil-protected)
|
||||
// chain. The round-1 hole was manager.go selecting LocalParams() (K=3/α=2 → f=0,
|
||||
// CFT) for ALL multi-node nets — a single Byzantine validator forks K=3/α=2.
|
||||
// These tests pin selectConsensusParams to a BFT-safe set for every multi-node
|
||||
// network and prove the value-network backstop (ValidateForValueNetwork) accepts
|
||||
// the selected params.
|
||||
package chains
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
consensusconfig "github.com/luxfi/consensus/config"
|
||||
"github.com/luxfi/constants"
|
||||
)
|
||||
|
||||
// TestSelectConsensusParams_MultiNodeIsBFT proves that for EVERY multi-validator
|
||||
// (sybilProtection==true) network the node selects a Byzantine-fault-tolerant
|
||||
// param set (f≥1, i.e. K≥4) that also clears the value-network validator — and
|
||||
// that it is NEVER LocalParams (K=3) or any K<4 set. This is the node half of
|
||||
// CRITICAL-2 (the consensus half is config.ValidateForValueNetwork, tested in
|
||||
// the consensus module).
|
||||
func TestSelectConsensusParams_MultiNodeIsBFT(t *testing.T) {
|
||||
local := consensusconfig.LocalParams()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
networkID uint32
|
||||
}{
|
||||
{"mainnet", constants.MainnetID},
|
||||
{"testnet", constants.TestnetID},
|
||||
{"localnet-multinode", constants.LocalID},
|
||||
{"unittest-multinode", constants.UnitTestID},
|
||||
{"arbitrary-multinode", 424242},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
p := selectConsensusParams(true /* sybilProtection */, tc.networkID)
|
||||
|
||||
// MUST be Byzantine-fault-tolerant: f≥1 ⟹ K≥4.
|
||||
if p.ByzantineFaultTolerance() < 1 {
|
||||
t.Fatalf("multi-node net %q got non-BFT params K=%d f=%d (CRITICAL-2: a single faulty validator forks)",
|
||||
tc.name, p.K, p.ByzantineFaultTolerance())
|
||||
}
|
||||
// MUST NOT be the CFT LocalParams (K=3/α=2) that was the round-1 hole.
|
||||
if p.K == local.K && p.AlphaPreference == local.AlphaPreference && p.K == 3 {
|
||||
t.Fatalf("multi-node net %q selected LocalParams (K=3/α=2) — the CRITICAL-2 fork config", tc.name)
|
||||
}
|
||||
// The selected params MUST themselves pass Valid() (the BFT α-floor:
|
||||
// 2·AlphaPreference − K ≥ f+1).
|
||||
if err := p.Valid(); err != nil {
|
||||
t.Fatalf("multi-node net %q selected params fail Valid(): %v (K=%d α=%d)",
|
||||
tc.name, err, p.K, p.AlphaPreference)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSelectConsensusParams_LocalDevIsSatisfiableBFT proves that an explicitly-
|
||||
// local dev network (devnet 3 / localnet 1337) selects the MINIMAL real-BFT set
|
||||
// (LocalBFTParams: K=4/α=3, f=1) — satisfiable by a handful of localhost
|
||||
// validators — and NOT the large Default (K=20/α=14), whose α=14 quorum is
|
||||
// unreachable with 3-4 validators (the freeze-at-height-0 bug). It must still be
|
||||
// genuinely BFT (f≥1) and pass the value backstop (K=4 ≥ 4), so production
|
||||
// safety / CRITICAL-2 is untouched: a custom VALUE L1 (high networkID) still gets
|
||||
// the large Default set, never the local one.
|
||||
func TestSelectConsensusParams_LocalDevIsSatisfiableBFT(t *testing.T) {
|
||||
for _, networkID := range []uint32{constants.DevnetID, constants.LocalID} {
|
||||
p := selectConsensusParams(true /* sybilProtection */, networkID)
|
||||
|
||||
// Satisfiable on a small committee: α must be small (K=4 → α=3), NOT 14.
|
||||
if p.K != 4 || p.AlphaPreference != 3 {
|
||||
t.Fatalf("local dev net %d: want minimal-BFT K=4/α=3, got K=%d/α=%d (Default K=20 is unsatisfiable on localhost)",
|
||||
networkID, p.K, p.AlphaPreference)
|
||||
}
|
||||
// Still genuinely BFT (f≥1) — does NOT regress CRITICAL-2.
|
||||
if p.ByzantineFaultTolerance() < 1 {
|
||||
t.Fatalf("local dev net %d: K=%d is not BFT (f=%d)", networkID, p.K, p.ByzantineFaultTolerance())
|
||||
}
|
||||
// Must clear the value backstop the manager asserts before engine start.
|
||||
if err := p.ValidateForValueNetwork(networkID); err != nil {
|
||||
t.Fatalf("local dev net %d: minimal-BFT params must pass the value backstop, got %v", networkID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// A custom value L1 with a high networkID is NOT a local dev network: it must
|
||||
// keep the large Default set (the isLocalDevNetwork predicate is EXACT, not a
|
||||
// >=1337 range that would wrongly catch value L1s).
|
||||
const customValueL1 = uint32(909090)
|
||||
if p := selectConsensusParams(true, customValueL1); p.K != consensusconfig.DefaultParams().K {
|
||||
t.Fatalf("custom value L1 %d must get Default K=%d, got K=%d (must NOT match the local-dev path)",
|
||||
customValueL1, consensusconfig.DefaultParams().K, p.K)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSelectConsensusParams_SingleNodeIsK1 proves --dev / sybil-disabled selects
|
||||
// the K=1 single-validator regime (the sole validator's accept is the 1-of-1
|
||||
// quorum) — and NOT a multi-node BFT set.
|
||||
func TestSelectConsensusParams_SingleNodeIsK1(t *testing.T) {
|
||||
p := selectConsensusParams(false /* sybilProtection */, constants.LocalID)
|
||||
if p.K != 1 {
|
||||
t.Fatalf("sybil-disabled (single-node) must select K=1, got K=%d", p.K)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSelectConsensusParams_ValueBackstop proves the params selected for a
|
||||
// multi-node net pass the STRICTER value-network validator for that net — the
|
||||
// fail-closed backstop asserted at the manager call site before starting the
|
||||
// engine. (Mainnet enforces K≥11, so MainnetParams K=21 passes; Default K=20
|
||||
// passes for an arbitrary value net.)
|
||||
func TestSelectConsensusParams_ValueBackstop(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
networkID uint32
|
||||
}{
|
||||
{"mainnet", constants.MainnetID},
|
||||
{"arbitrary-value-net", 909090},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
p := selectConsensusParams(true, tc.networkID)
|
||||
if err := p.ValidateForValueNetwork(tc.networkID); err != nil {
|
||||
t.Fatalf("selected params for value net %q must pass the value backstop, got %v (K=%d)",
|
||||
tc.name, err, p.K)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,293 +0,0 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// quorum_setroot_test.go — node-layer tests for MEDIUM-1: the set-root and the
|
||||
// ⅔-by-stake tally MUST be read from the HEIGHT-INDEXED validators.State at the
|
||||
// cert-position height, so every node computes the IDENTICAL root, weights, and
|
||||
// total for a given value-chain height — INDEPENDENT of async current-map skew
|
||||
// during a validator-set change.
|
||||
//
|
||||
// The cross-node test (TestValidatorSetRoot_CrossNodeAgreesDespiteSkew) is the
|
||||
// one the red flagged as MISSING: it models two nodes whose CURRENT validator
|
||||
// views diverge (a set-change in flight) but who agree on the historical set at a
|
||||
// pinned height H — and proves they nonetheless compute the SAME set-root at H.
|
||||
// Reading the current map (the prior bug) would have made their roots differ →
|
||||
// mismatched canonical signed messages → dropped votes → finality stall.
|
||||
package chains
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/ids"
|
||||
validators "github.com/luxfi/validators"
|
||||
"github.com/luxfi/validators/validatorstest"
|
||||
)
|
||||
|
||||
// expectedSetRoot is an INDEPENDENT reimplementation of the canonical set-root
|
||||
// spec, used only by the golden test to cross-check hashValidatorSet. It is
|
||||
// deliberately NOT a call to hashValidatorSet (that would be a tautology): if the
|
||||
// production encoding ever diverges from this spec the golden test fails.
|
||||
func expectedSetRoot(t *testing.T, vdrs []vdr) ids.ID {
|
||||
t.Helper()
|
||||
sort.Slice(vdrs, func(i, j int) bool {
|
||||
return bytes.Compare(vdrs[i].nodeID[:], vdrs[j].nodeID[:]) < 0
|
||||
})
|
||||
h := sha256.New()
|
||||
var u64 [8]byte
|
||||
for _, v := range vdrs {
|
||||
h.Write(v.nodeID[:])
|
||||
binary.BigEndian.PutUint64(u64[:], v.light)
|
||||
h.Write(u64[:])
|
||||
binary.BigEndian.PutUint64(u64[:], uint64(len(v.pk)))
|
||||
h.Write(u64[:])
|
||||
h.Write(v.pk)
|
||||
}
|
||||
var root ids.ID
|
||||
copy(root[:], h.Sum(nil))
|
||||
return root
|
||||
}
|
||||
|
||||
// vdr is a tiny height→set fixture: which validators (and weights/keys) the
|
||||
// height-indexed state reports at each value-chain height.
|
||||
type vdr struct {
|
||||
nodeID ids.NodeID
|
||||
pk []byte
|
||||
light uint64
|
||||
}
|
||||
|
||||
// stateWithHistory builds a validators.State whose GetValidatorSet returns the
|
||||
// set registered for the requested height (and an empty set for unknown heights),
|
||||
// scoped to netID. This is the height-indexed source MEDIUM-1 reads from.
|
||||
func stateWithHistory(netID ids.ID, byHeight map[uint64][]vdr) *validatorstest.TestState {
|
||||
s := validatorstest.NewTestState()
|
||||
s.GetValidatorSetF = func(_ context.Context, height uint64, gotNet ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
|
||||
if gotNet != netID {
|
||||
return map[ids.NodeID]*validators.GetValidatorOutput{}, nil
|
||||
}
|
||||
out := make(map[ids.NodeID]*validators.GetValidatorOutput)
|
||||
for _, v := range byHeight[height] {
|
||||
out[v.nodeID] = &validators.GetValidatorOutput{
|
||||
NodeID: v.nodeID,
|
||||
PublicKey: v.pk,
|
||||
Light: v.light,
|
||||
Weight: v.light,
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// TestValidatorSetRoot_CrossNodeAgreesDespiteSkew is the MEDIUM-1 regression
|
||||
// guard. Two nodes are mid validator-set change: their CURRENT sets differ
|
||||
// (height 11 vs height 12), but both still serve the SAME committed set at the
|
||||
// value-chain block height H=10 being voted on. The set-root at H MUST be
|
||||
// identical on both nodes — that identity is what makes their signatures over the
|
||||
// block's canonical message mutually verifiable. The prior current-map read would
|
||||
// have produced two different roots here and stalled finality.
|
||||
func TestValidatorSetRoot_CrossNodeAgreesDespiteSkew(t *testing.T) {
|
||||
netID := ids.GenerateTestID()
|
||||
n0, n1, n2, n3 := ids.GenerateTestNodeID(), ids.GenerateTestNodeID(), ids.GenerateTestNodeID(), ids.GenerateTestNodeID()
|
||||
pk0, pk1, pk2, pk3 := []byte("pk-0-48bytes-placeholder"), []byte("pk-1"), []byte("pk-2"), []byte("pk-3")
|
||||
|
||||
const H = uint64(10)
|
||||
setAtH := []vdr{{n0, pk0, 10}, {n1, pk1, 20}, {n2, pk2, 30}}
|
||||
|
||||
// Node A has already applied a stake bump that took effect at height 11
|
||||
// (n1: 20→25) and sees that as its current set. It still knows the height-10
|
||||
// set (the block being voted on).
|
||||
nodeA := stateWithHistory(netID, map[uint64][]vdr{
|
||||
H: setAtH,
|
||||
11: {{n0, pk0, 10}, {n1, pk1, 25}, {n2, pk2, 30}},
|
||||
})
|
||||
// Node B has already applied a NEW validator that joined at height 12
|
||||
// (n3 added) — a different, later current set. It too still knows height 10.
|
||||
nodeB := stateWithHistory(netID, map[uint64][]vdr{
|
||||
H: setAtH,
|
||||
12: {{n0, pk0, 10}, {n1, pk1, 25}, {n2, pk2, 30}, {n3, pk3, 5}},
|
||||
})
|
||||
|
||||
rootA := newValidatorSetRootSource(nodeA, netID).ValidatorSetRoot(H)
|
||||
rootB := newValidatorSetRootSource(nodeB, netID).ValidatorSetRoot(H)
|
||||
|
||||
if rootA == ids.Empty {
|
||||
t.Fatal("set-root at the voted height must be non-Empty (the set is non-empty)")
|
||||
}
|
||||
if rootA != rootB {
|
||||
t.Fatalf("MEDIUM-1: cross-node set-root at height %d MUST be identical despite "+
|
||||
"current-set skew, got A=%s B=%s", H, rootA, rootB)
|
||||
}
|
||||
|
||||
// Sanity: had either node (wrongly) committed to its CURRENT set instead of
|
||||
// the height-H set, the roots WOULD differ — proving the test actually
|
||||
// exercises the skew (not a vacuous match).
|
||||
rootA_cur := newValidatorSetRootSource(nodeA, netID).ValidatorSetRoot(11)
|
||||
rootB_cur := newValidatorSetRootSource(nodeB, netID).ValidatorSetRoot(12)
|
||||
if rootA_cur == rootB_cur {
|
||||
t.Fatal("test is vacuous: the two nodes' CURRENT sets must differ to exercise the skew")
|
||||
}
|
||||
if rootA == rootA_cur {
|
||||
t.Fatal("test is vacuous: height-H set must differ from node A's current set")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidatorSetRoot_HeightSelectsEpoch proves the root is a deterministic
|
||||
// FUNCTION OF HEIGHT (not a fixed current-set snapshot): different heights with
|
||||
// different sets yield different roots, the same height always yields the same
|
||||
// root, and the encoding is insertion-order independent (canonical sort).
|
||||
func TestValidatorSetRoot_HeightSelectsEpoch(t *testing.T) {
|
||||
netID := ids.GenerateTestID()
|
||||
n0, n1, n2 := ids.GenerateTestNodeID(), ids.GenerateTestNodeID(), ids.GenerateTestNodeID()
|
||||
pk0, pk1, pk2 := []byte("pk-0-48bytes-placeholder"), []byte("pk-1"), []byte("pk-2")
|
||||
|
||||
state := stateWithHistory(netID, map[uint64][]vdr{
|
||||
10: {{n0, pk0, 10}, {n1, pk1, 20}, {n2, pk2, 30}},
|
||||
11: {{n0, pk0, 10}, {n1, pk1, 21}, {n2, pk2, 30}}, // n1 weight changed
|
||||
})
|
||||
src := newValidatorSetRootSource(state, netID)
|
||||
|
||||
root10 := src.ValidatorSetRoot(10)
|
||||
root11 := src.ValidatorSetRoot(11)
|
||||
if root10 == ids.Empty || root11 == ids.Empty {
|
||||
t.Fatal("non-empty sets must commit to non-Empty roots")
|
||||
}
|
||||
if root10 == root11 {
|
||||
t.Fatal("a different epoch (height with a changed weight) MUST yield a different root")
|
||||
}
|
||||
// Same height is stable (deterministic), repeatedly.
|
||||
if again := src.ValidatorSetRoot(10); again != root10 {
|
||||
t.Fatalf("set-root at a fixed height must be deterministic: %s != %s", again, root10)
|
||||
}
|
||||
|
||||
// Insertion-order independence: a state that lists the SAME height-10 members
|
||||
// in a different slice order yields the SAME root (canonical NodeID sort).
|
||||
reordered := stateWithHistory(netID, map[uint64][]vdr{
|
||||
10: {{n2, pk2, 30}, {n0, pk0, 10}, {n1, pk1, 20}},
|
||||
})
|
||||
if r := newValidatorSetRootSource(reordered, netID).ValidatorSetRoot(10); r != root10 {
|
||||
t.Fatalf("set-root must be member-order independent: %s != %s", r, root10)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidatorSetRoot_FailSoftIsUniform proves the fail-soft answers are
|
||||
// Empty/uniform (never a panic, never a per-node-divergent default): a nil state,
|
||||
// an unknown height (empty set), an unknown network, and a height-read error all
|
||||
// commit to ids.Empty. Uniformity is the safety property — a symmetric error
|
||||
// degrades every node to the same Empty root, never to disagreeing roots.
|
||||
func TestValidatorSetRoot_FailSoftIsUniform(t *testing.T) {
|
||||
netID := ids.GenerateTestID()
|
||||
|
||||
// nil state → Empty.
|
||||
if got := (&validatorSetRootSource{state: nil, networkID: netID}).ValidatorSetRoot(10); got != ids.Empty {
|
||||
t.Fatalf("nil state must commit to ids.Empty, got %s", got)
|
||||
}
|
||||
|
||||
// Known network, but a height with no registered set → Empty.
|
||||
state := stateWithHistory(netID, map[uint64][]vdr{
|
||||
10: {{ids.GenerateTestNodeID(), []byte("pk"), 10}},
|
||||
})
|
||||
if got := newValidatorSetRootSource(state, netID).ValidatorSetRoot(999); got != ids.Empty {
|
||||
t.Fatalf("unknown height must commit to ids.Empty, got %s", got)
|
||||
}
|
||||
|
||||
// Wrong network → empty set → Empty.
|
||||
if got := newValidatorSetRootSource(state, ids.GenerateTestID()).ValidatorSetRoot(10); got != ids.Empty {
|
||||
t.Fatalf("unknown network must commit to ids.Empty, got %s", got)
|
||||
}
|
||||
|
||||
// A height-read ERROR → Empty (and it is symmetric: the same error on every
|
||||
// node yields the same Empty root).
|
||||
errState := validatorstest.NewTestState()
|
||||
errState.GetValidatorSetF = func(_ context.Context, _ uint64, _ ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
|
||||
return nil, errors.New("state unavailable at height")
|
||||
}
|
||||
if got := newValidatorSetRootSource(errState, netID).ValidatorSetRoot(10); got != ids.Empty {
|
||||
t.Fatalf("a height-read error must commit to ids.Empty, got %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidatorStakeSource_HeightPinned proves the ⅔-by-stake tally is read at
|
||||
// the SAME height as the set-root (MEDIUM-1's second skew): Weight/TotalStake are
|
||||
// a deterministic function of height, so a validator whose vote is in a
|
||||
// height-H cert contributes its height-H weight — not whatever the current map
|
||||
// happens to hold after a membership change. This is what stops a current-map
|
||||
// weight read from dropping a legitimately-signed quorum.
|
||||
func TestValidatorStakeSource_HeightPinned(t *testing.T) {
|
||||
netID := ids.GenerateTestID()
|
||||
n0, n1, n2 := ids.GenerateTestNodeID(), ids.GenerateTestNodeID(), ids.GenerateTestNodeID()
|
||||
|
||||
state := stateWithHistory(netID, map[uint64][]vdr{
|
||||
10: {{n0, []byte("pk0"), 70}, {n1, []byte("pk1"), 30}}, // total 100
|
||||
11: {{n0, []byte("pk0"), 70}, {n1, []byte("pk1"), 30}, {n2, []byte("pk2"), 50}}, // total 150
|
||||
})
|
||||
src := newValidatorStakeSource(state, netID)
|
||||
|
||||
// At height 10 the tally is the height-10 epoch.
|
||||
if w := src.Weight(n0, 10); w != 70 {
|
||||
t.Fatalf("Weight(n0, h=10) = %d, want 70", w)
|
||||
}
|
||||
if total := src.TotalStake(10); total != 100 {
|
||||
t.Fatalf("TotalStake(h=10) = %d, want 100", total)
|
||||
}
|
||||
// n2 is NOT in the height-10 set → 0 at h=10, but 50 at h=11. The tally is
|
||||
// height-pinned, not current-map.
|
||||
if w := src.Weight(n2, 10); w != 0 {
|
||||
t.Fatalf("Weight(n2, h=10) = %d, want 0 (n2 joined at h=11)", w)
|
||||
}
|
||||
if w := src.Weight(n2, 11); w != 50 {
|
||||
t.Fatalf("Weight(n2, h=11) = %d, want 50", w)
|
||||
}
|
||||
if total := src.TotalStake(11); total != 150 {
|
||||
t.Fatalf("TotalStake(h=11) = %d, want 150", total)
|
||||
}
|
||||
|
||||
// Unknown node at a known height → 0 (cannot inflate the numerator).
|
||||
if w := src.Weight(ids.GenerateTestNodeID(), 10); w != 0 {
|
||||
t.Fatalf("Weight(unknown, h=10) = %d, want 0", w)
|
||||
}
|
||||
// nil state → fail-soft zeros.
|
||||
nilSrc := &validatorStakeSource{state: nil, networkID: netID}
|
||||
if nilSrc.Weight(n0, 10) != 0 || nilSrc.TotalStake(10) != 0 {
|
||||
t.Fatal("nil state must yield zero weight and total")
|
||||
}
|
||||
}
|
||||
|
||||
// TestHashValidatorSet_ByteStability is a GOLDEN test pinning the canonical
|
||||
// set-root encoding so the wire format cannot drift (the engine's epoch-binding
|
||||
// contract and any persisted/gossiped cert depend on this exact byte layout). If
|
||||
// this value changes, the set-root encoding changed and every node in the
|
||||
// network must upgrade in lockstep — it is a CONSENSUS-BREAKING change.
|
||||
func TestHashValidatorSet_ByteStability(t *testing.T) {
|
||||
// Fixed (non-random) NodeIDs so the golden is reproducible.
|
||||
var a, b ids.NodeID
|
||||
a[0], b[0] = 0x01, 0x02
|
||||
set := map[ids.NodeID]*validators.GetValidatorOutput{
|
||||
a: {NodeID: a, PublicKey: []byte{0xaa, 0xbb}, Light: 10},
|
||||
b: {NodeID: b, PublicKey: []byte{0xcc}, Light: 20},
|
||||
}
|
||||
got := hashValidatorSet(set)
|
||||
|
||||
// Independently recompute the expected commitment from the canonical spec:
|
||||
// sorted-by-NodeID, each nodeID || light(8,BE) || len(pk)(8,BE) || pk, SHA-256.
|
||||
want := expectedSetRoot(t, []vdr{
|
||||
{a, []byte{0xaa, 0xbb}, 10},
|
||||
{b, []byte{0xcc}, 20},
|
||||
})
|
||||
if got != want {
|
||||
t.Fatalf("set-root encoding drifted (CONSENSUS-BREAKING):\n got %s\n want %s", got, want)
|
||||
}
|
||||
|
||||
// Empty/nil set → ids.Empty.
|
||||
if hashValidatorSet(nil) != ids.Empty {
|
||||
t.Fatal("nil set must commit to ids.Empty")
|
||||
}
|
||||
if hashValidatorSet(map[ids.NodeID]*validators.GetValidatorOutput{}) != ids.Empty {
|
||||
t.Fatal("empty set must commit to ids.Empty")
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// quorum_verifier_height_test.go — node-layer regression for RESIDUAL-B and the
|
||||
// CRITICAL-1 wiring: the BLS vote verifier resolves the voter's public key from
|
||||
// the HEIGHT-INDEXED validators.State AT THE BLOCK'S P-CHAIN EPOCH HEIGHT — the
|
||||
// SAME height-pinned source as the set-root and the ⅔-by-stake tally — NOT from
|
||||
// the current validator map.
|
||||
//
|
||||
// The round-1 fix left the verifier reading the CURRENT map (m.Validators):
|
||||
// a validator present in set@H (it legitimately signed block H) but already gone
|
||||
// from the current map during async staking skew had its vote DROPPED, and if it
|
||||
// held >⅓ of the stake-at-H the block never finalized. These tests prove the
|
||||
// verifier now reads set@H, so such a vote verifies at H (and a vote keyed to the
|
||||
// wrong height does not).
|
||||
package chains
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/consensus/engine/chain"
|
||||
"github.com/luxfi/crypto/bls"
|
||||
"github.com/luxfi/ids"
|
||||
validators "github.com/luxfi/validators"
|
||||
"github.com/luxfi/validators/validatorstest"
|
||||
)
|
||||
|
||||
// blsKey is a test validator's BLS key material.
|
||||
type blsKey struct {
|
||||
nodeID ids.NodeID
|
||||
sk *bls.SecretKey
|
||||
pkComp []byte
|
||||
}
|
||||
|
||||
func newBLSKey(t *testing.T) blsKey {
|
||||
t.Helper()
|
||||
sk, err := bls.NewSecretKey()
|
||||
if err != nil {
|
||||
t.Fatalf("NewSecretKey: %v", err)
|
||||
}
|
||||
return blsKey{
|
||||
nodeID: ids.GenerateTestNodeID(),
|
||||
sk: sk,
|
||||
pkComp: bls.PublicKeyToCompressedBytes(sk.PublicKey()),
|
||||
}
|
||||
}
|
||||
|
||||
// stateWithBLSByHeight builds a height-indexed validators.State that reports the
|
||||
// given BLS validators at each height (empty for unknown heights / wrong net).
|
||||
func stateWithBLSByHeight(netID ids.ID, byHeight map[uint64][]blsKey) *validatorstest.TestState {
|
||||
s := validatorstest.NewTestState()
|
||||
s.GetValidatorSetF = func(_ context.Context, height uint64, gotNet ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
|
||||
if gotNet != netID {
|
||||
return map[ids.NodeID]*validators.GetValidatorOutput{}, nil
|
||||
}
|
||||
out := make(map[ids.NodeID]*validators.GetValidatorOutput)
|
||||
for _, k := range byHeight[height] {
|
||||
out[k.nodeID] = &validators.GetValidatorOutput{
|
||||
NodeID: k.nodeID,
|
||||
PublicKey: k.pkComp,
|
||||
Light: 1,
|
||||
Weight: 1,
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// TestBLSVoteVerifier_ResolvesPubkeyAtEpochHeight is the RESIDUAL-B core: a
|
||||
// validator that is in set@H but has LEFT the set at a later height still has its
|
||||
// vote verified at H (the verifier reads set@H, not the current map).
|
||||
func TestBLSVoteVerifier_ResolvesPubkeyAtEpochHeight(t *testing.T) {
|
||||
netID := ids.GenerateTestID()
|
||||
keep := newBLSKey(t) // stays in the set across epochs
|
||||
leaver := newBLSKey(t) // in set@10, GONE from set@11 (departed the current set)
|
||||
|
||||
const H = uint64(10)
|
||||
state := stateWithBLSByHeight(netID, map[uint64][]blsKey{
|
||||
10: {keep, leaver},
|
||||
11: {keep}, // leaver has departed by height 11 (the "current" epoch)
|
||||
})
|
||||
|
||||
v := newBLSVoteVerifier(state, netID)
|
||||
|
||||
// The leaver signs a message; its vote MUST verify at epoch H=10 (it was a
|
||||
// member then), proving pubkey resolution is at the epoch, not the current set.
|
||||
msg := []byte("LUX/chain/vote/v1\x00 — epoch-pinned verify")
|
||||
sig, err := leaver.sk.Sign(msg)
|
||||
if err != nil {
|
||||
t.Fatalf("sign: %v", err)
|
||||
}
|
||||
sigBytes := bls.SignatureToBytes(sig)
|
||||
|
||||
if !v.VerifyVote(leaver.nodeID, msg, sigBytes, H) {
|
||||
t.Fatal("RESIDUAL-B: a validator in set@H must verify at H even after leaving the current set " +
|
||||
"(verifier read the current map instead of set@H)")
|
||||
}
|
||||
|
||||
// At height 11 the leaver is NOT a member → its vote MUST NOT verify there.
|
||||
// This proves the resolution is genuinely height-pinned (not height-agnostic).
|
||||
if v.VerifyVote(leaver.nodeID, msg, sigBytes, 11) {
|
||||
t.Fatal("a validator absent from set@11 must NOT verify at 11 (height pinning is not in effect)")
|
||||
}
|
||||
|
||||
// The keeper verifies at both heights (it is in both sets) — sanity that the
|
||||
// epoch read is not rejecting valid current members.
|
||||
keepSig, _ := keep.sk.Sign(msg)
|
||||
keepBytes := bls.SignatureToBytes(keepSig)
|
||||
if !v.VerifyVote(keep.nodeID, msg, keepBytes, 10) || !v.VerifyVote(keep.nodeID, msg, keepBytes, 11) {
|
||||
t.Fatal("a validator present at both epochs must verify at both")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBLSVoteVerifier_FailClosed proves the verifier never panics and returns
|
||||
// false for every fail-soft case: nil state, unknown voter at the epoch, wrong
|
||||
// network, an unknown height (empty set), a wrong-length signature, and the
|
||||
// HIGH-1 malformed-infinity signature (0x40||zeros) that used to PANIC the purego
|
||||
// BLS path — here it must be a clean false, not a crash.
|
||||
func TestBLSVoteVerifier_FailClosed(t *testing.T) {
|
||||
netID := ids.GenerateTestID()
|
||||
k := newBLSKey(t)
|
||||
state := stateWithBLSByHeight(netID, map[uint64][]blsKey{10: {k}})
|
||||
msg := []byte("msg")
|
||||
sig, _ := k.sk.Sign(msg)
|
||||
good := bls.SignatureToBytes(sig)
|
||||
|
||||
// nil state → false for any voter.
|
||||
if newBLSVoteVerifier(nil, netID).VerifyVote(k.nodeID, msg, good, 10) {
|
||||
t.Fatal("nil state must yield false")
|
||||
}
|
||||
v := newBLSVoteVerifier(state, netID)
|
||||
// unknown voter at a known epoch.
|
||||
if v.VerifyVote(ids.GenerateTestNodeID(), msg, good, 10) {
|
||||
t.Fatal("unknown voter at the epoch must yield false")
|
||||
}
|
||||
// known voter at an UNKNOWN epoch (empty set) → false.
|
||||
if v.VerifyVote(k.nodeID, msg, good, 999) {
|
||||
t.Fatal("known voter at an unknown epoch (empty set) must yield false")
|
||||
}
|
||||
// wrong network → empty set → false.
|
||||
if newBLSVoteVerifier(state, ids.GenerateTestID()).VerifyVote(k.nodeID, msg, good, 10) {
|
||||
t.Fatal("wrong network must yield false")
|
||||
}
|
||||
// wrong-length signature → false (no panic).
|
||||
if v.VerifyVote(k.nodeID, msg, good[:len(good)-1], 10) {
|
||||
t.Fatal("wrong-length signature must yield false")
|
||||
}
|
||||
// HIGH-1 malformed infinity sig (0x40||zeros) → clean false, NOT a panic.
|
||||
mal := make([]byte, bls.SignatureLen)
|
||||
mal[0] = 0x40
|
||||
if v.VerifyVote(k.nodeID, msg, mal, 10) {
|
||||
t.Fatal("malformed-infinity signature must yield false")
|
||||
}
|
||||
// A WRONG signature (valid form, wrong key) → false.
|
||||
other := newBLSKey(t)
|
||||
otherSig, _ := other.sk.Sign(msg)
|
||||
if v.VerifyVote(k.nodeID, msg, bls.SignatureToBytes(otherSig), 10) {
|
||||
t.Fatal("a signature by a different key must yield false")
|
||||
}
|
||||
}
|
||||
|
||||
// ensure the node verifier still satisfies the (now height-aware) engine interface.
|
||||
var _ chain.VoteVerifier = (*blsVoteVerifier)(nil)
|
||||
@@ -5,7 +5,7 @@ package rpc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/go-json-experiment/json"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -132,7 +132,7 @@ func (d *DebugTool) testEndpoint(url string) TestResult {
|
||||
|
||||
// Check if we got a valid JSON-RPC response
|
||||
var rpcResp map[string]interface{}
|
||||
if err := json.UnmarshalRead(postResp.Body, &rpcResp); err == nil {
|
||||
if err := json.NewDecoder(postResp.Body).Decode(&rpcResp); err == nil {
|
||||
if _, hasResult := rpcResp["result"]; hasResult {
|
||||
result.Success = true
|
||||
result.Response = fmt.Sprintf("Valid JSON-RPC response: %v", rpcResp["result"])
|
||||
@@ -179,7 +179,7 @@ func (d *DebugTool) testRPCMethods(url string) []RPCTest {
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.UnmarshalRead(resp.Body, &result); err == nil {
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err == nil {
|
||||
if res, ok := result["result"]; ok {
|
||||
test.Success = true
|
||||
test.Result = fmt.Sprintf("%v", res)
|
||||
|
||||
@@ -70,7 +70,7 @@ func (s *mockServer) AddAliasesWithReadLock(endpoint string, aliases ...string)
|
||||
func (s *mockServer) Dispatch() error { return nil }
|
||||
func (s *mockServer) RegisterChain(chainName string, rt *runtime.Runtime, vm vm.VM) {
|
||||
}
|
||||
func (s *mockServer) Shutdown() error { return nil }
|
||||
func (s *mockServer) Shutdown() error { return nil }
|
||||
func (s *mockServer) SetRootInfoProvider(_ server.RootInfoProvider) {}
|
||||
|
||||
func TestHandlerManager_RegisterChainHandlers(t *testing.T) {
|
||||
|
||||
@@ -10,7 +10,7 @@ package chains
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"github.com/go-json-experiment/json"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
consensusconfig "github.com/luxfi/consensus/config"
|
||||
|
||||
+6
-6
@@ -35,12 +35,12 @@ import (
|
||||
|
||||
var (
|
||||
// Flags
|
||||
outputPath string
|
||||
inputPath string
|
||||
sinceVer uint64
|
||||
dataDir string
|
||||
dbType string
|
||||
noCompress bool
|
||||
outputPath string
|
||||
inputPath string
|
||||
sinceVer uint64
|
||||
dataDir string
|
||||
dbType string
|
||||
noCompress bool
|
||||
forceRestore bool
|
||||
)
|
||||
|
||||
|
||||
@@ -3,12 +3,10 @@ package main
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -192,7 +190,7 @@ func cmdExport(args []string) {
|
||||
|
||||
// stateEnvelope wraps CeremonyState with a SHA-256 integrity hash.
|
||||
type stateEnvelope struct {
|
||||
State jsontext.Value `json:"state"`
|
||||
State json.RawMessage `json:"state"`
|
||||
Integrity string `json:"integrity"` // hex(SHA-256(state bytes))
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -11,8 +12,6 @@ import (
|
||||
|
||||
"github.com/consensys/gnark-crypto/ecc/bn254"
|
||||
"github.com/consensys/gnark-crypto/ecc/bn254/fr"
|
||||
"github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
@@ -310,7 +309,7 @@ func TestRegressionL03_StateFileHasIntegrity(t *testing.T) {
|
||||
}
|
||||
|
||||
var envelope struct {
|
||||
State jsontext.Value `json:"state"`
|
||||
State json.RawMessage `json:"state"`
|
||||
Integrity string `json:"integrity"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &envelope); err != nil {
|
||||
|
||||
+11
-11
@@ -3,7 +3,7 @@ package main
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"github.com/go-json-experiment/json"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@@ -12,16 +12,16 @@ import (
|
||||
|
||||
// CeremonyState holds the full state of a powers-of-tau ceremony.
|
||||
type CeremonyState struct {
|
||||
Circuit string `json:"circuit"`
|
||||
NumConstraints int `json:"numConstraints"`
|
||||
PowersNeeded int `json:"powersNeeded"`
|
||||
Participants int `json:"participants"`
|
||||
TauG1 []G1Point `json:"tauG1"`
|
||||
TauG2 []G2Point `json:"tauG2"`
|
||||
AlphaG1 []G1Point `json:"alphaG1"`
|
||||
BetaG1 []G1Point `json:"betaG1"`
|
||||
BetaG2 G2Point `json:"betaG2"`
|
||||
Contributions []Contribution `json:"contributions"`
|
||||
Circuit string `json:"circuit"`
|
||||
NumConstraints int `json:"numConstraints"`
|
||||
PowersNeeded int `json:"powersNeeded"`
|
||||
Participants int `json:"participants"`
|
||||
TauG1 []G1Point `json:"tauG1"`
|
||||
TauG2 []G2Point `json:"tauG2"`
|
||||
AlphaG1 []G1Point `json:"alphaG1"`
|
||||
BetaG1 []G1Point `json:"betaG1"`
|
||||
BetaG2 G2Point `json:"betaG2"`
|
||||
Contributions []Contribution `json:"contributions"`
|
||||
}
|
||||
|
||||
// Contribution records a single participant's contribution.
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/go-json-experiment/json"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
@@ -2,8 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
@@ -30,7 +29,7 @@ func main() {
|
||||
"allocations": []map[string]interface{}{
|
||||
{
|
||||
"evmAddr": "0x9011E888251AB053B7bD1cdB598Db4f9DEd94714",
|
||||
"utxoAddr": zooAddr,
|
||||
"utxoAddr": zooAddr,
|
||||
"initialAmount": 0,
|
||||
"unlockSchedule": []map[string]interface{}{
|
||||
{
|
||||
@@ -76,7 +75,7 @@ func main() {
|
||||
ccBytes, _ := json.Marshal(cc)
|
||||
genesis["cChainGenesis"] = string(ccBytes)
|
||||
|
||||
out, _ := json.Marshal(genesis, jsontext.WithIndent(" "))
|
||||
out, _ := json.MarshalIndent(genesis, "", " ")
|
||||
os.WriteFile(os.ExpandEnv("$HOME/work/lux/mainnet/zoo_genesis_valid.json"), out, 0644)
|
||||
fmt.Println("Wrote zoo_genesis_valid.json")
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
|
||||
"github.com/luxfi/crypto/hash"
|
||||
"github.com/luxfi/ids"
|
||||
)
|
||||
@@ -13,15 +13,15 @@ func main() {
|
||||
fmt.Println("Usage: go run main.go <genesis_file>")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
|
||||
data, err := os.ReadFile(os.Args[1])
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
|
||||
fmt.Printf("File size: %d bytes\n", len(data))
|
||||
|
||||
|
||||
// Compute hash the same way luxd does
|
||||
rawHash := hash.ComputeHash256(data)
|
||||
id, err := ids.ToID(rawHash)
|
||||
@@ -29,6 +29,6 @@ func main() {
|
||||
fmt.Printf("Error converting to ID: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
|
||||
fmt.Printf("Genesis ID: %s\n", id.String())
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
// pqkeygen provisions the strict-PQ staking keypairs a local luxd needs:
|
||||
// ML-DSA-65 (FIPS 204) staking key + ML-KEM-768 (FIPS 203) handshake key.
|
||||
// Writes PEM blocks with the exact types the node's config loader expects.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/luxfi/crypto/mldsa"
|
||||
"github.com/luxfi/crypto/mlkem"
|
||||
)
|
||||
|
||||
func writePEM(path, typ string, der []byte) error {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
var b bytes.Buffer
|
||||
if err := pem.Encode(&b, &pem.Block{Type: typ, Bytes: der}); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, b.Bytes(), 0o600)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Fprintln(os.Stderr, "usage: pqkeygen <staking-dir>")
|
||||
os.Exit(1)
|
||||
}
|
||||
dir := os.Args[1]
|
||||
|
||||
// ML-DSA-65 staking key
|
||||
dsaPriv, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
dsaPub := dsaPriv.PublicKey.Bytes()
|
||||
if err := writePEM(filepath.Join(dir, "mldsa.key"), "ML-DSA-65 PRIVATE KEY", dsaPriv.Bytes()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := writePEM(filepath.Join(dir, "mldsa.pub"), "ML-DSA-65 PUBLIC KEY", dsaPub); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// ML-KEM-768 handshake key
|
||||
kemPub, kemPriv, err := mlkem.GenerateKeyPair(rand.Reader, mlkem.MLKEM768)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := writePEM(filepath.Join(dir, "mlkem.key"), "ML-KEM-768 PRIVATE KEY", kemPriv.Bytes()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := writePEM(filepath.Join(dir, "mlkem.pub"), "ML-KEM-768 PUBLIC KEY", kemPub.Bytes()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("ML-DSA-65 priv=%dB pub=%dB; ML-KEM-768 priv=%dB pub=%dB written to %s\n",
|
||||
len(dsaPriv.Bytes()), len(dsaPub), len(kemPriv.Bytes()), len(kemPub.Bytes()), dir)
|
||||
}
|
||||
@@ -46,7 +46,7 @@ func (c *ChainDatabaseConfig) GetDatabaseType(chainAlias string) string {
|
||||
func (c *ChainDatabaseConfig) Validate() error {
|
||||
validTypes := map[string]bool{
|
||||
"pebbledb": true,
|
||||
"zapdb": true,
|
||||
"zapdb": true,
|
||||
"memdb": true,
|
||||
}
|
||||
|
||||
|
||||
+27
-44
@@ -8,6 +8,7 @@ import (
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -18,8 +19,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
compression "github.com/luxfi/compress"
|
||||
@@ -775,18 +774,15 @@ func loadPEMBlock(path, expectType string) ([]byte, error) {
|
||||
// pemBytesOrFile resolves either a base64-encoded PEM in
|
||||
// contentKey (highest precedence, matches the
|
||||
// --foo-file-content / --foo-file pattern the existing TLS
|
||||
// loaders use) or a filesystem path in pathKey. A set-but-empty
|
||||
// contentKey is treated as absent and falls through to pathKey, so a
|
||||
// blank content flag and a missing one behave identically. Empty
|
||||
// return = neither was provided; caller decides whether that's a fatal
|
||||
// config error or a "fall through to classical-compat" path.
|
||||
// loaders use) or a filesystem path in pathKey. Empty return =
|
||||
// neither was set; caller decides whether that's a fatal config
|
||||
// error or a "fall through to classical-compat" path.
|
||||
func pemBytesOrFile(v *viper.Viper, contentKey, pathKey, expectType string) ([]byte, string, error) {
|
||||
// A non-empty *-content flag wins. A set-but-empty one (e.g. an env var or
|
||||
// a deploy template that rendered to "") is treated identically to an
|
||||
// absent flag: fall through to the *-file path below. Short-circuiting on
|
||||
// the empty value here would silently degrade a strict-PQ validator to a
|
||||
// classical ECDSA NodeID — the strict-PQ activation outage this guards.
|
||||
if raw := v.GetString(contentKey); raw != "" {
|
||||
if v.IsSet(contentKey) {
|
||||
raw := v.GetString(contentKey)
|
||||
if raw == "" {
|
||||
return nil, "", nil
|
||||
}
|
||||
decoded, err := base64.StdEncoding.DecodeString(raw)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("decode %s base64: %w", contentKey, err)
|
||||
@@ -1036,7 +1032,7 @@ func getUpgradeConfig(v *viper.Viper, networkID uint32) (upgrade.Config, error)
|
||||
}
|
||||
|
||||
var upgradeConfig upgrade.Config
|
||||
if err := json.Unmarshal(upgradeBytes, &upgradeConfig, json.MatchCaseInsensitiveNames(true)); err != nil {
|
||||
if err := json.Unmarshal(upgradeBytes, &upgradeConfig); err != nil {
|
||||
return upgrade.Config{}, fmt.Errorf("unable to unmarshal upgrade bytes: %w", err)
|
||||
}
|
||||
return upgradeConfig, nil
|
||||
@@ -1056,7 +1052,7 @@ func getGenesisData(v *viper.Viper, networkID uint32, stakingCfg *builder.Stakin
|
||||
if err != nil {
|
||||
return nil, ids.Empty, fmt.Errorf("failed to decode %s: %w", GenesisRawBytesKey, err)
|
||||
}
|
||||
utxoAssetID, err := resolveUTXOAssetID(networkID, genesisBytes)
|
||||
utxoAssetID, err := resolveXAssetID(networkID, genesisBytes)
|
||||
if err != nil {
|
||||
return nil, ids.Empty, fmt.Errorf("resolve X-Chain asset ID from %s: %w", GenesisRawBytesKey, err)
|
||||
}
|
||||
@@ -1095,29 +1091,16 @@ func getGenesisData(v *viper.Viper, networkID uint32, stakingCfg *builder.Stakin
|
||||
// Check if we have cached genesis bytes to avoid rebuilding
|
||||
cacheFile := filepath.Join(dataDir, "genesis.bytes")
|
||||
if cachedBytes, err := loadCachedGenesisBytes(cacheFile); err == nil && len(cachedBytes) > 0 {
|
||||
utxoAssetID, err := resolveUTXOAssetID(networkID, cachedBytes)
|
||||
if err == nil {
|
||||
log.Info("loaded cached genesis bytes for hash stability",
|
||||
"cacheFile", cacheFile,
|
||||
"size", len(cachedBytes),
|
||||
"utxoAssetID", utxoAssetID,
|
||||
)
|
||||
return cachedBytes, utxoAssetID, nil
|
||||
utxoAssetID, err := resolveXAssetID(networkID, cachedBytes)
|
||||
if err != nil {
|
||||
return nil, ids.Empty, fmt.Errorf("resolve X-Chain asset ID from cached genesis: %w", err)
|
||||
}
|
||||
// Cache parse failed — almost certainly a codec mismatch
|
||||
// after a binary upgrade (e.g. v1-codec bytes persisted by
|
||||
// an older luxd, multi-version v0+v1 dispatcher in this
|
||||
// luxd doesn't recognise a type the cached blob still
|
||||
// uses). Drop the cache and rebuild from the file rather
|
||||
// than wedging in a CrashLoop. Hash stability is forfeit
|
||||
// for this single restart — intentional, the alternative
|
||||
// is a permanent outage on every binary bump.
|
||||
log.Warn("cached genesis bytes failed to parse — invalidating cache and rebuilding from genesis-file",
|
||||
log.Info("loaded cached genesis bytes for hash stability",
|
||||
"cacheFile", cacheFile,
|
||||
"size", len(cachedBytes),
|
||||
"error", err,
|
||||
"utxoAssetID", utxoAssetID,
|
||||
)
|
||||
_ = os.Remove(cacheFile)
|
||||
return cachedBytes, utxoAssetID, nil
|
||||
}
|
||||
// No cache or invalid cache - build from file and cache the result
|
||||
genesisBytes, utxoAssetID, err := builder.FromFile(networkID, genesisFileName, stakingCfg)
|
||||
@@ -1143,7 +1126,7 @@ func getGenesisData(v *viper.Viper, networkID uint32, stakingCfg *builder.Stakin
|
||||
return builder.FromConfig(config)
|
||||
}
|
||||
|
||||
// resolveUTXOAssetID extracts the X-Chain native asset ID from the loaded
|
||||
// resolveXAssetID extracts the X-Chain native asset ID from the loaded
|
||||
// platform-genesis blob. It is the canonical source of truth used by
|
||||
// every load path that bypasses FromConfig (raw bytes, cached bytes —
|
||||
// the paths that historically defaulted to constants.UTXOAssetIDFor and
|
||||
@@ -1162,8 +1145,8 @@ func getGenesisData(v *viper.Viper, networkID uint32, stakingCfg *builder.Stakin
|
||||
//
|
||||
// Tested against both sovereign-network genesis (X-Chain present, asset
|
||||
// ID differs from the constant) and upstream Lux genesis fixtures.
|
||||
func resolveUTXOAssetID(networkID uint32, genesisBytes []byte) (ids.ID, error) {
|
||||
id, ok, err := builder.UTXOAssetIDFromGenesisBytes(genesisBytes)
|
||||
func resolveXAssetID(networkID uint32, genesisBytes []byte) (ids.ID, error) {
|
||||
id, ok, err := builder.XAssetIDFromGenesisBytes(genesisBytes)
|
||||
if err != nil {
|
||||
return ids.Empty, err
|
||||
}
|
||||
@@ -1311,7 +1294,7 @@ func getAliases(v *viper.Viper, name string, contentKey string, fileKey string)
|
||||
}
|
||||
|
||||
aliasMap := make(map[ids.ID][]string)
|
||||
if err := json.Unmarshal(fileBytes, &aliasMap, json.MatchCaseInsensitiveNames(true)); err != nil {
|
||||
if err := json.Unmarshal(fileBytes, &aliasMap); err != nil {
|
||||
return nil, fmt.Errorf("%w on %s: %w", errUnmarshalling, name, err)
|
||||
}
|
||||
return aliasMap, nil
|
||||
@@ -1351,7 +1334,7 @@ func getChainConfigsFromFlag(v *viper.Viper) (map[string]chains.ChainConfig, err
|
||||
}
|
||||
|
||||
chainConfigs := make(map[string]chains.ChainConfig)
|
||||
if err := json.Unmarshal(chainConfigContent, &chainConfigs, json.MatchCaseInsensitiveNames(true)); err != nil {
|
||||
if err := json.Unmarshal(chainConfigContent, &chainConfigs); err != nil {
|
||||
return nil, fmt.Errorf("could not unmarshal JSON: %w", err)
|
||||
}
|
||||
return chainConfigs, nil
|
||||
@@ -1433,8 +1416,8 @@ func getNetConfigsFromFlags(v *viper.Viper, netIDs []ids.ID) (map[ids.ID]nets.Co
|
||||
}
|
||||
|
||||
// partially parse configs to be filled by defaults later
|
||||
chainConfigs := make(map[ids.ID]jsontext.Value, len(netIDs))
|
||||
if err := json.Unmarshal(netConfigContent, &chainConfigs, json.MatchCaseInsensitiveNames(true)); err != nil {
|
||||
chainConfigs := make(map[ids.ID]json.RawMessage, len(netIDs))
|
||||
if err := json.Unmarshal(netConfigContent, &chainConfigs); err != nil {
|
||||
return nil, fmt.Errorf("could not unmarshal JSON: %w", err)
|
||||
}
|
||||
|
||||
@@ -1443,7 +1426,7 @@ func getNetConfigsFromFlags(v *viper.Viper, netIDs []ids.ID) (map[ids.ID]nets.Co
|
||||
config := getDefaultNetConfig(v)
|
||||
|
||||
if rawNetConfigBytes, ok := chainConfigs[chainID]; ok {
|
||||
if err := json.Unmarshal(rawNetConfigBytes, &config, json.MatchCaseInsensitiveNames(true)); err != nil {
|
||||
if err := json.Unmarshal(rawNetConfigBytes, &config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1502,7 +1485,7 @@ func getNetConfigsFromDir(v *viper.Viper, chainIDs []ids.ID) (map[ids.ID]nets.Co
|
||||
}
|
||||
|
||||
// Update the default config with the values from the file
|
||||
if err := json.Unmarshal(file, &config, json.MatchCaseInsensitiveNames(true)); err != nil {
|
||||
if err := json.Unmarshal(file, &config); err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", errUnmarshalling, err)
|
||||
}
|
||||
|
||||
@@ -1749,7 +1732,7 @@ func GetNodeConfig(v *viper.Viper) (node.Config, error) {
|
||||
return node.Config{}, err
|
||||
}
|
||||
nodeConfig.TrackAllChains = v.GetBool(TrackAllChainsKey)
|
||||
nodeConfig.DexValidator = v.GetBool(DexValidatorKey)
|
||||
|
||||
|
||||
// HTTP APIs
|
||||
nodeConfig.HTTPConfig, err = getHTTPConfig(v)
|
||||
|
||||
+18
-40
@@ -4,10 +4,9 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/go-json-experiment/json"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -17,8 +16,8 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
consensusconfig "github.com/luxfi/consensus/config"
|
||||
"github.com/luxfi/constants"
|
||||
consensusconfig "github.com/luxfi/consensus/config"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/node/chains"
|
||||
"github.com/luxfi/node/genesis/builder"
|
||||
@@ -29,23 +28,6 @@ import (
|
||||
|
||||
const chainConfigFilenameExtension = ".ex"
|
||||
|
||||
// equalChainConfigs compares two ChainConfig maps using bytes.Equal for the
|
||||
// []byte fields, so nil and empty []byte compare equal. json/v2 unmarshals
|
||||
// absent/null []byte fields to empty (non-nil), unlike v1 which left them nil.
|
||||
func equalChainConfigs(t *testing.T, expected, actual map[string]chains.ChainConfig) {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
require.Equal(len(expected), len(actual), "map length mismatch")
|
||||
for k, want := range expected {
|
||||
got, ok := actual[k]
|
||||
require.True(ok, "missing key %q", k)
|
||||
require.True(bytes.Equal(want.Config, got.Config),
|
||||
"%q Config: expected %x got %x", k, want.Config, got.Config)
|
||||
require.True(bytes.Equal(want.Upgrade, got.Upgrade),
|
||||
"%q Upgrade: expected %x got %x", k, want.Upgrade, got.Upgrade)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetChainConfigsFromFiles(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
configs map[string]string
|
||||
@@ -109,7 +91,7 @@ func TestGetChainConfigsFromFiles(t *testing.T) {
|
||||
require.Equal(root, v.GetString(ChainConfigDirKey))
|
||||
chainConfigs, err := getChainConfigs(v)
|
||||
require.NoError(err)
|
||||
equalChainConfigs(t, test.expected, chainConfigs)
|
||||
require.Equal(test.expected, chainConfigs)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -169,11 +151,7 @@ func TestGetChainConfigsDirNotExist(t *testing.T) {
|
||||
// don't read with getConfigFromViper since it's very slow.
|
||||
chainConfigs, err := getChainConfigs(v)
|
||||
require.ErrorIs(err, test.expectedErr)
|
||||
if test.expected == nil {
|
||||
require.Nil(chainConfigs)
|
||||
} else {
|
||||
equalChainConfigs(t, test.expected, chainConfigs)
|
||||
}
|
||||
require.Equal(test.expected, chainConfigs)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -193,7 +171,7 @@ func TestSetChainConfigDefaultDir(t *testing.T) {
|
||||
chainConfigs, err := getChainConfigs(v)
|
||||
require.NoError(err)
|
||||
expected := map[string]chains.ChainConfig{"C": {Config: []byte("helloworld"), Upgrade: []byte(nil)}}
|
||||
equalChainConfigs(t, expected, chainConfigs)
|
||||
require.Equal(expected, chainConfigs)
|
||||
}
|
||||
|
||||
func TestGetChainConfigsFromFlags(t *testing.T) {
|
||||
@@ -260,7 +238,7 @@ func TestGetChainConfigsFromFlags(t *testing.T) {
|
||||
// Parse config
|
||||
chainConfigs, err := getChainConfigs(v)
|
||||
require.NoError(err)
|
||||
equalChainConfigs(t, test.expected, chainConfigs)
|
||||
require.Equal(test.expected, chainConfigs)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -698,16 +676,16 @@ func TestDevModeFlags(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolveUTXOAssetID_FromSovereignGenesis covers the canonical
|
||||
// TestResolveXAssetID_FromSovereignGenesis covers the canonical
|
||||
// behaviour the sovereign-L1 fix relies on: when the loaded platform
|
||||
// genesis bakes an X-Chain, resolveUTXOAssetID returns the runtime asset
|
||||
// genesis bakes an X-Chain, resolveXAssetID returns the runtime asset
|
||||
// ID encoded IN the genesis (matches what FromConfig produces, matches
|
||||
// what the running X-Chain reports via platform.getStakingAssetID).
|
||||
//
|
||||
// Critically: NOT constants.UTXOAssetIDFor(networkID). That value is
|
||||
// network-id-keyed and would silently collide between two sovereign L1s
|
||||
// sharing a primary-network ID.
|
||||
func TestResolveUTXOAssetID_FromSovereignGenesis(t *testing.T) {
|
||||
func TestResolveXAssetID_FromSovereignGenesis(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
cfg := builder.GetConfig(constants.LocalID)
|
||||
@@ -718,37 +696,37 @@ func TestResolveUTXOAssetID_FromSovereignGenesis(t *testing.T) {
|
||||
require.NoError(err)
|
||||
require.NotEqual(ids.Empty, expectedID)
|
||||
|
||||
gotID, err := resolveUTXOAssetID(constants.LocalID, genesisBytes)
|
||||
gotID, err := resolveXAssetID(constants.LocalID, genesisBytes)
|
||||
require.NoError(err)
|
||||
require.Equal(expectedID, gotID,
|
||||
"resolveUTXOAssetID must agree with FromConfig on the genesis-derived asset ID")
|
||||
"resolveXAssetID must agree with FromConfig on the genesis-derived asset ID")
|
||||
}
|
||||
|
||||
// TestResolveUTXOAssetID_POnlyFallback verifies that when the platform
|
||||
// genesis bakes no X-Chain (P-only mode), resolveUTXOAssetID falls back
|
||||
// TestResolveXAssetID_POnlyFallback verifies that when the platform
|
||||
// genesis bakes no X-Chain (P-only mode), resolveXAssetID falls back
|
||||
// to constants.UTXOAssetIDFor(networkID). That value is unused at
|
||||
// runtime (no X-Chain to mint on) but keeps the existing nodeConfig
|
||||
// shape consistent for downstream consumers.
|
||||
func TestResolveUTXOAssetID_POnlyFallback(t *testing.T) {
|
||||
func TestResolveXAssetID_POnlyFallback(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
pOnly := &pchaingenesis.Genesis{Chains: nil}
|
||||
pOnlyBytes, err := pchaingenesis.Codec.Marshal(pchaintxs.CodecVersion, pOnly)
|
||||
require.NoError(err)
|
||||
|
||||
gotID, err := resolveUTXOAssetID(42, pOnlyBytes)
|
||||
gotID, err := resolveXAssetID(42, pOnlyBytes)
|
||||
require.NoError(err)
|
||||
require.Equal(constants.UTXOAssetIDFor(42), gotID,
|
||||
"P-only must fall through to UTXOAssetIDFor(networkID)")
|
||||
}
|
||||
|
||||
// TestResolveUTXOAssetID_Malformed asserts that bad genesis bytes surface
|
||||
// TestResolveXAssetID_Malformed asserts that bad genesis bytes surface
|
||||
// an error rather than silently returning ids.Empty (which would
|
||||
// reintroduce the UTXOAssetIDFor fallback and defeat the fix on
|
||||
// sovereign L1s where the fallback value is wrong).
|
||||
func TestResolveUTXOAssetID_Malformed(t *testing.T) {
|
||||
func TestResolveXAssetID_Malformed(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
_, err := resolveUTXOAssetID(1, []byte{0xff, 0xfe, 0xfd, 0xfc})
|
||||
_, err := resolveXAssetID(1, []byte{0xff, 0xfe, 0xfd, 0xfc})
|
||||
require.Error(err)
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestDexValidatorFlagDefaultsOff pins the runtime opt-in contract for the
|
||||
// D-Chain DEX: the dex-validator flag exists, defaults to false (most nodes do
|
||||
// NOT run the DEX even though the DEXVM factory is always linked), and flips to
|
||||
// true when set. Participation is a runtime decision, not a compile-time one.
|
||||
func TestDexValidatorFlagDefaultsOff(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
// The flag must be registered in the canonical flag set.
|
||||
fs := BuildFlagSet()
|
||||
flag := fs.Lookup(DexValidatorKey)
|
||||
require.NotNil(flag, "dex-validator flag must be registered")
|
||||
require.Equal("false", flag.DefValue, "dex-validator must default to false")
|
||||
|
||||
// Default (unset) reads as false.
|
||||
v := viper.New()
|
||||
require.NoError(v.BindPFlags(fs))
|
||||
require.False(v.GetBool(DexValidatorKey), "dex-validator must read false by default")
|
||||
|
||||
// Explicit opt-in flips it.
|
||||
v.Set(DexValidatorKey, true)
|
||||
require.True(v.GetBool(DexValidatorKey), "dex-validator must read true when set")
|
||||
}
|
||||
+14
-15
@@ -13,14 +13,14 @@ import (
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
compression "github.com/luxfi/compress"
|
||||
"github.com/luxfi/constants"
|
||||
"github.com/luxfi/genesis/pkg/genesis"
|
||||
"github.com/luxfi/net/dynamicip"
|
||||
"github.com/luxfi/node/genesis/builder"
|
||||
"github.com/luxfi/node/trace"
|
||||
"github.com/luxfi/node/vms/components/gas"
|
||||
"github.com/luxfi/node/vms/proposervm"
|
||||
compression "github.com/luxfi/compress"
|
||||
"github.com/luxfi/net/dynamicip"
|
||||
"github.com/luxfi/sys/ulimit"
|
||||
|
||||
consensusconfig "github.com/luxfi/consensus/config"
|
||||
@@ -48,19 +48,19 @@ var (
|
||||
defaultStakingSignerKeyPath = filepath.Join(defaultStakingPath, "signer.key")
|
||||
// Strict-PQ default paths — mirror downstream-tenant CLI `<tenantctl> key gen`
|
||||
// layout so the operator init container + lqd see the same files.
|
||||
defaultStakingMLDSAKeyPath = filepath.Join(defaultStakingPath, "mldsa.key")
|
||||
defaultStakingMLDSAPubKeyPath = filepath.Join(defaultStakingPath, "mldsa.pub")
|
||||
defaultStakingMLDSAKeyPath = filepath.Join(defaultStakingPath, "mldsa.key")
|
||||
defaultStakingMLDSAPubKeyPath = filepath.Join(defaultStakingPath, "mldsa.pub")
|
||||
defaultHandshakeMLKEMKeyPath = filepath.Join(defaultStakingPath, "mlkem.key")
|
||||
defaultHandshakeMLKEMPubKeyPath = filepath.Join(defaultStakingPath, "mlkem.pub")
|
||||
defaultConfigDir = filepath.Join(defaultUnexpandedDataDir, "configs")
|
||||
defaultChainConfigDir = filepath.Join(defaultConfigDir, "chains")
|
||||
defaultVMConfigDir = filepath.Join(defaultConfigDir, "vms")
|
||||
defaultVMAliasFilePath = filepath.Join(defaultVMConfigDir, "aliases.json")
|
||||
defaultChainAliasFilePath = filepath.Join(defaultChainConfigDir, "aliases.json")
|
||||
defaultNetConfigDir = filepath.Join(defaultConfigDir, "chains")
|
||||
defaultPluginDir = filepath.Join(defaultUnexpandedDataDir, "plugins", "current")
|
||||
defaultChainDataDir = filepath.Join(defaultUnexpandedDataDir, "chainData")
|
||||
defaultProcessContextPath = filepath.Join(defaultUnexpandedDataDir, DefaultProcessContextFilename)
|
||||
defaultConfigDir = filepath.Join(defaultUnexpandedDataDir, "configs")
|
||||
defaultChainConfigDir = filepath.Join(defaultConfigDir, "chains")
|
||||
defaultVMConfigDir = filepath.Join(defaultConfigDir, "vms")
|
||||
defaultVMAliasFilePath = filepath.Join(defaultVMConfigDir, "aliases.json")
|
||||
defaultChainAliasFilePath = filepath.Join(defaultChainConfigDir, "aliases.json")
|
||||
defaultNetConfigDir = filepath.Join(defaultConfigDir, "chains")
|
||||
defaultPluginDir = filepath.Join(defaultUnexpandedDataDir, "plugins", "current")
|
||||
defaultChainDataDir = filepath.Join(defaultUnexpandedDataDir, "chainData")
|
||||
defaultProcessContextPath = filepath.Join(defaultUnexpandedDataDir, DefaultProcessContextFilename)
|
||||
)
|
||||
|
||||
func deprecateFlags(fs *pflag.FlagSet) error {
|
||||
@@ -309,7 +309,7 @@ func addNodeFlags(fs *pflag.FlagSet) {
|
||||
fs.String(HandshakeMLKEMPubKeyPathKey, defaultHandshakeMLKEMPubKeyPath, fmt.Sprintf("Path to the ML-KEM-768 handshake public key (FIPS 203 PEM). Ignored if %s is specified", HandshakeMLKEMPubKeyContentKey))
|
||||
fs.String(HandshakeMLKEMPubKeyContentKey, "", "Base64-encoded ML-KEM-768 handshake public key (FIPS 203 PEM)")
|
||||
fs.String(StakingKMSEndpointKey, "", "KMS endpoint for staking key retrieval (e.g., https://kms.dev.lux.network)")
|
||||
fs.String(StakingKMSSecretPathKey, "", "KMS secret path for staking keys (e.g., /staking/devnet/node-0)")
|
||||
fs.String(StakingKMSSecretPathKey, "", "KMS secret path for staking keys (e.g., /staking/liquid-devnet/node-0)")
|
||||
fs.String(StakingKMSTokenKey, "", "KMS auth token for staking key retrieval")
|
||||
fs.Bool(SybilProtectionEnabledKey, true, "Enables sybil protection. If enabled, Network TLS is required")
|
||||
fs.Uint64(SybilProtectionDisabledWeightKey, 100, "Weight to provide to each peer when sybil protection is disabled")
|
||||
@@ -335,7 +335,6 @@ func addNodeFlags(fs *pflag.FlagSet) {
|
||||
// Chain tracking
|
||||
fs.String(TrackChainsKey, "", "Comma-separated list of chain IDs to track. A node tracking chains will sync those chains and track validator uptimes. Use --track-all-chains for development networks")
|
||||
fs.Bool(TrackAllChainsKey, false, "If true, track all chains automatically. Useful for development and testing networks where you want to sync all deployed chains without specifying each one")
|
||||
fs.Bool(DexValidatorKey, false, "If true, this node activates and participates in the D-Chain DEX: it tracks/validates the D-Chain and dials the venue matcher engine. Default off — the DEXVM factory is always linked (D-Chain is a genesis chain), but a node does NOT activate the D-Chain unless this flag is set, even under --track-all-chains. When a network configures the dex-operator NFT collection, activation also requires the node's X-Chain staking address to hold that NFT (flag AND NFT)")
|
||||
|
||||
// State syncing
|
||||
fs.String(StateSyncIPsKey, "", "Comma separated list of state sync peer ips to connect to. Example: 127.0.0.1:9630,127.0.0.1:9631")
|
||||
|
||||
+36
-37
@@ -78,36 +78,36 @@ const (
|
||||
HTTPReadTimeoutKey = "http-read-timeout"
|
||||
HTTPReadHeaderTimeoutKey = "http-read-header-timeout"
|
||||
|
||||
HTTPIdleTimeoutKey = "http-idle-timeout"
|
||||
StateSyncIPsKey = "state-sync-ips"
|
||||
StateSyncIDsKey = "state-sync-ids"
|
||||
BootstrapNodesKey = "bootstrap-nodes"
|
||||
BootstrapIPsKey = "bootstrap-ips"
|
||||
BootstrapIDsKey = "bootstrap-ids"
|
||||
SkipBootstrapKey = "skip-bootstrap"
|
||||
EnableAutominingKey = "enable-automining"
|
||||
StakingHostKey = "staking-host"
|
||||
StakingPortKey = "staking-port"
|
||||
StakingEphemeralCertEnabledKey = "staking-ephemeral-cert-enabled"
|
||||
StakingTLSKeyPathKey = "staking-tls-key-file"
|
||||
StakingTLSKeyContentKey = "staking-tls-key-file-content"
|
||||
StakingCertPathKey = "staking-tls-cert-file"
|
||||
StakingCertContentKey = "staking-tls-cert-file-content"
|
||||
StakingEphemeralSignerEnabledKey = "staking-ephemeral-signer-enabled"
|
||||
StakingSignerKeyPathKey = "staking-signer-key-file"
|
||||
StakingSignerKeyContentKey = "staking-signer-key-file-content"
|
||||
StakingKMSEndpointKey = "staking-kms-endpoint"
|
||||
StakingKMSSecretPathKey = "staking-kms-secret-path"
|
||||
StakingKMSTokenKey = "staking-kms-token"
|
||||
HTTPIdleTimeoutKey = "http-idle-timeout"
|
||||
StateSyncIPsKey = "state-sync-ips"
|
||||
StateSyncIDsKey = "state-sync-ids"
|
||||
BootstrapNodesKey = "bootstrap-nodes"
|
||||
BootstrapIPsKey = "bootstrap-ips"
|
||||
BootstrapIDsKey = "bootstrap-ids"
|
||||
SkipBootstrapKey = "skip-bootstrap"
|
||||
EnableAutominingKey = "enable-automining"
|
||||
StakingHostKey = "staking-host"
|
||||
StakingPortKey = "staking-port"
|
||||
StakingEphemeralCertEnabledKey = "staking-ephemeral-cert-enabled"
|
||||
StakingTLSKeyPathKey = "staking-tls-key-file"
|
||||
StakingTLSKeyContentKey = "staking-tls-key-file-content"
|
||||
StakingCertPathKey = "staking-tls-cert-file"
|
||||
StakingCertContentKey = "staking-tls-cert-file-content"
|
||||
StakingEphemeralSignerEnabledKey = "staking-ephemeral-signer-enabled"
|
||||
StakingSignerKeyPathKey = "staking-signer-key-file"
|
||||
StakingSignerKeyContentKey = "staking-signer-key-file-content"
|
||||
StakingKMSEndpointKey = "staking-kms-endpoint"
|
||||
StakingKMSSecretPathKey = "staking-kms-secret-path"
|
||||
StakingKMSTokenKey = "staking-kms-token"
|
||||
// Strict-PQ staking identity (FIPS 204 ML-DSA-65). When set, the node
|
||||
// uses the ML-DSA-65 public key as the NodeID source via
|
||||
// ids.NodeIDSchemeMLDSA65.DeriveMLDSA(chainID, pubKey), replacing the
|
||||
// classical TLS-cert NodeID derivation. Strict-PQ profiles require
|
||||
// these; classical-compat chains ignore them.
|
||||
StakingMLDSAKeyPathKey = "staking-mldsa-key-file"
|
||||
StakingMLDSAKeyContentKey = "staking-mldsa-key-file-content"
|
||||
StakingMLDSAPubKeyPathKey = "staking-mldsa-pub-key-file"
|
||||
StakingMLDSAPubKeyContentKey = "staking-mldsa-pub-key-file-content"
|
||||
StakingMLDSAKeyPathKey = "staking-mldsa-key-file"
|
||||
StakingMLDSAKeyContentKey = "staking-mldsa-key-file-content"
|
||||
StakingMLDSAPubKeyPathKey = "staking-mldsa-pub-key-file"
|
||||
StakingMLDSAPubKeyContentKey = "staking-mldsa-pub-key-file-content"
|
||||
// Strict-PQ handshake KEM (FIPS 203 ML-KEM-768). Peer-facing public
|
||||
// key is published in the validator-set entry so peers can encapsulate
|
||||
// to it for session-key establishment with no classical fallback.
|
||||
@@ -175,7 +175,6 @@ const (
|
||||
PartialSyncPrimaryNetworkKey = "partial-sync-primary-network"
|
||||
TrackChainsKey = "track-chains"
|
||||
TrackAllChainsKey = "track-all-chains"
|
||||
DexValidatorKey = "dex-validator"
|
||||
AdminAPIEnabledKey = "api-admin-enabled"
|
||||
InfoAPIEnabledKey = "api-info-enabled"
|
||||
KeystoreAPIEnabledKey = "api-keystore-enabled"
|
||||
@@ -265,17 +264,17 @@ const (
|
||||
ForceIgnoreChecksumKey = "force-ignore-checksum"
|
||||
|
||||
// Low Memory / Dev Light Mode Keys
|
||||
LowMemoryKey = "low-memory"
|
||||
MemoryProfileKey = "memory-profile"
|
||||
DevLightKey = "dev-light"
|
||||
ConfigProfileKey = "config-profile"
|
||||
DBCacheSizeKey = "db-cache-size"
|
||||
DBMemtableSizeKey = "db-memtable-size"
|
||||
StateCacheSizeKey = "state-cache-size"
|
||||
BlockCacheSizeKey = "block-cache-size"
|
||||
DisableBloomFiltersKey = "disable-bloom-filters"
|
||||
LazyChainLoadingKey = "lazy-chain-loading"
|
||||
SingleValidatorModeKey = "single-validator-mode"
|
||||
LowMemoryKey = "low-memory"
|
||||
MemoryProfileKey = "memory-profile"
|
||||
DevLightKey = "dev-light"
|
||||
ConfigProfileKey = "config-profile"
|
||||
DBCacheSizeKey = "db-cache-size"
|
||||
DBMemtableSizeKey = "db-memtable-size"
|
||||
StateCacheSizeKey = "state-cache-size"
|
||||
BlockCacheSizeKey = "block-cache-size"
|
||||
DisableBloomFiltersKey = "disable-bloom-filters"
|
||||
LazyChainLoadingKey = "lazy-chain-loading"
|
||||
SingleValidatorModeKey = "single-validator-mode"
|
||||
|
||||
// VM Transport Keys
|
||||
VMTransportKey = "vm-transport"
|
||||
|
||||
@@ -5,7 +5,7 @@ package config
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"github.com/go-json-experiment/json"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/go-json-experiment/json"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
|
||||
+10
-47
@@ -10,23 +10,23 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/node/server/http"
|
||||
"github.com/luxfi/node/benchlist"
|
||||
"github.com/luxfi/node/chains"
|
||||
"github.com/luxfi/node/genesis/builder"
|
||||
"github.com/luxfi/node/network"
|
||||
"github.com/luxfi/node/server/http"
|
||||
// "github.com/luxfi/consensus/core/router" // Unused
|
||||
"github.com/luxfi/crypto/bls"
|
||||
"github.com/luxfi/crypto/mldsa"
|
||||
mlkemcrypto "github.com/luxfi/crypto/mlkem"
|
||||
"github.com/luxfi/node/nets"
|
||||
"github.com/luxfi/node/network/tracker"
|
||||
"github.com/luxfi/node/trace"
|
||||
"github.com/luxfi/node/upgrade"
|
||||
"github.com/luxfi/node/trace"
|
||||
// "github.com/luxfi/log" // Unused
|
||||
"github.com/luxfi/math/set"
|
||||
"github.com/luxfi/node/utils/profiler"
|
||||
"github.com/luxfi/timer"
|
||||
"github.com/luxfi/node/utils/profiler"
|
||||
)
|
||||
|
||||
type APIIndexerConfig struct {
|
||||
@@ -101,13 +101,13 @@ type StakingConfig struct {
|
||||
// publishes in its validator-set entry so peers can encapsulate to it
|
||||
// for session-key establishment with no classical fallback. The
|
||||
// HandshakeMLKEMPriv stays local to the pod.
|
||||
StakingMLDSA *mldsa.PrivateKey `json:"-"`
|
||||
StakingMLDSAPub []byte `json:"-"`
|
||||
HandshakeMLKEMPriv *mlkemcrypto.PrivateKey `json:"-"`
|
||||
HandshakeMLKEMPub []byte `json:"-"`
|
||||
StakingMLDSA *mldsa.PrivateKey `json:"-"`
|
||||
StakingMLDSAPub []byte `json:"-"`
|
||||
HandshakeMLKEMPriv *mlkemcrypto.PrivateKey `json:"-"`
|
||||
HandshakeMLKEMPub []byte `json:"-"`
|
||||
// File paths kept for log-line context, mirroring StakingKeyPath etc.
|
||||
StakingMLDSAKeyPath string `json:"stakingMLDSAKeyPath,omitempty"`
|
||||
StakingMLDSAPubPath string `json:"stakingMLDSAPubPath,omitempty"`
|
||||
StakingMLDSAKeyPath string `json:"stakingMLDSAKeyPath,omitempty"`
|
||||
StakingMLDSAPubPath string `json:"stakingMLDSAPubPath,omitempty"`
|
||||
HandshakeMLKEMKeyPath string `json:"handshakeMLKEMKeyPath,omitempty"`
|
||||
HandshakeMLKEMPubPath string `json:"handshakeMLKEMPubPath,omitempty"`
|
||||
}
|
||||
@@ -169,7 +169,7 @@ type Config struct {
|
||||
|
||||
// Genesis information
|
||||
GenesisBytes []byte `json:"-"`
|
||||
UTXOAssetID ids.ID `json:"utxoAssetID"`
|
||||
UTXOAssetID ids.ID `json:"utxoAssetID"`
|
||||
|
||||
// ID of the network this node should connect to
|
||||
NetworkID uint32 `json:"networkID"`
|
||||
@@ -209,43 +209,6 @@ type Config struct {
|
||||
TrackedChains set.Set[ids.ID] `json:"trackedChains"`
|
||||
TrackAllChains bool `json:"trackAllChains"`
|
||||
|
||||
// DexValidator gates whether this node PARTICIPATES in the D-Chain DEX —
|
||||
// i.e. tracks/validates the D-Chain and dials the venue matcher engine.
|
||||
// Default false: the DEXVM factory is always registered (the D-Chain is a
|
||||
// first-class genesis chain in every build), but a node only ACTIVATES the
|
||||
// D-Chain when its operator opts in. The licensed/private piece is the GPU
|
||||
// venue ENGINE, never the node.
|
||||
//
|
||||
// Enforcement is real, not advisory: this flag flows into
|
||||
// chains.ManagerConfig.DexValidator and the chain manager's
|
||||
// authorizeChainActivation declines the D-Chain when it is false — even when
|
||||
// --track-all-chains queued the chain. It composes AND-wise with the
|
||||
// X-Chain operator-NFT gate (OptionalVMs[DexVMID].RequiredNFT joined with
|
||||
// NFTAuthorizationAssets): the D-Chain activates only if DexValidator is
|
||||
// true AND (when a network configures the dex-operator collection) the
|
||||
// node's staking X-address holds that NFT.
|
||||
//
|
||||
// Phase-2 (NOT built here — see participatesInDEXChain in node/node.go and
|
||||
// the StakingXAddress seam in chains.ManagerConfig): binding the NFT check
|
||||
// to proof-of-possession of the staking key, and mDNS local-fiber geofence
|
||||
// discovery among the HFT DEX cluster. Until a network configures the
|
||||
// dex-operator collection the NFT half is a no-op and this flag alone gates.
|
||||
DexValidator bool `json:"dexValidator"`
|
||||
|
||||
// NFTAuthorizationAssets supplies, per network, the X-Chain operator-NFT
|
||||
// collection asset for each NFT-gated optional VM, keyed by VMID (e.g.
|
||||
// DexVMID -> the dex-operator collection asset; BridgeVMID -> the
|
||||
// bridge-operator collection asset). It is the per-network VALUE half of the
|
||||
// activation gate; the POLICY half (which VMs need an NFT + which group)
|
||||
// lives in node.OptionalVMs. node.chainAuthorizationsFor joins the two into
|
||||
// the chain manager's ChainAuthorizations map.
|
||||
//
|
||||
// Empty by default: minting the real operator collections is a follow-up, so
|
||||
// until a network sets an asset here the corresponding chain is ungated
|
||||
// (today's opt-in-flag behavior) while the gate itself stays fail-closed for
|
||||
// any configured-but-unheld NFT.
|
||||
NFTAuthorizationAssets map[ids.ID]ids.ID `json:"nftAuthorizationAssets"`
|
||||
|
||||
NetConfigs map[ids.ID]nets.Config `json:"chainConfigs"`
|
||||
|
||||
ChainConfigs map[string]chains.ChainConfig `json:"-"`
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
"encoding/json"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
@@ -55,7 +54,7 @@ func TestProcessContext(t *testing.T) {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
contextJSON, err := json.Marshal(test.context, jsontext.WithIndent("\t"))
|
||||
contextJSON, err := json.MarshalIndent(test.context, "", "\t")
|
||||
require.NoError(err)
|
||||
require.JSONEq(test.expected, string(contextJSON))
|
||||
})
|
||||
|
||||
+2
-4
@@ -7,9 +7,7 @@
|
||||
package spec
|
||||
|
||||
import (
|
||||
"github.com/go-json-experiment/json"
|
||||
jsonv1 "github.com/go-json-experiment/json/v1"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -209,7 +207,7 @@ func (s *ConfigSpec) DeprecatedFlags() []FlagSpec {
|
||||
|
||||
// JSON returns the spec as formatted JSON.
|
||||
func (s *ConfigSpec) JSON() ([]byte, error) {
|
||||
return json.Marshal(s, jsontext.WithIndent(" "), jsonv1.FormatDurationAsNano(true))
|
||||
return json.MarshalIndent(s, "", " ")
|
||||
}
|
||||
|
||||
// ValidateValue checks if a value is valid for a flag.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
package spec
|
||||
|
||||
import (
|
||||
"github.com/go-json-experiment/json"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/crypto/mldsa"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// writeMLDSAFixture generates an ML-DSA-65 keypair, PEM-encodes both halves
|
||||
// with the block types loadStakingMLDSA expects, writes them under dir, and
|
||||
// returns (privPath, pubPath, privPEM, pubPEM).
|
||||
func writeMLDSAFixture(t *testing.T, dir string) (privPath, pubPath string, privPEM, pubPEM []byte) {
|
||||
t.Helper()
|
||||
priv, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
|
||||
require.NoError(t, err)
|
||||
|
||||
privPEM = pem.EncodeToMemory(&pem.Block{Type: "ML-DSA-65 PRIVATE KEY", Bytes: priv.Bytes()})
|
||||
pubPEM = pem.EncodeToMemory(&pem.Block{Type: "ML-DSA-65 PUBLIC KEY", Bytes: priv.PublicKey.Bytes()})
|
||||
|
||||
require.NoError(t, os.MkdirAll(dir, 0o755))
|
||||
privPath = filepath.Join(dir, "mldsa.key")
|
||||
pubPath = filepath.Join(dir, "mldsa.pub")
|
||||
require.NoError(t, os.WriteFile(privPath, privPEM, 0o600))
|
||||
require.NoError(t, os.WriteFile(pubPath, pubPEM, 0o644))
|
||||
return privPath, pubPath, privPEM, pubPEM
|
||||
}
|
||||
|
||||
// TestLoadStakingMLDSA_PathForm: both keys via explicit --staking-mldsa-*-file
|
||||
// paths. The canonical strict-PQ deploy form.
|
||||
func TestLoadStakingMLDSA_PathForm(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
privPath, pubPath, _, _ := writeMLDSAFixture(t, dir)
|
||||
|
||||
v, err := BuildViper(BuildFlagSet(), []string{
|
||||
"--" + StakingMLDSAKeyPathKey + "=" + privPath,
|
||||
"--" + StakingMLDSAPubKeyPathKey + "=" + pubPath,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
priv, pub, _, _, err := loadStakingMLDSA(v)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, priv)
|
||||
require.NotEmpty(t, pub)
|
||||
}
|
||||
|
||||
// TestLoadStakingMLDSA_ContentForm: keys via base64 PEM --*-content flags.
|
||||
func TestLoadStakingMLDSA_ContentForm(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
_, _, privPEM, pubPEM := writeMLDSAFixture(t, dir)
|
||||
|
||||
v, err := BuildViper(BuildFlagSet(), []string{
|
||||
"--" + StakingMLDSAKeyContentKey + "=" + base64.StdEncoding.EncodeToString(privPEM),
|
||||
"--" + StakingMLDSAPubKeyContentKey + "=" + base64.StdEncoding.EncodeToString(pubPEM),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
priv, pub, _, _, err := loadStakingMLDSA(v)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, priv)
|
||||
require.NotEmpty(t, pub)
|
||||
}
|
||||
|
||||
// TestLoadStakingMLDSA_EmptyContentFallsThroughToPath is the regression guard
|
||||
// for the strict-PQ activation outage: when a deploy passes the *-content flag
|
||||
// blank (e.g. an env/template that rendered to "") alongside a valid *-file
|
||||
// path, the loader MUST consult the path. The previous behavior short-circuited
|
||||
// on the empty content value and returned no key, so StakingMLDSAPub stayed
|
||||
// empty, IsStrictPQ() was false, and the node booted under an ECDSA NodeID with
|
||||
// no error — silently degrading a strict-PQ validator to classical-compat.
|
||||
func TestLoadStakingMLDSA_EmptyContentFallsThroughToPath(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
privPath, pubPath, _, _ := writeMLDSAFixture(t, dir)
|
||||
|
||||
v, err := BuildViper(BuildFlagSet(), []string{
|
||||
"--" + StakingMLDSAKeyPathKey + "=" + privPath,
|
||||
"--" + StakingMLDSAKeyContentKey + "=", // blank content
|
||||
"--" + StakingMLDSAPubKeyPathKey + "=" + pubPath,
|
||||
"--" + StakingMLDSAPubKeyContentKey + "=", // blank content
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
priv, pub, gotPrivPath, gotPubPath, err := loadStakingMLDSA(v)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, priv, "blank content must fall through to the path")
|
||||
require.NotEmpty(t, pub, "blank content must fall through to the path")
|
||||
require.Equal(t, privPath, gotPrivPath)
|
||||
require.Equal(t, pubPath, gotPubPath)
|
||||
}
|
||||
|
||||
// TestLoadStakingMLDSA_NeitherIsClassicalCompat: no key material at all (and a
|
||||
// default pub path that does not exist) is classical-compat — empty, no error.
|
||||
func TestLoadStakingMLDSA_NeitherIsClassicalCompat(t *testing.T) {
|
||||
// Point DataDir at an empty dir so the default pub path resolves to a
|
||||
// non-existent file rather than picking up a developer's ~/.lux key.
|
||||
v, err := BuildViper(BuildFlagSet(), []string{
|
||||
"--" + DataDirKey + "=" + t.TempDir(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
priv, pub, _, _, err := loadStakingMLDSA(v)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, priv)
|
||||
require.Empty(t, pub)
|
||||
}
|
||||
|
||||
// TestLoadStakingMLDSA_PrivOnlyIsFatal: a private key with no public key is a
|
||||
// loud config error — a strict-PQ validator must never silently degrade.
|
||||
func TestLoadStakingMLDSA_PrivOnlyIsFatal(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
_, _, privPEM, _ := writeMLDSAFixture(t, filepath.Join(base, "staking"))
|
||||
|
||||
v, err := BuildViper(BuildFlagSet(), []string{
|
||||
"--" + DataDirKey + "=" + base, // default pub path under here will be missing... but priv content wins
|
||||
"--" + StakingMLDSAKeyContentKey + "=" + base64.StdEncoding.EncodeToString(privPEM),
|
||||
"--" + StakingMLDSAPubKeyPathKey + "=" + filepath.Join(base, "does-not-exist.pub"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, _, _, err = loadStakingMLDSA(v)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "both private and public key are required")
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
# Consensus Package - AI Assistant Guide
|
||||
|
||||
This package is node-side glue around the canonical consensus engine in
|
||||
`github.com/luxfi/consensus`. It does NOT implement the protocol — the
|
||||
protocol lives in that module.
|
||||
|
||||
## Package Structure
|
||||
|
||||
- `acceptor.go` - Node-side block-acceptance callbacks (chain-ID-keyed)
|
||||
- `quasar/` - Node-side wiring around `consensus/protocol/quasar`
|
||||
- `zap/` - ZAP agentic-consensus / DID bridge (self-contained, opt-in)
|
||||
|
||||
## Acceptor
|
||||
|
||||
`Acceptor` interface called when consensus accepts a block / vertex.
|
||||
`AcceptorGroup` manages multiple acceptors per chain — used by the indexer
|
||||
and warp IPC. The variant here differs from the canonical
|
||||
`luxfi/consensus/core.Acceptor` (different signature: `*runtime.Runtime`
|
||||
instead of `context.Context`, plus chain-ID-keyed registration).
|
||||
|
||||
## Quasar Wiring
|
||||
|
||||
The `quasar/` subpackage wraps `github.com/luxfi/consensus/protocol/quasar`
|
||||
with node-specific adapters (P-Chain validator state, BLS signing keys,
|
||||
Corona threshold coordinator stub).
|
||||
|
||||
### Signature Types
|
||||
|
||||
```go
|
||||
SignatureTypeBLS // Classical BLS
|
||||
SignatureTypeCorona // Post-quantum threshold
|
||||
SignatureTypeQuasar // Hybrid BLS + Corona
|
||||
SignatureTypeMLDSA // Fallback ML-DSA
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
GOWORK=off go test ./consensus/... -v
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `github.com/luxfi/consensus` - Canonical consensus protocol (engine,
|
||||
protocol/quasar, types, etc.)
|
||||
- `github.com/luxfi/ids` - ID types
|
||||
- `github.com/luxfi/log` - Logging
|
||||
- `github.com/luxfi/runtime` - Runtime context for acceptors
|
||||
@@ -9,9 +9,9 @@ import (
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/luxfi/runtime"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/runtime"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -11,8 +11,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/accel"
|
||||
"github.com/luxfi/crypto/bls"
|
||||
"github.com/luxfi/crypto/mldsa"
|
||||
)
|
||||
|
||||
// GPUVerifyPipeline fuses multiple cryptographic verification operations into
|
||||
@@ -61,41 +59,41 @@ type ZKWork struct {
|
||||
Bases [][]byte // [M, N, point_size]
|
||||
}
|
||||
|
||||
// MLDSAWork holds a batch of ML-DSA signatures to verify.
|
||||
// MLDSAWork holds a batch of ML-DSA (Dilithium) signatures to verify.
|
||||
type MLDSAWork struct {
|
||||
Messages [][]byte // [N, msg_len]
|
||||
Signatures [][]byte // [N, 3309] FIPS-204 ML-DSA-65 (3293 was the stale round-3 Dilithium3 size)
|
||||
PubKeys [][]byte // [N, 1952] FIPS-204 ML-DSA-65
|
||||
Signatures [][]byte // [N, 3293] Dilithium3
|
||||
PubKeys [][]byte // [N, 1952] Dilithium3
|
||||
}
|
||||
|
||||
// BlockVerifyWork contains all verification batches for a single block.
|
||||
type BlockVerifyWork struct {
|
||||
BLS *BLSWork
|
||||
BLS *BLSWork
|
||||
Corona *CoronaWork
|
||||
ZK *ZKWork
|
||||
MLDSA *MLDSAWork
|
||||
ZK *ZKWork
|
||||
MLDSA *MLDSAWork
|
||||
}
|
||||
|
||||
// BlockVerifyResult contains verification results for all batch types.
|
||||
type BlockVerifyResult struct {
|
||||
BLSValid []bool
|
||||
BLSValid []bool
|
||||
CoronaValid []bool
|
||||
ZKValid bool
|
||||
MLDSAValid []bool
|
||||
ZKValid bool
|
||||
MLDSAValid []bool
|
||||
|
||||
GPUUsed bool
|
||||
BLSTime time.Duration
|
||||
GPUUsed bool
|
||||
BLSTime time.Duration
|
||||
CoronaTime time.Duration
|
||||
ZKTime time.Duration
|
||||
MLDSATime time.Duration
|
||||
TotalTime time.Duration
|
||||
ZKTime time.Duration
|
||||
MLDSATime time.Duration
|
||||
TotalTime time.Duration
|
||||
}
|
||||
|
||||
var (
|
||||
ErrBLSSizeMismatch = errors.New("BLS batch size mismatch: messages, signatures, and pubkeys must have equal length")
|
||||
ErrBLSSizeMismatch = errors.New("BLS batch size mismatch: messages, signatures, and pubkeys must have equal length")
|
||||
ErrCoronaSizeMismatch = errors.New("Corona batch size mismatch: messages, signatures, and pubkeys must have equal length")
|
||||
ErrZKSizeMismatch = errors.New("ZK batch size mismatch: scalars and bases must have equal length")
|
||||
ErrMLDSASizeMismatch = errors.New("ML-DSA batch size mismatch: messages, signatures, and pubkeys must have equal length")
|
||||
ErrZKSizeMismatch = errors.New("ZK batch size mismatch: scalars and bases must have equal length")
|
||||
ErrMLDSASizeMismatch = errors.New("ML-DSA batch size mismatch: messages, signatures, and pubkeys must have equal length")
|
||||
)
|
||||
|
||||
// VerifyBlock dispatches all verification work for a block through the GPU pipeline.
|
||||
@@ -378,8 +376,8 @@ func gpuMLDSAVerify(sess *accel.Session, work *MLDSAWork) ([]bool, error) {
|
||||
n := len(work.Messages)
|
||||
|
||||
msgLen := maxByteLen(work.Messages)
|
||||
sigLen := 3309 // FIPS-204 ML-DSA-65 signature (3293 was the stale round-3 Dilithium3 size)
|
||||
pkLen := 1952 // FIPS-204 ML-DSA-65 public key (unchanged across the round-3 -> final transition)
|
||||
sigLen := 3293 // Dilithium3 signature
|
||||
pkLen := 1952 // Dilithium3 public key
|
||||
|
||||
msgFlat := flattenPadded(work.Messages, n, msgLen)
|
||||
sigFlat := flattenPadded(work.Signatures, n, sigLen)
|
||||
@@ -487,75 +485,42 @@ func (p *GPUVerifyPipeline) verifyCPU(work *BlockVerifyWork) *BlockVerifyResult
|
||||
return result
|
||||
}
|
||||
|
||||
// CPU fallback implementations — the pure-Go correctness oracle.
|
||||
//
|
||||
// These perform REAL per-element cryptographic verification using the
|
||||
// luxfi/crypto pure-Go primitives (CGO_ENABLED=0-clean). They interpret each
|
||||
// element's raw bytes with the SAME layout the GPU kernels use (see
|
||||
// gpuBLSVerify / gpuMLDSAVerify), so the CPU and GPU paths are a genuine
|
||||
// equivalence pair: a no-GPU node accepts exactly the signatures a GPU node
|
||||
// accepts, and never rubber-stamps a forged one.
|
||||
//
|
||||
// Corona and ZK have no pure-Go verifier in luxfi/crypto, so those paths
|
||||
// fail closed (return false) rather than format-check-and-accept. They MUST
|
||||
// be wired to a real verifier before block-accept depends on them.
|
||||
// CPU fallback implementations.
|
||||
// These verify signatures using pure Go. In a non-CGO build without real
|
||||
// crypto libraries for BLS/Dilithium, we validate format and return true
|
||||
// for well-formed inputs (actual verification would use luxfi/crypto).
|
||||
|
||||
func cpuBLSVerify(work *BLSWork) []bool {
|
||||
valid := make([]bool, len(work.Messages))
|
||||
for i := range work.Messages {
|
||||
// Mirror gpuBLSVerify's layout: pk is a 48-byte compressed G1 point,
|
||||
// sig is a 96-byte compressed G2 point, msg is the raw message.
|
||||
// PublicKeyFromCompressedBytes / SignatureFromBytes enforce the exact
|
||||
// length, on-curve and subgroup membership the blst/CGO path enforces;
|
||||
// any malformed input fails closed (constructor error => false).
|
||||
pk, err := bls.PublicKeyFromCompressedBytes(work.PubKeys[i])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
sig, err := bls.SignatureFromBytes(work.Signatures[i])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
valid[i] = bls.Verify(pk, sig, work.Messages[i])
|
||||
// Format check: message present, sig is 96 bytes, pk is 48 bytes
|
||||
valid[i] = len(work.Messages[i]) > 0 &&
|
||||
len(work.Signatures[i]) == 96 &&
|
||||
len(work.PubKeys[i]) == 48
|
||||
}
|
||||
return valid
|
||||
}
|
||||
|
||||
func cpuCoronaVerify(work *CoronaWork) []bool {
|
||||
// FAIL CLOSED: luxfi/crypto exposes no pure-Go Corona (lattice threshold)
|
||||
// signature verifier, and the GPU Corona kernel is the known-wrong-prime
|
||||
// BLOCKED kernel. There is no correct way to verify a Corona signature on
|
||||
// the CPU here, so every element is rejected. Never return true for an
|
||||
// unverified signature. Wire a real Corona verifier before block-accept
|
||||
// consumes this result.
|
||||
return make([]bool, len(work.Messages))
|
||||
valid := make([]bool, len(work.Messages))
|
||||
for i := range work.Messages {
|
||||
valid[i] = len(work.Messages[i]) > 0 &&
|
||||
len(work.Signatures[i]) > 0 &&
|
||||
len(work.PubKeys[i]) > 0
|
||||
}
|
||||
return valid
|
||||
}
|
||||
|
||||
func cpuZKVerify(work *ZKWork) bool {
|
||||
// FAIL CLOSED: luxfi/crypto exposes no standalone pure-Go ZK proof
|
||||
// verifier (the accel MSM path is a GPU primitive, not a proof check), so
|
||||
// CPU verification cannot establish proof validity. Reject rather than
|
||||
// rubber-stamp. Wire a real ZK verifier before block-accept consumes this.
|
||||
return false
|
||||
return len(work.Scalars) > 0 && len(work.Bases) > 0
|
||||
}
|
||||
|
||||
func cpuMLDSAVerify(work *MLDSAWork) []bool {
|
||||
valid := make([]bool, len(work.Messages))
|
||||
for i := range work.Messages {
|
||||
// Mirror gpuMLDSAVerify's layout: ML-DSA-65, pk 1952 bytes, msg raw.
|
||||
// PublicKeyFromBytes enforces the exact key length and decodes the
|
||||
// point; VerifySignature uses the FIPS 204 nil-context verify,
|
||||
// matching the kernel's contextless per-tx verification, and accepts
|
||||
// the signature at its true FIPS-204 length (3309 bytes). The GPU
|
||||
// flatten width (gpuMLDSAVerify's sigLen) now agrees at 3309, so the
|
||||
// CPU oracle and GPU path size the signature identically; see
|
||||
// TestMLDSA_WorkStructSizeIsCanonical. Malformed pk fails closed
|
||||
// (constructor error).
|
||||
pub, err := mldsa.PublicKeyFromBytes(work.PubKeys[i], mldsa.MLDSA65)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
valid[i] = pub.VerifySignature(work.Messages[i], work.Signatures[i])
|
||||
valid[i] = len(work.Messages[i]) > 0 &&
|
||||
len(work.Signatures[i]) == 3293 &&
|
||||
len(work.PubKeys[i]) == 1952
|
||||
}
|
||||
return valid
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ import (
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/crypto/bls"
|
||||
"github.com/luxfi/crypto/mldsa"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -19,54 +17,17 @@ func makeRandomBytes(n int) []byte {
|
||||
return b
|
||||
}
|
||||
|
||||
// makeValidBLSEntry returns (msg, sig, pk) for a REAL BLS signature over a
|
||||
// random 32-byte message: pk is the 48-byte compressed G1 key, sig is the
|
||||
// 96-byte compressed G2 signature. The CPU oracle must accept this.
|
||||
func makeValidBLSEntry(t testing.TB) (msg, sig, pk []byte) {
|
||||
t.Helper()
|
||||
sk, err := bls.NewSecretKey()
|
||||
require.NoError(t, err)
|
||||
msg = makeRandomBytes(32)
|
||||
s, err := sk.Sign(msg)
|
||||
require.NoError(t, err)
|
||||
sig = bls.SignatureToBytes(s)
|
||||
pk = bls.PublicKeyToCompressedBytes(sk.PublicKey())
|
||||
require.Len(t, sig, 96)
|
||||
require.Len(t, pk, 48)
|
||||
return msg, sig, pk
|
||||
}
|
||||
|
||||
// makeValidMLDSAEntry returns (msg, sig, pk) for a REAL ML-DSA-65 signature
|
||||
// over a random 64-byte message. The sizes are taken from the crypto package
|
||||
// constants (FIPS-204 ML-DSA-65: pk 1952 bytes, sig 3309 bytes) rather than
|
||||
// hard-coded — see TestMLDSA_WorkStructSizeIsCanonical, which holds the
|
||||
// MLDSAWork struct / gpuMLDSAVerify sig constant pinned at the FIPS-204 3309
|
||||
// (corrected from the stale round-3 Dilithium3 3293). The CPU oracle uses the
|
||||
// typed Verify, so it accepts the real signature regardless.
|
||||
func makeValidMLDSAEntry(t testing.TB) (msg, sig, pk []byte) {
|
||||
t.Helper()
|
||||
priv, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
|
||||
require.NoError(t, err)
|
||||
msg = makeRandomBytes(64)
|
||||
sig, err = priv.Sign(rand.Reader, msg, nil)
|
||||
require.NoError(t, err)
|
||||
pk = priv.PublicKey.Bytes()
|
||||
require.Len(t, sig, mldsa.MLDSA65SignatureSize)
|
||||
require.Len(t, pk, mldsa.MLDSA65PublicKeySize)
|
||||
return msg, sig, pk
|
||||
}
|
||||
|
||||
// makeBLSWork creates BLSWork with n entries carrying REAL valid BLS
|
||||
// signatures (the CPU oracle now performs real verification).
|
||||
func makeBLSWork(t testing.TB, n int) *BLSWork {
|
||||
t.Helper()
|
||||
// makeBLSWork creates BLSWork with n entries using correct BLS sizes.
|
||||
func makeBLSWork(n int) *BLSWork {
|
||||
w := &BLSWork{
|
||||
Messages: make([][]byte, n),
|
||||
Signatures: make([][]byte, n),
|
||||
PubKeys: make([][]byte, n),
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
w.Messages[i], w.Signatures[i], w.PubKeys[i] = makeValidBLSEntry(t)
|
||||
w.Messages[i] = makeRandomBytes(32)
|
||||
w.Signatures[i] = makeRandomBytes(96) // BLS G2 point
|
||||
w.PubKeys[i] = makeRandomBytes(48) // BLS G1 point
|
||||
}
|
||||
return w
|
||||
}
|
||||
@@ -99,18 +60,17 @@ func makeZKWork(m int) *ZKWork {
|
||||
return w
|
||||
}
|
||||
|
||||
// makeMLDSAWork creates MLDSAWork with n entries carrying REAL valid
|
||||
// ML-DSA-65 (Dilithium3) signatures (the CPU oracle now performs real
|
||||
// verification).
|
||||
func makeMLDSAWork(t testing.TB, n int) *MLDSAWork {
|
||||
t.Helper()
|
||||
// makeMLDSAWork creates MLDSAWork with n entries using correct Dilithium3 sizes.
|
||||
func makeMLDSAWork(n int) *MLDSAWork {
|
||||
w := &MLDSAWork{
|
||||
Messages: make([][]byte, n),
|
||||
Signatures: make([][]byte, n),
|
||||
PubKeys: make([][]byte, n),
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
w.Messages[i], w.Signatures[i], w.PubKeys[i] = makeValidMLDSAEntry(t)
|
||||
w.Messages[i] = makeRandomBytes(64)
|
||||
w.Signatures[i] = makeRandomBytes(3293) // Dilithium3 signature
|
||||
w.PubKeys[i] = makeRandomBytes(1952) // Dilithium3 public key
|
||||
}
|
||||
return w
|
||||
}
|
||||
@@ -119,33 +79,32 @@ func TestGPUPipeline_AllFourTypes(t *testing.T) {
|
||||
pipeline := NewGPUVerifyPipeline()
|
||||
|
||||
work := &BlockVerifyWork{
|
||||
BLS: makeBLSWork(t, 5),
|
||||
BLS: makeBLSWork(5),
|
||||
Corona: makeCoronaWork(3),
|
||||
ZK: makeZKWork(2),
|
||||
MLDSA: makeMLDSAWork(t, 10),
|
||||
ZK: makeZKWork(2),
|
||||
MLDSA: makeMLDSAWork(10),
|
||||
}
|
||||
|
||||
result, err := pipeline.VerifyBlock(work)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
|
||||
// BLS results: real valid signatures, CPU oracle accepts.
|
||||
// BLS results
|
||||
require.Len(t, result.BLSValid, 5, "should have 5 BLS results")
|
||||
for i, v := range result.BLSValid {
|
||||
require.True(t, v, "BLS[%d] should be valid", i)
|
||||
}
|
||||
|
||||
// Corona results: no pure-Go Corona verifier exists, so the CPU oracle
|
||||
// fails closed — every element is rejected (never rubber-stamped).
|
||||
// Corona results
|
||||
require.Len(t, result.CoronaValid, 3, "should have 3 Corona results")
|
||||
for i, v := range result.CoronaValid {
|
||||
require.False(t, v, "Corona[%d] must fail closed (no pure-Go verifier)", i)
|
||||
require.True(t, v, "Corona[%d] should be valid", i)
|
||||
}
|
||||
|
||||
// ZK result: no pure-Go ZK verifier exists, so the CPU oracle fails closed.
|
||||
require.False(t, result.ZKValid, "ZK batch must fail closed (no pure-Go verifier)")
|
||||
// ZK result
|
||||
require.True(t, result.ZKValid, "ZK batch should be valid")
|
||||
|
||||
// ML-DSA results: real valid signatures, CPU oracle accepts.
|
||||
// ML-DSA results
|
||||
require.Len(t, result.MLDSAValid, 10, "should have 10 ML-DSA results")
|
||||
for i, v := range result.MLDSAValid {
|
||||
require.True(t, v, "MLDSA[%d] should be valid", i)
|
||||
@@ -170,15 +129,15 @@ func TestGPUPipeline_CPUFallback(t *testing.T) {
|
||||
pipeline := NewGPUVerifyPipeline()
|
||||
|
||||
work := &BlockVerifyWork{
|
||||
BLS: makeBLSWork(t, 3),
|
||||
MLDSA: makeMLDSAWork(t, 4),
|
||||
BLS: makeBLSWork(3),
|
||||
MLDSA: makeMLDSAWork(4),
|
||||
}
|
||||
|
||||
result, err := pipeline.VerifyBlock(work)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
|
||||
// CPU fallback performs real verification; valid signatures are accepted.
|
||||
// CPU fallback must produce valid results for well-formed inputs
|
||||
require.Len(t, result.BLSValid, 3)
|
||||
for i, v := range result.BLSValid {
|
||||
require.True(t, v, "CPU BLS[%d] should be valid", i)
|
||||
@@ -196,111 +155,6 @@ func TestGPUPipeline_CPUFallback(t *testing.T) {
|
||||
require.Equal(t, uint64(1), stats.CPUVerifies)
|
||||
}
|
||||
|
||||
// TestCPUVerify_RealOracle proves the CPU fallback is a real cryptographic
|
||||
// oracle, not a length-checking rubber stamp: a valid signature is accepted
|
||||
// and a well-formed-but-FORGED signature (correct lengths, wrong bytes) is
|
||||
// REJECTED. The forged-rejection case is the regression guard against the
|
||||
// old `return true for well-formed inputs` behavior.
|
||||
func TestCPUVerify_RealOracle(t *testing.T) {
|
||||
t.Run("BLS valid accepted, forged rejected", func(t *testing.T) {
|
||||
msg, sig, pk := makeValidBLSEntry(t)
|
||||
|
||||
// Valid signature => accepted.
|
||||
good := cpuBLSVerify(&BLSWork{
|
||||
Messages: [][]byte{msg},
|
||||
Signatures: [][]byte{sig},
|
||||
PubKeys: [][]byte{pk},
|
||||
})
|
||||
require.Equal(t, []bool{true}, good, "valid BLS signature must be accepted")
|
||||
|
||||
// Forged signature: correct 96-byte length, random bytes => rejected.
|
||||
forgedSig := makeRandomBytes(96)
|
||||
bad := cpuBLSVerify(&BLSWork{
|
||||
Messages: [][]byte{msg},
|
||||
Signatures: [][]byte{forgedSig},
|
||||
PubKeys: [][]byte{pk},
|
||||
})
|
||||
require.Equal(t, []bool{false}, bad, "forged BLS signature (right length, wrong bytes) must be REJECTED")
|
||||
|
||||
// Valid signature against the WRONG message => rejected.
|
||||
wrongMsg := cpuBLSVerify(&BLSWork{
|
||||
Messages: [][]byte{makeRandomBytes(32)},
|
||||
Signatures: [][]byte{sig},
|
||||
PubKeys: [][]byte{pk},
|
||||
})
|
||||
require.Equal(t, []bool{false}, wrongMsg, "BLS signature over a different message must be REJECTED")
|
||||
})
|
||||
|
||||
t.Run("MLDSA valid accepted, forged rejected", func(t *testing.T) {
|
||||
msg, sig, pk := makeValidMLDSAEntry(t)
|
||||
|
||||
// Valid signature => accepted.
|
||||
good := cpuMLDSAVerify(&MLDSAWork{
|
||||
Messages: [][]byte{msg},
|
||||
Signatures: [][]byte{sig},
|
||||
PubKeys: [][]byte{pk},
|
||||
})
|
||||
require.Equal(t, []bool{true}, good, "valid ML-DSA signature must be accepted")
|
||||
|
||||
// Forged signature: correct length, random bytes => rejected.
|
||||
forgedSig := makeRandomBytes(mldsa.MLDSA65SignatureSize)
|
||||
bad := cpuMLDSAVerify(&MLDSAWork{
|
||||
Messages: [][]byte{msg},
|
||||
Signatures: [][]byte{forgedSig},
|
||||
PubKeys: [][]byte{pk},
|
||||
})
|
||||
require.Equal(t, []bool{false}, bad, "forged ML-DSA signature (right length, wrong bytes) must be REJECTED")
|
||||
|
||||
// Valid signature against the WRONG message => rejected.
|
||||
wrongMsg := cpuMLDSAVerify(&MLDSAWork{
|
||||
Messages: [][]byte{makeRandomBytes(64)},
|
||||
Signatures: [][]byte{sig},
|
||||
PubKeys: [][]byte{pk},
|
||||
})
|
||||
require.Equal(t, []bool{false}, wrongMsg, "ML-DSA signature over a different message must be REJECTED")
|
||||
})
|
||||
|
||||
t.Run("Corona fails closed", func(t *testing.T) {
|
||||
// No pure-Go Corona verifier exists; every element must be rejected,
|
||||
// never rubber-stamped on length alone.
|
||||
got := cpuCoronaVerify(&CoronaWork{
|
||||
Messages: [][]byte{makeRandomBytes(48), makeRandomBytes(48)},
|
||||
Signatures: [][]byte{makeRandomBytes(512), makeRandomBytes(512)},
|
||||
PubKeys: [][]byte{makeRandomBytes(256), makeRandomBytes(256)},
|
||||
})
|
||||
require.Equal(t, []bool{false, false}, got, "Corona must fail closed for all elements")
|
||||
})
|
||||
|
||||
t.Run("ZK fails closed", func(t *testing.T) {
|
||||
// No pure-Go ZK proof verifier exists; the batch must be rejected.
|
||||
got := cpuZKVerify(&ZKWork{
|
||||
Scalars: [][]byte{makeRandomBytes(32)},
|
||||
Bases: [][]byte{makeRandomBytes(64)},
|
||||
})
|
||||
require.False(t, got, "ZK must fail closed")
|
||||
})
|
||||
}
|
||||
|
||||
// TestMLDSA_WorkStructSizeIsCanonical pins the FIPS-204 ML-DSA-65 signature and
|
||||
// public key sizes that the MLDSAWork struct comment and gpuMLDSAVerify's
|
||||
// fixed-size flatten now use. luxfi/crypto (circl v1.6.3, FIPS-204 final)
|
||||
// produces 3309-byte ML-DSA-65 signatures (5*640 + 55 + 6 + 48 = 3309); the
|
||||
// GPU flatten width was corrected from the stale round-3 Dilithium3 size 3293
|
||||
// to 3309 so the GPU path no longer clamps/corrupts a real signature. The
|
||||
// public key size (1952) is unchanged across the round-3 -> final transition.
|
||||
//
|
||||
// This is the equivalence-pair guard: the CPU oracle (cpuMLDSAVerify) parses
|
||||
// pk via the typed PublicKeyFromBytes and calls VerifySignature, accepting the
|
||||
// real 3309-byte signature; the GPU path sizes the signature identically. If
|
||||
// the crypto constant ever drifts, this test fails before any divergence
|
||||
// reaches the GPU ML-DSA kernel (not yet wired into block-accept).
|
||||
func TestMLDSA_WorkStructSizeIsCanonical(t *testing.T) {
|
||||
require.Equal(t, 1952, mldsa.MLDSA65PublicKeySize,
|
||||
"ML-DSA-65 public key is 1952 bytes (matches MLDSAWork / gpuMLDSAVerify pkLen)")
|
||||
require.Equal(t, 3309, mldsa.MLDSA65SignatureSize,
|
||||
"ML-DSA-65 signature is 3309 bytes (FIPS-204), matching the MLDSAWork struct / gpuMLDSAVerify sigLen")
|
||||
}
|
||||
|
||||
func TestGPUPipeline_EmptyBatches(t *testing.T) {
|
||||
pipeline := NewGPUVerifyPipeline()
|
||||
|
||||
@@ -325,7 +179,7 @@ func TestGPUPipeline_EmptyBatches(t *testing.T) {
|
||||
{
|
||||
name: "BLS filled, rest nil",
|
||||
work: &BlockVerifyWork{
|
||||
BLS: makeBLSWork(t, 2),
|
||||
BLS: makeBLSWork(2),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -337,7 +191,7 @@ func TestGPUPipeline_EmptyBatches(t *testing.T) {
|
||||
{
|
||||
name: "MLDSA only",
|
||||
work: &BlockVerifyWork{
|
||||
MLDSA: makeMLDSAWork(t, 1),
|
||||
MLDSA: makeMLDSAWork(1),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -422,10 +276,10 @@ func BenchmarkGPUPipeline(b *testing.B) {
|
||||
pipeline := NewGPUVerifyPipeline()
|
||||
|
||||
work := &BlockVerifyWork{
|
||||
BLS: makeBLSWork(b, 100),
|
||||
BLS: makeBLSWork(100),
|
||||
Corona: makeCoronaWork(50),
|
||||
ZK: makeZKWork(10),
|
||||
MLDSA: makeMLDSAWork(b, 200),
|
||||
ZK: makeZKWork(10),
|
||||
MLDSA: makeMLDSAWork(200),
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
@@ -116,11 +116,11 @@ func generateValidatorStates(n int) []ValidatorState {
|
||||
_, _ = rand.Read(rtKey)
|
||||
|
||||
states[i] = ValidatorState{
|
||||
NodeID: ids.GenerateTestNodeID(),
|
||||
Weight: 1000,
|
||||
BLSPubKey: blsKey,
|
||||
NodeID: ids.GenerateTestNodeID(),
|
||||
Weight: 1000,
|
||||
BLSPubKey: blsKey,
|
||||
CoronaKey: rtKey,
|
||||
Active: true,
|
||||
Active: true,
|
||||
}
|
||||
}
|
||||
return states
|
||||
@@ -575,13 +575,13 @@ func TestQuasarMemoryPressure(t *testing.T) {
|
||||
_, _ = rand.Read(blockID[:])
|
||||
|
||||
finality := &QuantumFinality{
|
||||
BlockID: blockID,
|
||||
PChainHeight: uint64(i),
|
||||
QChainHeight: uint64(i),
|
||||
BLSProof: make([]byte, 96),
|
||||
CoronaProof: make([]byte, 1024),
|
||||
TotalWeight: 1000,
|
||||
SignerWeight: 700,
|
||||
BlockID: blockID,
|
||||
PChainHeight: uint64(i),
|
||||
QChainHeight: uint64(i),
|
||||
BLSProof: make([]byte, 96),
|
||||
CoronaProof: make([]byte, 1024),
|
||||
TotalWeight: 1000,
|
||||
SignerWeight: 700,
|
||||
}
|
||||
q.SetFinalized(blockID, finality)
|
||||
}
|
||||
@@ -913,10 +913,10 @@ func TestQuasarEdgeCases(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
finality := &QuantumFinality{
|
||||
BLSProof: nil,
|
||||
CoronaProof: nil,
|
||||
TotalWeight: 1000,
|
||||
SignerWeight: 700,
|
||||
BLSProof: nil,
|
||||
CoronaProof: nil,
|
||||
TotalWeight: 1000,
|
||||
SignerWeight: 700,
|
||||
}
|
||||
|
||||
err = q.Verify(finality)
|
||||
@@ -928,10 +928,10 @@ func TestQuasarEdgeCases(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
finality := &QuantumFinality{
|
||||
BLSProof: []byte("proof"),
|
||||
CoronaProof: []byte("proof"),
|
||||
TotalWeight: 1000,
|
||||
SignerWeight: 500, // Only 50%, needs 67%
|
||||
BLSProof: []byte("proof"),
|
||||
CoronaProof: []byte("proof"),
|
||||
TotalWeight: 1000,
|
||||
SignerWeight: 500, // Only 50%, needs 67%
|
||||
}
|
||||
|
||||
err = q.Verify(finality)
|
||||
|
||||
@@ -401,7 +401,7 @@ func TestConcurrentPruningStress(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
const (
|
||||
numWriters = 8
|
||||
numWriters = 8
|
||||
opsPerWriter = 5000
|
||||
)
|
||||
|
||||
|
||||
+45
-45
@@ -61,15 +61,15 @@ import (
|
||||
// 2. t-of-n validators completed Corona threshold (post-quantum secure)
|
||||
|
||||
var (
|
||||
ErrQuasarNotStarted = errors.New("quasar not started")
|
||||
ErrPChainNotConnected = errors.New("P-Chain not connected")
|
||||
ErrQChainNotConnected = errors.New("Q-Chain not connected")
|
||||
ErrCoronaNotConnected = errors.New("Corona coordinator not connected")
|
||||
ErrInsufficientWeight = errors.New("insufficient validator weight")
|
||||
ErrInsufficientSigners = errors.New("insufficient Corona signers")
|
||||
ErrFinalityFailed = errors.New("hybrid finality verification failed")
|
||||
ErrBLSFailed = errors.New("BLS aggregation failed")
|
||||
ErrCoronaFailed = errors.New("Corona threshold signing failed")
|
||||
ErrQuasarNotStarted = errors.New("quasar not started")
|
||||
ErrPChainNotConnected = errors.New("P-Chain not connected")
|
||||
ErrQChainNotConnected = errors.New("Q-Chain not connected")
|
||||
ErrCoronaNotConnected = errors.New("Corona coordinator not connected")
|
||||
ErrInsufficientWeight = errors.New("insufficient validator weight")
|
||||
ErrInsufficientSigners = errors.New("insufficient Corona signers")
|
||||
ErrFinalityFailed = errors.New("hybrid finality verification failed")
|
||||
ErrBLSFailed = errors.New("BLS aggregation failed")
|
||||
ErrCoronaFailed = errors.New("Corona threshold signing failed")
|
||||
)
|
||||
|
||||
// PChainProvider provides P-Chain state and finality events
|
||||
@@ -87,11 +87,11 @@ type QuantumSignerFallback interface {
|
||||
// ValidatorState represents a validator's current state
|
||||
// Each validator has BOTH BLS and Corona keys
|
||||
type ValidatorState struct {
|
||||
NodeID ids.NodeID
|
||||
Weight uint64
|
||||
BLSPubKey []byte // BLS public key for aggregate signatures
|
||||
NodeID ids.NodeID
|
||||
Weight uint64
|
||||
BLSPubKey []byte // BLS public key for aggregate signatures
|
||||
CoronaKey []byte // Corona public key share for threshold sigs
|
||||
Active bool
|
||||
Active bool
|
||||
}
|
||||
|
||||
// FinalityEvent represents a P-Chain finality event
|
||||
@@ -104,18 +104,18 @@ type FinalityEvent struct {
|
||||
|
||||
// QuantumFinality represents a block that achieved hybrid quantum finality
|
||||
type QuantumFinality struct {
|
||||
BlockID ids.ID
|
||||
PChainHeight uint64
|
||||
QChainHeight uint64
|
||||
BLSProof []byte // Aggregated BLS signature (96 bytes)
|
||||
BlockID ids.ID
|
||||
PChainHeight uint64
|
||||
QChainHeight uint64
|
||||
BLSProof []byte // Aggregated BLS signature (96 bytes)
|
||||
CoronaProof []byte // Serialized Corona threshold signature
|
||||
SignerBitset []byte // Which validators signed BLS
|
||||
SignerBitset []byte // Which validators signed BLS
|
||||
CoronaSigners []ids.NodeID // Which validators participated in Corona
|
||||
TotalWeight uint64
|
||||
SignerWeight uint64
|
||||
BLSLatency time.Duration
|
||||
TotalWeight uint64
|
||||
SignerWeight uint64
|
||||
BLSLatency time.Duration
|
||||
CoronaLatency time.Duration
|
||||
Timestamp time.Time
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// Quasar binds P-Chain and Q-Chain consensus into hybrid quantum finality
|
||||
@@ -369,18 +369,18 @@ func (q *Quasar) processFinality(ctx context.Context, event FinalityEvent) error
|
||||
coronaProof = coronaSig.Bytes()
|
||||
}
|
||||
finality := &QuantumFinality{
|
||||
BlockID: event.BlockID,
|
||||
PChainHeight: event.Height,
|
||||
QChainHeight: q.qHeight,
|
||||
BLSProof: blsProof,
|
||||
BlockID: event.BlockID,
|
||||
PChainHeight: event.Height,
|
||||
QChainHeight: q.qHeight,
|
||||
BLSProof: blsProof,
|
||||
CoronaProof: coronaProof,
|
||||
SignerBitset: signerBitset,
|
||||
SignerBitset: signerBitset,
|
||||
CoronaSigners: coronaSigners,
|
||||
TotalWeight: totalWeight,
|
||||
SignerWeight: signerWeight,
|
||||
BLSLatency: blsLatency,
|
||||
TotalWeight: totalWeight,
|
||||
SignerWeight: signerWeight,
|
||||
BLSLatency: blsLatency,
|
||||
CoronaLatency: coronaLatency,
|
||||
Timestamp: time.Now(),
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
q.finalized[event.BlockID] = finality
|
||||
@@ -590,13 +590,13 @@ func (q *Quasar) Stats() QuasarStats {
|
||||
}
|
||||
|
||||
return QuasarStats{
|
||||
PChainHeight: q.pHeight,
|
||||
QChainHeight: q.qHeight,
|
||||
FinalizedBlocks: len(q.finalized),
|
||||
Threshold: q.threshold,
|
||||
QuorumNum: q.quorumNum,
|
||||
QuorumDen: q.quorumDen,
|
||||
Running: q.running,
|
||||
PChainHeight: q.pHeight,
|
||||
QChainHeight: q.qHeight,
|
||||
FinalizedBlocks: len(q.finalized),
|
||||
Threshold: q.threshold,
|
||||
QuorumNum: q.quorumNum,
|
||||
QuorumDen: q.quorumDen,
|
||||
Running: q.running,
|
||||
CoronaParties: coronaStats.NumParties,
|
||||
CoronaThreshold: coronaStats.Threshold,
|
||||
CoronaReady: coronaStats.Initialized,
|
||||
@@ -605,13 +605,13 @@ func (q *Quasar) Stats() QuasarStats {
|
||||
|
||||
// QuasarStats contains quasar statistics
|
||||
type QuasarStats struct {
|
||||
PChainHeight uint64
|
||||
QChainHeight uint64
|
||||
FinalizedBlocks int
|
||||
Threshold int
|
||||
QuorumNum uint64
|
||||
QuorumDen uint64
|
||||
Running bool
|
||||
PChainHeight uint64
|
||||
QChainHeight uint64
|
||||
FinalizedBlocks int
|
||||
Threshold int
|
||||
QuorumNum uint64
|
||||
QuorumDen uint64
|
||||
Running bool
|
||||
CoronaParties int
|
||||
CoronaThreshold int
|
||||
CoronaReady bool
|
||||
|
||||
@@ -181,7 +181,7 @@ func (s *BLSSignature) Signers() []ids.NodeID { return s.signers }
|
||||
|
||||
// QuasarSignature combines BLS and Corona signatures for P/Q security
|
||||
type QuasarSignature struct {
|
||||
bls *BLSSignature
|
||||
bls *BLSSignature
|
||||
corona *CoronaSignature
|
||||
}
|
||||
|
||||
@@ -211,7 +211,7 @@ func (s *QuasarSignature) Signers() []ids.NodeID {
|
||||
return s.bls.Signers()
|
||||
}
|
||||
|
||||
func (s *QuasarSignature) BLS() *BLSSignature { return s.bls }
|
||||
func (s *QuasarSignature) BLS() *BLSSignature { return s.bls }
|
||||
func (s *QuasarSignature) Corona() *CoronaSignature { return s.corona }
|
||||
|
||||
// QuasarSigner combines classical and post-quantum signers
|
||||
|
||||
+2
-2
@@ -7,9 +7,9 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/consensus/utils/set"
|
||||
validators "github.com/luxfi/validators"
|
||||
"github.com/luxfi/constants"
|
||||
"github.com/luxfi/ids"
|
||||
validators "github.com/luxfi/validators"
|
||||
)
|
||||
|
||||
func TestFullValidatorFunctionality(t *testing.T) {
|
||||
@@ -151,7 +151,7 @@ func TestNetworkConfiguration(t *testing.T) {
|
||||
t.Logf("✓ Network configuration verified: ID=%d, Name=%s", networkID, expectedName)
|
||||
}
|
||||
|
||||
func TestUTXOAssetID(t *testing.T) {
|
||||
func TestXAssetID(t *testing.T) {
|
||||
// Verify UTXOAssetID is used for native asset
|
||||
utxoAssetID := ids.Empty // Our implementation uses Empty ID for native asset
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
validators "github.com/luxfi/validators"
|
||||
"github.com/luxfi/constants"
|
||||
"github.com/luxfi/ids"
|
||||
validators "github.com/luxfi/validators"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
# Deploy all 4 app chains (Zoo, Hanzo, SPC, Pars) to mainnet + testnet
|
||||
# Usage: ./deploy-chains.sh [mainnet|testnet|devnet|both]
|
||||
# Deploy all 4 subnet chains (Zoo, Hanzo, SPC, Pars) to mainnet + testnet
|
||||
# Usage: ./deploy-subnets.sh [mainnet|testnet|devnet|both]
|
||||
#
|
||||
# Prerequisites:
|
||||
# - macOS keychain must have mainnet-key-02 key (will prompt for approval)
|
||||
@@ -21,7 +21,7 @@ if [ ! -f "$DEPLOY_BIN" ]; then
|
||||
fi
|
||||
|
||||
# Use mainnet-key-02 (NOT 01!) because 01's funds are all staked by initialStakers
|
||||
# mainnet-key-02 has 70M LUX unlocked on P-chain, sufficient for chain creation
|
||||
# mainnet-key-02 has 70M LUX unlocked on P-chain, sufficient for subnet creation
|
||||
KEY_NAME="mainnet-key-02"
|
||||
echo "Extracting $KEY_NAME from keychain..."
|
||||
echo "(Approve the macOS keychain dialog that appears)"
|
||||
@@ -39,7 +39,7 @@ deploy_network() {
|
||||
local network=$1
|
||||
echo ""
|
||||
echo "=============================="
|
||||
echo "Deploying chains to $network"
|
||||
echo "Deploying subnets to $network"
|
||||
echo "=============================="
|
||||
|
||||
PRIVATE_KEY="$KEY" "$DEPLOY_BIN" \
|
||||
@@ -77,7 +77,7 @@ echo ""
|
||||
echo "All deployments complete!"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Copy the Chain IDs and Blockchain IDs from output above"
|
||||
echo " 1. Copy the Subnet IDs and Blockchain IDs from output above"
|
||||
echo " 2. Update Helm values files:"
|
||||
echo " ~/work/lux/devops/charts/lux/values-mainnet.yaml"
|
||||
echo " ~/work/lux/devops/charts/lux/values-testnet.yaml"
|
||||
@@ -206,7 +206,7 @@ EOF
|
||||
"apricotPhase4BlockTimestamp": 0,
|
||||
"apricotPhase5BlockTimestamp": 0,
|
||||
"durangoBlockTimestamp": 0,
|
||||
"quasarTimestamp": 0,
|
||||
"etnaTimestamp": 0,
|
||||
"feeConfig": {
|
||||
"gasLimit": 20000000,
|
||||
"minBaseFee": 1000000000,
|
||||
|
||||
@@ -237,8 +237,8 @@ Final Validation
|
||||
### Properties
|
||||
|
||||
- **Security**: Quantum-resistant (192-bit)
|
||||
- **Signatures**: ML-DSA-65 (Dilithium) + Corona (LWE-based threshold)
|
||||
- **Threshold scheme**: Corona — native t-of-n in 2 rounds
|
||||
- **Signatures**: ML-DSA-65 (Dilithium)
|
||||
- **Privacy**: Corona ring signatures
|
||||
- **Performance**: ~1,500 TPS
|
||||
|
||||
### Implementation
|
||||
@@ -247,7 +247,7 @@ Final Validation
|
||||
type PQConsensus struct {
|
||||
classicalSigner *bls.Signer
|
||||
quantumSigner *mldsa.Signer
|
||||
coronaSigner *corona.Signer
|
||||
coronaSigner *corona.Signer
|
||||
|
||||
threshold int
|
||||
validatorSet []Validator
|
||||
@@ -279,32 +279,27 @@ func (pq *PQConsensus) VerifyBlock(block Block, sig Signature) bool {
|
||||
}
|
||||
```
|
||||
|
||||
### Corona Threshold Layer
|
||||
|
||||
Corona is a lattice-based threshold signature scheme from LWE
|
||||
([eprint 2024/1113](https://eprint.iacr.org/2024/1113)) implemented in
|
||||
`github.com/luxfi/corona`. Unlike a ring signature, Corona signers are
|
||||
known and weighted; the scheme provides a single aggregated signature
|
||||
that anyone can verify against the group public key.
|
||||
### Corona Privacy Layer
|
||||
|
||||
```go
|
||||
type CoronaSignature struct {
|
||||
Signers []byte // bitset of signing validator indices
|
||||
Signature []byte // aggregated lattice signature
|
||||
Ring []PublicKey
|
||||
Signature []byte
|
||||
KeyImage []byte
|
||||
}
|
||||
|
||||
func (pq *PQConsensus) CoronaSign(
|
||||
func (pq *PQConsensus) CreateRingSignature(
|
||||
message []byte,
|
||||
signerKey *corona.PrivateKey,
|
||||
signers []*Validator,
|
||||
signerKey PrivateKey,
|
||||
ring []PublicKey,
|
||||
) (*CoronaSignature, error) {
|
||||
sig, err := pq.coronaSigner.Sign(message, signerKey, signers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Create anonymous signature within ring
|
||||
sig := pq.coronaSigner.Sign(message, signerKey, ring)
|
||||
|
||||
return &CoronaSignature{
|
||||
Signers: sig.SignerBitset(),
|
||||
Ring: ring,
|
||||
Signature: sig.Bytes(),
|
||||
KeyImage: sig.KeyImage(),
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
|
||||
@@ -25,7 +25,7 @@ Lux implements a 4-chain primary network architecture, each optimized for specif
|
||||
│ (Quantum) │
|
||||
│ │
|
||||
│ Post-Quantum Consensus │
|
||||
│ Corona Threshold Signatures │
|
||||
│ Corona Signatures │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
@@ -76,7 +76,7 @@ Lux implements a 4-chain primary network architecture, each optimized for specif
|
||||
**Purpose:** Post-quantum security layer
|
||||
- Quantum-resistant cryptography
|
||||
- ML-DSA-65 (Dilithium) signatures
|
||||
- Corona (LWE-based) threshold signatures
|
||||
- Corona ring signatures for privacy
|
||||
- Hybrid classical/quantum consensus
|
||||
|
||||
**Key Features:**
|
||||
@@ -301,7 +301,7 @@ Native cross-chain communication protocol:
|
||||
| secp256k1 | ECDSA signatures | 128-bit |
|
||||
| BLS12-381 | Threshold signatures | 128-bit |
|
||||
| ML-DSA-65 | Post-quantum signatures | 192-bit |
|
||||
| Corona | LWE threshold signatures | 192-bit (post-quantum) |
|
||||
| Corona | Ring signatures | 128-bit |
|
||||
| SHA-256 | Hashing | 128-bit |
|
||||
| AES-256-GCM | Encryption | 256-bit |
|
||||
|
||||
|
||||
@@ -310,7 +310,7 @@ Create `~/.luxd/configs/chains/Q/config.json`:
|
||||
"corona-signatures-enabled": true,
|
||||
"post-quantum-algorithm": "ml-dsa-65",
|
||||
"quantum-signature-cache-size": 10000,
|
||||
"corona-threshold-size": 16,
|
||||
"ring-signature-size": 16,
|
||||
"corona-key-size": 1024,
|
||||
"quantum-stamp-enabled": true,
|
||||
"quantum-stamp-window": "30s",
|
||||
|
||||
@@ -226,7 +226,7 @@ Create `~/.luxd/configs/chains/Q/config.json`:
|
||||
"quantum-verification-enabled": true,
|
||||
"corona-signatures-enabled": true,
|
||||
"post-quantum-algorithm": "ml-dsa-65",
|
||||
"corona-threshold-size": 16,
|
||||
"ring-signature-size": 16,
|
||||
"quantum-cache-size": 10000,
|
||||
"parallel-batch-size": 10
|
||||
}
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
"fumadocs-mdx": "^12.0.3",
|
||||
"fumadocs-ui": "^15.8.5",
|
||||
"lucide-react": "^0.468.0",
|
||||
"next": "16.2.6",
|
||||
"next": "16.1.5",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"tailwindcss": "^4.1.16",
|
||||
|
||||
Generated
+74
-83
@@ -10,19 +10,19 @@ importers:
|
||||
dependencies:
|
||||
fumadocs-core:
|
||||
specifier: ^15.8.5
|
||||
version: 15.8.5(@types/react@19.2.7)(lucide-react@0.468.0(react@19.2.0))(next@16.2.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
version: 15.8.5(@types/react@19.2.7)(lucide-react@0.468.0(react@19.2.0))(next@16.1.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
fumadocs-mdx:
|
||||
specifier: ^12.0.3
|
||||
version: 12.0.3(fumadocs-core@15.8.5(@types/react@19.2.7)(lucide-react@0.468.0(react@19.2.0))(next@16.2.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(next@16.2.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)
|
||||
version: 12.0.3(fumadocs-core@15.8.5(@types/react@19.2.7)(lucide-react@0.468.0(react@19.2.0))(next@16.1.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(next@16.1.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)
|
||||
fumadocs-ui:
|
||||
specifier: ^15.8.5
|
||||
version: 15.8.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(lucide-react@0.468.0(react@19.2.0))(next@16.2.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(tailwindcss@4.1.18)
|
||||
version: 15.8.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(lucide-react@0.468.0(react@19.2.0))(next@16.1.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(tailwindcss@4.1.18)
|
||||
lucide-react:
|
||||
specifier: ^0.468.0
|
||||
version: 0.468.0(react@19.2.0)
|
||||
next:
|
||||
specifier: 16.2.6
|
||||
version: 16.2.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
specifier: 16.1.5
|
||||
version: 16.1.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
react:
|
||||
specifier: 19.2.0
|
||||
version: 19.2.0
|
||||
@@ -70,8 +70,8 @@ packages:
|
||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
'@emnapi/runtime@1.10.0':
|
||||
resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==}
|
||||
'@emnapi/runtime@1.8.1':
|
||||
resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==}
|
||||
|
||||
'@esbuild/aix-ppc64@0.25.12':
|
||||
resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
|
||||
@@ -247,8 +247,8 @@ packages:
|
||||
'@formatjs/intl-localematcher@0.6.2':
|
||||
resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==}
|
||||
|
||||
'@img/colour@1.1.0':
|
||||
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
|
||||
'@img/colour@1.0.0':
|
||||
resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@img/sharp-darwin-arm64@0.34.5':
|
||||
@@ -403,53 +403,53 @@ packages:
|
||||
'@mdx-js/mdx@3.1.1':
|
||||
resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==}
|
||||
|
||||
'@next/env@16.2.6':
|
||||
resolution: {integrity: sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==}
|
||||
'@next/env@16.1.5':
|
||||
resolution: {integrity: sha512-CRSCPJiSZoi4Pn69RYBDI9R7YK2g59vLexPQFXY0eyw+ILevIenCywzg+DqmlBik9zszEnw2HLFOUlLAcJbL7g==}
|
||||
|
||||
'@next/swc-darwin-arm64@16.2.6':
|
||||
resolution: {integrity: sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==}
|
||||
'@next/swc-darwin-arm64@16.1.5':
|
||||
resolution: {integrity: sha512-eK7Wdm3Hjy/SCL7TevlH0C9chrpeOYWx2iR7guJDaz4zEQKWcS1IMVfMb9UKBFMg1XgzcPTYPIp1Vcpukkjg6Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-darwin-x64@16.2.6':
|
||||
resolution: {integrity: sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==}
|
||||
'@next/swc-darwin-x64@16.1.5':
|
||||
resolution: {integrity: sha512-foQscSHD1dCuxBmGkbIr6ScAUF6pRoDZP6czajyvmXPAOFNnQUJu2Os1SGELODjKp/ULa4fulnBWoHV3XdPLfA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-linux-arm64-gnu@16.2.6':
|
||||
resolution: {integrity: sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==}
|
||||
'@next/swc-linux-arm64-gnu@16.1.5':
|
||||
resolution: {integrity: sha512-qNIb42o3C02ccIeSeKjacF3HXotGsxh/FMk/rSRmCzOVMtoWH88odn2uZqF8RLsSUWHcAqTgYmPD3pZ03L9ZAA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-arm64-musl@16.2.6':
|
||||
resolution: {integrity: sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==}
|
||||
'@next/swc-linux-arm64-musl@16.1.5':
|
||||
resolution: {integrity: sha512-U+kBxGUY1xMAzDTXmuVMfhaWUZQAwzRaHJ/I6ihtR5SbTVUEaDRiEU9YMjy1obBWpdOBuk1bcm+tsmifYSygfw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-x64-gnu@16.2.6':
|
||||
resolution: {integrity: sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==}
|
||||
'@next/swc-linux-x64-gnu@16.1.5':
|
||||
resolution: {integrity: sha512-gq2UtoCpN7Ke/7tKaU7i/1L7eFLfhMbXjNghSv0MVGF1dmuoaPeEVDvkDuO/9LVa44h5gqpWeJ4mRRznjDv7LA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-x64-musl@16.2.6':
|
||||
resolution: {integrity: sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==}
|
||||
'@next/swc-linux-x64-musl@16.1.5':
|
||||
resolution: {integrity: sha512-bQWSE729PbXT6mMklWLf8dotislPle2L70E9q6iwETYEOt092GDn0c+TTNj26AjmeceSsC4ndyGsK5nKqHYXjQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-win32-arm64-msvc@16.2.6':
|
||||
resolution: {integrity: sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==}
|
||||
'@next/swc-win32-arm64-msvc@16.1.5':
|
||||
resolution: {integrity: sha512-LZli0anutkIllMtTAWZlDqdfvjWX/ch8AFK5WgkNTvaqwlouiD1oHM+WW8RXMiL0+vAkAJyAGEzPPjO+hnrSNQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@next/swc-win32-x64-msvc@16.2.6':
|
||||
resolution: {integrity: sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==}
|
||||
'@next/swc-win32-x64-msvc@16.1.5':
|
||||
resolution: {integrity: sha512-7is37HJTNQGhjPpQbkKjKEboHYQnCgpVt/4rBrrln0D9nderNxZ8ZWs8w1fAtzUx7wEyYjQ+/13myFgFj6K2Ng==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -1002,7 +1002,6 @@ packages:
|
||||
|
||||
'@ungap/structured-clone@1.3.0':
|
||||
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
||||
deprecated: Potential CWE-502 - Update to 1.3.1 or higher
|
||||
|
||||
acorn-jsx@5.3.2:
|
||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||
@@ -1035,9 +1034,8 @@ packages:
|
||||
bail@2.0.2:
|
||||
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
|
||||
|
||||
baseline-browser-mapping@2.10.33:
|
||||
resolution: {integrity: sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
baseline-browser-mapping@2.9.19:
|
||||
resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==}
|
||||
hasBin: true
|
||||
|
||||
browserslist@4.28.1:
|
||||
@@ -1048,8 +1046,8 @@ packages:
|
||||
caniuse-lite@1.0.30001760:
|
||||
resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==}
|
||||
|
||||
caniuse-lite@1.0.30001793:
|
||||
resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==}
|
||||
caniuse-lite@1.0.30001766:
|
||||
resolution: {integrity: sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==}
|
||||
|
||||
ccount@2.0.1:
|
||||
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
||||
@@ -1596,11 +1594,6 @@ packages:
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
nanoid@3.3.12:
|
||||
resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
negotiator@1.0.0:
|
||||
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -1611,8 +1604,8 @@ packages:
|
||||
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
|
||||
next@16.2.6:
|
||||
resolution: {integrity: sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==}
|
||||
next@16.1.5:
|
||||
resolution: {integrity: sha512-f+wE+NSbiQgh3DSAlTaw2FwY5yGdVViAtp8TotNQj4kk4Q8Bh1sC/aL9aH+Rg1YAVn18OYXsRDT7U/079jgP7w==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -1799,8 +1792,8 @@ packages:
|
||||
scroll-into-view-if-needed@3.1.0:
|
||||
resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==}
|
||||
|
||||
semver@7.8.1:
|
||||
resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==}
|
||||
semver@7.7.3:
|
||||
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
@@ -1957,7 +1950,7 @@ snapshots:
|
||||
|
||||
'@alloc/quick-lru@5.2.0': {}
|
||||
|
||||
'@emnapi/runtime@1.10.0':
|
||||
'@emnapi/runtime@1.8.1':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
@@ -2061,7 +2054,7 @@ snapshots:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@img/colour@1.1.0':
|
||||
'@img/colour@1.0.0':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-darwin-arm64@0.34.5':
|
||||
@@ -2146,7 +2139,7 @@ snapshots:
|
||||
|
||||
'@img/sharp-wasm32@0.34.5':
|
||||
dependencies:
|
||||
'@emnapi/runtime': 1.10.0
|
||||
'@emnapi/runtime': 1.8.1
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-arm64@0.34.5':
|
||||
@@ -2207,30 +2200,30 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@next/env@16.2.6': {}
|
||||
'@next/env@16.1.5': {}
|
||||
|
||||
'@next/swc-darwin-arm64@16.2.6':
|
||||
'@next/swc-darwin-arm64@16.1.5':
|
||||
optional: true
|
||||
|
||||
'@next/swc-darwin-x64@16.2.6':
|
||||
'@next/swc-darwin-x64@16.1.5':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-gnu@16.2.6':
|
||||
'@next/swc-linux-arm64-gnu@16.1.5':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-musl@16.2.6':
|
||||
'@next/swc-linux-arm64-musl@16.1.5':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-gnu@16.2.6':
|
||||
'@next/swc-linux-x64-gnu@16.1.5':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-musl@16.2.6':
|
||||
'@next/swc-linux-x64-musl@16.1.5':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-arm64-msvc@16.2.6':
|
||||
'@next/swc-win32-arm64-msvc@16.1.5':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-x64-msvc@16.2.6':
|
||||
'@next/swc-win32-x64-msvc@16.1.5':
|
||||
optional: true
|
||||
|
||||
'@orama/orama@3.1.17': {}
|
||||
@@ -2811,11 +2804,11 @@ snapshots:
|
||||
|
||||
bail@2.0.2: {}
|
||||
|
||||
baseline-browser-mapping@2.10.33: {}
|
||||
baseline-browser-mapping@2.9.19: {}
|
||||
|
||||
browserslist@4.28.1:
|
||||
dependencies:
|
||||
baseline-browser-mapping: 2.10.33
|
||||
baseline-browser-mapping: 2.9.19
|
||||
caniuse-lite: 1.0.30001760
|
||||
electron-to-chromium: 1.5.267
|
||||
node-releases: 2.0.27
|
||||
@@ -2823,7 +2816,7 @@ snapshots:
|
||||
|
||||
caniuse-lite@1.0.30001760: {}
|
||||
|
||||
caniuse-lite@1.0.30001793: {}
|
||||
caniuse-lite@1.0.30001766: {}
|
||||
|
||||
ccount@2.0.1: {}
|
||||
|
||||
@@ -2978,7 +2971,7 @@ snapshots:
|
||||
|
||||
fraction.js@5.3.4: {}
|
||||
|
||||
fumadocs-core@15.8.5(@types/react@19.2.7)(lucide-react@0.468.0(react@19.2.0))(next@16.2.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
fumadocs-core@15.8.5(@types/react@19.2.7)(lucide-react@0.468.0(react@19.2.0))(next@16.1.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
'@formatjs/intl-localematcher': 0.6.2
|
||||
'@orama/orama': 3.1.17
|
||||
@@ -3001,20 +2994,20 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.7
|
||||
lucide-react: 0.468.0(react@19.2.0)
|
||||
next: 16.2.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
next: 16.1.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
fumadocs-mdx@12.0.3(fumadocs-core@15.8.5(@types/react@19.2.7)(lucide-react@0.468.0(react@19.2.0))(next@16.2.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(next@16.2.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0):
|
||||
fumadocs-mdx@12.0.3(fumadocs-core@15.8.5(@types/react@19.2.7)(lucide-react@0.468.0(react@19.2.0))(next@16.1.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(next@16.1.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
'@mdx-js/mdx': 3.1.1
|
||||
'@standard-schema/spec': 1.0.0
|
||||
chokidar: 4.0.3
|
||||
esbuild: 0.25.12
|
||||
estree-util-value-to-estree: 3.5.0
|
||||
fumadocs-core: 15.8.5(@types/react@19.2.7)(lucide-react@0.468.0(react@19.2.0))(next@16.2.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
fumadocs-core: 15.8.5(@types/react@19.2.7)(lucide-react@0.468.0(react@19.2.0))(next@16.1.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
js-yaml: 4.1.1
|
||||
lru-cache: 11.2.4
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
@@ -3027,12 +3020,12 @@ snapshots:
|
||||
unist-util-visit: 5.0.0
|
||||
zod: 4.1.13
|
||||
optionalDependencies:
|
||||
next: 16.2.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
next: 16.1.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
react: 19.2.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
fumadocs-ui@15.8.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(lucide-react@0.468.0(react@19.2.0))(next@16.2.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(tailwindcss@4.1.18):
|
||||
fumadocs-ui@15.8.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(lucide-react@0.468.0(react@19.2.0))(next@16.1.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(tailwindcss@4.1.18):
|
||||
dependencies:
|
||||
'@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
@@ -3045,7 +3038,7 @@ snapshots:
|
||||
'@radix-ui/react-slot': 1.2.4(@types/react@19.2.7)(react@19.2.0)
|
||||
'@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
class-variance-authority: 0.7.1
|
||||
fumadocs-core: 15.8.5(@types/react@19.2.7)(lucide-react@0.468.0(react@19.2.0))(next@16.2.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
fumadocs-core: 15.8.5(@types/react@19.2.7)(lucide-react@0.468.0(react@19.2.0))(next@16.1.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
lodash.merge: 4.6.2
|
||||
next-themes: 0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
postcss-selector-parser: 7.1.1
|
||||
@@ -3056,7 +3049,7 @@ snapshots:
|
||||
tailwind-merge: 3.4.0
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.7
|
||||
next: 16.2.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
next: 16.1.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
tailwindcss: 4.1.18
|
||||
transitivePeerDependencies:
|
||||
- '@mixedbread/sdk'
|
||||
@@ -3693,8 +3686,6 @@ snapshots:
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
nanoid@3.3.12: {}
|
||||
|
||||
negotiator@1.0.0: {}
|
||||
|
||||
next-themes@0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
@@ -3702,25 +3693,25 @@ snapshots:
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
next@16.2.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
next@16.1.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
'@next/env': 16.2.6
|
||||
'@next/env': 16.1.5
|
||||
'@swc/helpers': 0.5.15
|
||||
baseline-browser-mapping: 2.10.33
|
||||
caniuse-lite: 1.0.30001793
|
||||
baseline-browser-mapping: 2.9.19
|
||||
caniuse-lite: 1.0.30001766
|
||||
postcss: 8.4.31
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
styled-jsx: 5.1.6(react@19.2.0)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 16.2.6
|
||||
'@next/swc-darwin-x64': 16.2.6
|
||||
'@next/swc-linux-arm64-gnu': 16.2.6
|
||||
'@next/swc-linux-arm64-musl': 16.2.6
|
||||
'@next/swc-linux-x64-gnu': 16.2.6
|
||||
'@next/swc-linux-x64-musl': 16.2.6
|
||||
'@next/swc-win32-arm64-msvc': 16.2.6
|
||||
'@next/swc-win32-x64-msvc': 16.2.6
|
||||
'@next/swc-darwin-arm64': 16.1.5
|
||||
'@next/swc-darwin-x64': 16.1.5
|
||||
'@next/swc-linux-arm64-gnu': 16.1.5
|
||||
'@next/swc-linux-arm64-musl': 16.1.5
|
||||
'@next/swc-linux-x64-gnu': 16.1.5
|
||||
'@next/swc-linux-x64-musl': 16.1.5
|
||||
'@next/swc-win32-arm64-msvc': 16.1.5
|
||||
'@next/swc-win32-x64-msvc': 16.1.5
|
||||
sharp: 0.34.5
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
@@ -3775,7 +3766,7 @@ snapshots:
|
||||
|
||||
postcss@8.4.31:
|
||||
dependencies:
|
||||
nanoid: 3.3.12
|
||||
nanoid: 3.3.11
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
@@ -3956,14 +3947,14 @@ snapshots:
|
||||
dependencies:
|
||||
compute-scroll-into-view: 3.1.1
|
||||
|
||||
semver@7.8.1:
|
||||
semver@7.7.3:
|
||||
optional: true
|
||||
|
||||
sharp@0.34.5:
|
||||
dependencies:
|
||||
'@img/colour': 1.1.0
|
||||
'@img/colour': 1.0.0
|
||||
detect-libc: 2.1.2
|
||||
semver: 7.8.1
|
||||
semver: 7.7.3
|
||||
optionalDependencies:
|
||||
'@img/sharp-darwin-arm64': 0.34.5
|
||||
'@img/sharp-darwin-x64': 0.34.5
|
||||
|
||||
@@ -1,259 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package errors_test
|
||||
|
||||
import (
|
||||
stderrors "errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
nodeerrors "github.com/luxfi/node/errors"
|
||||
)
|
||||
|
||||
// tempErr is a test helper that implements the optional Temporary() interface
|
||||
// exercised by IsTemporary.
|
||||
type tempErr struct {
|
||||
msg string
|
||||
temp bool
|
||||
}
|
||||
|
||||
func (t *tempErr) Error() string { return t.msg }
|
||||
func (t *tempErr) Temporary() bool { return t.temp }
|
||||
|
||||
func TestWrappedError_ErrorFormat(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
base := stderrors.New("disk gone")
|
||||
|
||||
withMsg := nodeerrors.Wrap(base, nodeerrors.CategoryDatabase, "open failed")
|
||||
require.Equal("[database] open failed: disk gone", withMsg.Error())
|
||||
|
||||
// Empty message — formatter omits the message portion.
|
||||
emptyMsg := nodeerrors.Wrap(base, nodeerrors.CategoryNetwork, "")
|
||||
require.Equal("[network] disk gone", emptyMsg.Error())
|
||||
}
|
||||
|
||||
func TestWrap_NilPassThrough(t *testing.T) {
|
||||
require.Nil(t, nodeerrors.Wrap(nil, nodeerrors.CategoryDatabase, "ignored"))
|
||||
require.Nil(t, nodeerrors.WrapWithContext(nil, nodeerrors.CategoryDatabase, "ignored", map[string]interface{}{"k": "v"}))
|
||||
}
|
||||
|
||||
func TestWrap_AllocatesContextMap(t *testing.T) {
|
||||
wrapped := nodeerrors.Wrap(stderrors.New("x"), nodeerrors.CategoryState, "msg")
|
||||
we := asWrapped(t, wrapped)
|
||||
require.NotNil(t, we.Context)
|
||||
// Should be writable without panic.
|
||||
we.Context["k"] = "v"
|
||||
require.Equal(t, "v", we.Context["k"])
|
||||
}
|
||||
|
||||
func TestWrapWithContext_PreservesContext(t *testing.T) {
|
||||
ctx := map[string]interface{}{"tx_id": "0xabc", "height": uint64(42)}
|
||||
wrapped := nodeerrors.WrapWithContext(nodeerrors.ErrCorrupted, nodeerrors.CategoryDatabase, "block load", ctx)
|
||||
|
||||
we := asWrapped(t, wrapped)
|
||||
require.Equal(t, ctx, we.Context)
|
||||
require.Equal(t, nodeerrors.CategoryDatabase, we.Category)
|
||||
require.Equal(t, "block load", we.Message)
|
||||
}
|
||||
|
||||
func TestWrappedError_UnwrapAndIs(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
wrapped := nodeerrors.Wrap(nodeerrors.ErrNotFound, nodeerrors.CategoryDatabase, "lookup")
|
||||
require.True(stderrors.Is(wrapped, nodeerrors.ErrNotFound))
|
||||
require.False(stderrors.Is(wrapped, nodeerrors.ErrClosed))
|
||||
|
||||
// errors.As must reach the WrappedError.
|
||||
var target *nodeerrors.WrappedError
|
||||
require.True(stderrors.As(wrapped, &target))
|
||||
require.Equal(nodeerrors.ErrNotFound, target.Unwrap())
|
||||
}
|
||||
|
||||
func TestWrappedError_DoubleWrapChain(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
inner := nodeerrors.Wrap(nodeerrors.ErrTimeout, nodeerrors.CategoryNetwork, "dial")
|
||||
outer := nodeerrors.Wrap(inner, nodeerrors.CategoryNetwork, "rpc")
|
||||
|
||||
// Sentinel survives through both wrap layers via Unwrap chain.
|
||||
require.True(stderrors.Is(outer, nodeerrors.ErrTimeout))
|
||||
require.True(nodeerrors.IsTimeout(outer))
|
||||
}
|
||||
|
||||
func TestIsNotFound(t *testing.T) {
|
||||
require := require.New(t)
|
||||
require.True(nodeerrors.IsNotFound(nodeerrors.ErrNotFound))
|
||||
require.True(nodeerrors.IsNotFound(fmt.Errorf("lookup: %w", nodeerrors.ErrNotFound)))
|
||||
require.False(nodeerrors.IsNotFound(nodeerrors.ErrClosed))
|
||||
require.False(nodeerrors.IsNotFound(nil))
|
||||
}
|
||||
|
||||
func TestIsClosed(t *testing.T) {
|
||||
require := require.New(t)
|
||||
require.True(nodeerrors.IsClosed(nodeerrors.ErrClosed))
|
||||
require.True(nodeerrors.IsClosed(fmt.Errorf("shutdown: %w", nodeerrors.ErrClosed)))
|
||||
require.False(nodeerrors.IsClosed(nodeerrors.ErrTimeout))
|
||||
require.False(nodeerrors.IsClosed(nil))
|
||||
}
|
||||
|
||||
func TestIsTimeout(t *testing.T) {
|
||||
require := require.New(t)
|
||||
require.True(nodeerrors.IsTimeout(nodeerrors.ErrTimeout))
|
||||
require.True(nodeerrors.IsTimeout(fmt.Errorf("connect: %w", nodeerrors.ErrTimeout)))
|
||||
require.False(nodeerrors.IsTimeout(nodeerrors.ErrNotFound))
|
||||
require.False(nodeerrors.IsTimeout(nil))
|
||||
}
|
||||
|
||||
func TestIsTemporary_SentinelErrors(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
require.True(nodeerrors.IsTemporary(nodeerrors.ErrTimeout))
|
||||
require.True(nodeerrors.IsTemporary(nodeerrors.ErrRateLimited))
|
||||
require.True(nodeerrors.IsTemporary(nodeerrors.ErrResourceExhausted))
|
||||
|
||||
// Permanent / unrelated sentinels are not temporary.
|
||||
require.False(nodeerrors.IsTemporary(nodeerrors.ErrNotSupported))
|
||||
require.False(nodeerrors.IsTemporary(nodeerrors.ErrInvalidInput))
|
||||
require.False(nodeerrors.IsTemporary(stderrors.New("random")))
|
||||
}
|
||||
|
||||
func TestIsTemporary_TemporaryInterface(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
require.True(nodeerrors.IsTemporary(&tempErr{msg: "blip", temp: true}))
|
||||
require.False(nodeerrors.IsTemporary(&tempErr{msg: "fatal", temp: false}))
|
||||
}
|
||||
|
||||
func TestIsPermanent(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
permanent := []error{
|
||||
nodeerrors.ErrNotSupported,
|
||||
nodeerrors.ErrDeprecated,
|
||||
nodeerrors.ErrInvalidInput,
|
||||
nodeerrors.ErrInvalidSignature,
|
||||
nodeerrors.ErrInvalidFormat,
|
||||
nodeerrors.ErrForbidden,
|
||||
nodeerrors.ErrUnauthorized,
|
||||
}
|
||||
for _, err := range permanent {
|
||||
require.Truef(nodeerrors.IsPermanent(err), "expected permanent: %v", err)
|
||||
}
|
||||
|
||||
require.False(nodeerrors.IsPermanent(nodeerrors.ErrTimeout))
|
||||
require.False(nodeerrors.IsPermanent(nodeerrors.ErrRateLimited))
|
||||
require.False(nodeerrors.IsPermanent(nil))
|
||||
|
||||
// fmt.Errorf %w chain still resolves to permanent.
|
||||
require.True(nodeerrors.IsPermanent(fmt.Errorf("validate: %w", nodeerrors.ErrInvalidSignature)))
|
||||
}
|
||||
|
||||
func TestGetCategory_FromWrappedError(t *testing.T) {
|
||||
require := require.New(t)
|
||||
wrapped := nodeerrors.Wrap(stderrors.New("x"), nodeerrors.CategoryResource, "oom")
|
||||
require.Equal(nodeerrors.CategoryResource, nodeerrors.GetCategory(wrapped))
|
||||
}
|
||||
|
||||
func TestGetCategory_InferredFromSentinel(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
cases := []struct {
|
||||
err error
|
||||
want nodeerrors.Category
|
||||
}{
|
||||
{nodeerrors.ErrNotFound, nodeerrors.CategoryDatabase},
|
||||
{nodeerrors.ErrClosed, nodeerrors.CategoryDatabase},
|
||||
{nodeerrors.ErrTimeout, nodeerrors.CategoryNetwork},
|
||||
{nodeerrors.ErrConnectionClosed, nodeerrors.CategoryNetwork},
|
||||
{nodeerrors.ErrInvalidInput, nodeerrors.CategoryValidation},
|
||||
{nodeerrors.ErrInvalidSignature, nodeerrors.CategoryValidation},
|
||||
{nodeerrors.ErrNotInitialized, nodeerrors.CategoryState},
|
||||
{nodeerrors.ErrAlreadyExists, nodeerrors.CategoryState},
|
||||
{nodeerrors.ErrResourceExhausted, nodeerrors.CategoryResource},
|
||||
{nodeerrors.ErrOutOfMemory, nodeerrors.CategoryResource},
|
||||
{nodeerrors.ErrUnauthorized, nodeerrors.CategoryPermission},
|
||||
{nodeerrors.ErrForbidden, nodeerrors.CategoryPermission},
|
||||
{stderrors.New("mystery"), nodeerrors.CategoryUnknown},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
require.Equalf(tc.want, nodeerrors.GetCategory(tc.err),
|
||||
"category for %v", tc.err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCategory_WrappedTakesPrecedenceOverInference(t *testing.T) {
|
||||
// ErrNotFound would normally infer CategoryDatabase. If wrapped with a
|
||||
// different explicit category, the wrapped value must win.
|
||||
wrapped := nodeerrors.Wrap(nodeerrors.ErrNotFound, nodeerrors.CategoryInternal, "ctx")
|
||||
require.Equal(t, nodeerrors.CategoryInternal, nodeerrors.GetCategory(wrapped))
|
||||
}
|
||||
|
||||
func TestMulti_EmptyError(t *testing.T) {
|
||||
m := &nodeerrors.Multi{}
|
||||
require.Equal(t, "no errors", m.Error())
|
||||
require.Nil(t, m.Err())
|
||||
}
|
||||
|
||||
func TestMulti_SingleErrorUnwrapsToInnerString(t *testing.T) {
|
||||
m := &nodeerrors.Multi{}
|
||||
m.Add(stderrors.New("only one"))
|
||||
require.Equal(t, "only one", m.Error())
|
||||
require.Equal(t, m, m.Err())
|
||||
}
|
||||
|
||||
func TestMulti_MultipleErrorsContainsEach(t *testing.T) {
|
||||
m := &nodeerrors.Multi{}
|
||||
m.Add(stderrors.New("first"))
|
||||
m.Add(stderrors.New("second"))
|
||||
m.Add(stderrors.New("third"))
|
||||
|
||||
s := m.Error()
|
||||
require.True(t, strings.Contains(s, "multiple errors"), "got: %s", s)
|
||||
require.True(t, strings.Contains(s, "first"))
|
||||
require.True(t, strings.Contains(s, "second"))
|
||||
require.True(t, strings.Contains(s, "third"))
|
||||
}
|
||||
|
||||
func TestMulti_AddIgnoresNil(t *testing.T) {
|
||||
m := &nodeerrors.Multi{}
|
||||
m.Add(nil)
|
||||
m.Add(nil)
|
||||
require.Empty(t, m.Errors)
|
||||
require.Nil(t, m.Err())
|
||||
}
|
||||
|
||||
func TestJoin_AllNilReturnsNil(t *testing.T) {
|
||||
require.Nil(t, nodeerrors.Join())
|
||||
require.Nil(t, nodeerrors.Join(nil, nil, nil))
|
||||
}
|
||||
|
||||
func TestJoin_DropsNilEntries(t *testing.T) {
|
||||
err := nodeerrors.Join(nil, stderrors.New("real"), nil)
|
||||
require.NotNil(t, err)
|
||||
require.Equal(t, "real", err.Error())
|
||||
}
|
||||
|
||||
func TestJoin_CombinesMultipleErrors(t *testing.T) {
|
||||
err := nodeerrors.Join(
|
||||
nodeerrors.ErrNotFound,
|
||||
nodeerrors.ErrClosed,
|
||||
)
|
||||
require.NotNil(t, err)
|
||||
s := err.Error()
|
||||
require.True(t, strings.Contains(s, "multiple errors"), "got: %s", s)
|
||||
require.True(t, strings.Contains(s, "not found"))
|
||||
require.True(t, strings.Contains(s, "closed"))
|
||||
}
|
||||
|
||||
// asWrapped extracts a *nodeerrors.WrappedError or fails the test.
|
||||
func asWrapped(t *testing.T, err error) *nodeerrors.WrappedError {
|
||||
t.Helper()
|
||||
var w *nodeerrors.WrappedError
|
||||
require.True(t, stderrors.As(err, &w), "expected *WrappedError, got %T", err)
|
||||
return w
|
||||
}
|
||||
@@ -55,9 +55,9 @@ func NewMultiNetworkNode() *MultiNetworkNode {
|
||||
ChainID: "2JVSBoinj9C2J33VntvzYtVJNZdN2NKiwwKjcumHUWEb5DbBrm",
|
||||
Active: true,
|
||||
},
|
||||
200200: { // Zoo chain Chain ID
|
||||
200200: { // Zoo L2 Chain ID
|
||||
NetworkID: 200200,
|
||||
NetworkName: "Zoo Network",
|
||||
NetworkName: "Zoo Network (L2)",
|
||||
RPCPort: 2000,
|
||||
Validators: 5,
|
||||
ChainID: "2ebCneCbwthjQ1rYT41nhd7M76Hc6YmosMAQrTFhBq8qeqh6tt",
|
||||
|
||||
@@ -8,9 +8,9 @@ package builder
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/go-json-experiment/json"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
@@ -23,11 +23,11 @@ import (
|
||||
"github.com/luxfi/net/endpoints"
|
||||
"github.com/luxfi/node/vms/components/gas"
|
||||
"github.com/luxfi/node/vms/platformvm/genesis"
|
||||
xvmgenesis "github.com/luxfi/node/vms/xvm/genesis"
|
||||
"github.com/luxfi/node/vms/platformvm/reward"
|
||||
"github.com/luxfi/node/vms/platformvm/signer"
|
||||
pchaintxs "github.com/luxfi/node/vms/platformvm/txs"
|
||||
"github.com/luxfi/node/vms/platformvm/validators/fee"
|
||||
xvmgenesis "github.com/luxfi/node/vms/xvm/genesis"
|
||||
"github.com/luxfi/utxo/bls12381fx"
|
||||
"github.com/luxfi/utxo/ed25519fx"
|
||||
"github.com/luxfi/utxo/mldsafx"
|
||||
@@ -38,8 +38,8 @@ import (
|
||||
"github.com/luxfi/utxo/secp256r1fx"
|
||||
"github.com/luxfi/utxo/slhdsafx"
|
||||
|
||||
genesisconfigs "github.com/luxfi/genesis/configs"
|
||||
genesiscfg "github.com/luxfi/genesis/pkg/genesis"
|
||||
genesisconfigs "github.com/luxfi/genesis/configs"
|
||||
)
|
||||
|
||||
// Chain alias vars are derived from the single source of truth (Registry
|
||||
@@ -58,9 +58,6 @@ var (
|
||||
ZChainAliases = AliasesFor("Z")
|
||||
GChainAliases = AliasesFor("G")
|
||||
KChainAliases = AliasesFor("K")
|
||||
IChainAliases = AliasesFor("I")
|
||||
OChainAliases = AliasesFor("O")
|
||||
RChainAliases = AliasesFor("R")
|
||||
|
||||
// Network-specific genesis messages (Latin for mainnet, descriptive for others)
|
||||
// Mainnet: "Lux et Libertas" - Light and Liberty
|
||||
@@ -607,12 +604,6 @@ func FromConfig(config *genesiscfg.Config) ([]byte, ids.ID, error) {
|
||||
{[]byte(config.TChainGenesis), constants.ThresholdVMID, "T-Chain", nil},
|
||||
{[]byte(config.QChainGenesis), constants.QuantumVMID, "Q-Chain", nil},
|
||||
{[]byte(config.ZChainGenesis), constants.ZKVMID, "Z-Chain", nil},
|
||||
// A/G/K carry genesis blobs and are deterministic genesis chains
|
||||
// (per the no-CreateChainTx model). Appended AFTER Z so the existing
|
||||
// X→C→D→B→T→Q→Z blockchain IDs are preserved; A/G/K take fresh tail IDs.
|
||||
{[]byte(config.AChainGenesis), constants.AIVMID, "A-Chain", nil},
|
||||
{[]byte(config.GChainGenesis), constants.GraphVMID, "G-Chain", nil},
|
||||
{[]byte(config.KChainGenesis), constants.KeyVMID, "K-Chain", nil},
|
||||
}
|
||||
chains := []genesis.Chain{}
|
||||
for _, c := range optIn {
|
||||
@@ -628,8 +619,8 @@ func FromConfig(config *genesiscfg.Config) ([]byte, ids.ID, error) {
|
||||
})
|
||||
}
|
||||
|
||||
// I/O/R chains carry no genesis blob; they load as plugins and are
|
||||
// instantiated via CreateChainTx post-genesis on their own chains.
|
||||
// G/K/A/I chains are loaded as plugins and instantiated via
|
||||
// CreateChainTx post-genesis on their own chains.
|
||||
|
||||
pChainGenesis, err := genesis.New(
|
||||
utxoAssetID,
|
||||
@@ -691,7 +682,7 @@ func FromDatabase(networkID uint32, dbPath string, dbType string, stakingCfg *St
|
||||
return FromConfig(config)
|
||||
}
|
||||
|
||||
// UTXOAssetIDFromGenesisBytes returns the X-Chain native asset ID encoded
|
||||
// XAssetIDFromGenesisBytes returns the X-Chain native asset ID encoded
|
||||
// in a platform-genesis blob. It parses the platform genesis, finds the
|
||||
// X-Chain CreateChainTx (vmID == constants.XVMID), then decodes that
|
||||
// chain's embedded XVM genesis bytes to recover the runtime asset ID
|
||||
@@ -712,7 +703,7 @@ func FromDatabase(networkID uint32, dbPath string, dbType string, stakingCfg *St
|
||||
// helper through getGenesisData means the node always reports the
|
||||
// genesis-derived asset ID via platform.getStakingAssetID regardless
|
||||
// of which load path was taken.
|
||||
func UTXOAssetIDFromGenesisBytes(genesisBytes []byte) (ids.ID, bool, error) {
|
||||
func XAssetIDFromGenesisBytes(genesisBytes []byte) (ids.ID, bool, error) {
|
||||
// Parse the platform genesis directly so we can distinguish parse
|
||||
// failure ("garbage bytes — fatal") from missing X-Chain ("P-only
|
||||
// mode — caller falls back to UTXOAssetIDFor"). VMGenesis collapses
|
||||
@@ -870,36 +861,6 @@ func Aliases(genesisBytes []byte) (map[string][]string, map[ids.ID][]string, err
|
||||
path.Join(constants.ChainAliasPrefix, "kms"),
|
||||
}
|
||||
chainAliases[chainID] = KChainAliases
|
||||
case constants.IdentityVMID:
|
||||
apiAliases[endpoint] = []string{
|
||||
"I",
|
||||
"identity",
|
||||
"identityvm",
|
||||
"id",
|
||||
path.Join(constants.ChainAliasPrefix, "I"),
|
||||
path.Join(constants.ChainAliasPrefix, "identity"),
|
||||
}
|
||||
chainAliases[chainID] = IChainAliases
|
||||
case constants.OracleVMID:
|
||||
apiAliases[endpoint] = []string{
|
||||
"O",
|
||||
"oracle",
|
||||
"oraclevm",
|
||||
"feed",
|
||||
path.Join(constants.ChainAliasPrefix, "O"),
|
||||
path.Join(constants.ChainAliasPrefix, "oracle"),
|
||||
}
|
||||
chainAliases[chainID] = OChainAliases
|
||||
case constants.RelayVMID:
|
||||
apiAliases[endpoint] = []string{
|
||||
"R",
|
||||
"relay",
|
||||
"relayvm",
|
||||
"msg",
|
||||
path.Join(constants.ChainAliasPrefix, "R"),
|
||||
path.Join(constants.ChainAliasPrefix, "relay"),
|
||||
}
|
||||
chainAliases[chainID] = RChainAliases
|
||||
}
|
||||
}
|
||||
return apiAliases, chainAliases, nil
|
||||
|
||||
@@ -14,17 +14,17 @@ import (
|
||||
pchaintxs "github.com/luxfi/node/vms/platformvm/txs"
|
||||
)
|
||||
|
||||
// TestUTXOAssetIDFromGenesisBytes_Sovereign asserts the canonical
|
||||
// TestXAssetIDFromGenesisBytes_Sovereign asserts the canonical
|
||||
// behaviour the sovereign-L1 fix relies on: when the platform genesis
|
||||
// bakes an X-Chain, the X-Chain asset ID returned by
|
||||
// UTXOAssetIDFromGenesisBytes is the runtime ID encoded IN the genesis
|
||||
// XAssetIDFromGenesisBytes is the runtime ID encoded IN the genesis
|
||||
// (different across networks with different validator/holder sets),
|
||||
// NOT the network-id-keyed constants.UTXOAssetIDFor(networkID) value.
|
||||
//
|
||||
// The two values disagreeing is the whole reason the helper exists —
|
||||
// sovereign primary networks sharing a networkID would otherwise
|
||||
// silently collide on the constant.
|
||||
func TestUTXOAssetIDFromGenesisBytes_Sovereign(t *testing.T) {
|
||||
func TestXAssetIDFromGenesisBytes_Sovereign(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
// Use the local devnet config — it has an X-Chain genesis baked in,
|
||||
@@ -38,7 +38,7 @@ func TestUTXOAssetIDFromGenesisBytes_Sovereign(t *testing.T) {
|
||||
require.NotEmpty(genesisBytes)
|
||||
require.NotEqual(ids.Empty, fromConfigID)
|
||||
|
||||
helperID, ok, err := UTXOAssetIDFromGenesisBytes(genesisBytes)
|
||||
helperID, ok, err := XAssetIDFromGenesisBytes(genesisBytes)
|
||||
require.NoError(err)
|
||||
require.True(ok, "X-Chain is in genesis — helper must report ok=true")
|
||||
require.Equal(fromConfigID, helperID, "helper must agree with FromConfig")
|
||||
@@ -54,31 +54,31 @@ func TestUTXOAssetIDFromGenesisBytes_Sovereign(t *testing.T) {
|
||||
// assert the value is non-zero and matches FromConfig's return.
|
||||
}
|
||||
|
||||
// TestUTXOAssetIDFromGenesisBytes_POnly verifies that when the platform
|
||||
// TestXAssetIDFromGenesisBytes_POnly verifies that when the platform
|
||||
// genesis has no X-Chain (P-only mode), the helper returns ok=false
|
||||
// and ids.Empty rather than an error. Callers fall back to
|
||||
// constants.UTXOAssetIDFor(networkID) in that case (the value is
|
||||
// unused at runtime since there is no X-Chain to mint on).
|
||||
func TestUTXOAssetIDFromGenesisBytes_POnly(t *testing.T) {
|
||||
func TestXAssetIDFromGenesisBytes_POnly(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
pOnly := &genesis.Genesis{Chains: nil}
|
||||
pOnlyBytes, err := genesis.Codec.Marshal(pchaintxs.CodecVersion, pOnly)
|
||||
require.NoError(err)
|
||||
|
||||
id, ok, err := UTXOAssetIDFromGenesisBytes(pOnlyBytes)
|
||||
id, ok, err := XAssetIDFromGenesisBytes(pOnlyBytes)
|
||||
require.NoError(err, "P-only is a valid mode, not a parse error")
|
||||
require.False(ok, "P-only must report ok=false so caller falls back")
|
||||
require.Equal(ids.Empty, id)
|
||||
}
|
||||
|
||||
// TestUTXOAssetIDFromGenesisBytes_Malformed asserts that bad input
|
||||
// TestXAssetIDFromGenesisBytes_Malformed asserts that bad input
|
||||
// surfaces an error — silently returning ids.Empty would reintroduce
|
||||
// the UTXOAssetIDFor(networkID) fallback path and defeat the fix.
|
||||
func TestUTXOAssetIDFromGenesisBytes_Malformed(t *testing.T) {
|
||||
func TestXAssetIDFromGenesisBytes_Malformed(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
_, _, err := UTXOAssetIDFromGenesisBytes([]byte{0xde, 0xad})
|
||||
_, _, err := XAssetIDFromGenesisBytes([]byte{0xde, 0xad})
|
||||
require.Error(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -160,10 +160,10 @@ func TestGetConfig(t *testing.T) {
|
||||
|
||||
func TestGetConfigAllocations(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
networkID uint32
|
||||
minAllocs int
|
||||
minStakers int
|
||||
name string
|
||||
networkID uint32
|
||||
minAllocs int
|
||||
minStakers int
|
||||
}{
|
||||
{"Mainnet", constants.MainnetID, 50, 1},
|
||||
{"Testnet", constants.TestnetID, 50, 1},
|
||||
|
||||
@@ -66,9 +66,6 @@ var Registry = []ChainSpec{
|
||||
{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"},
|
||||
{Letter: "I", VMID: constants.IdentityVMID, Aliases: []string{"identity", "identityvm", "id"}, Name: "I-Chain"},
|
||||
{Letter: "O", VMID: constants.OracleVMID, Aliases: []string{"oracle", "oraclevm", "feed"}, Name: "O-Chain"},
|
||||
{Letter: "R", VMID: constants.RelayVMID, Aliases: []string{"relay", "relayvm", "msg"}, Name: "R-Chain"},
|
||||
}
|
||||
|
||||
// specsByLetter is an O(1) lookup built once at package init.
|
||||
@@ -129,3 +126,4 @@ func VMAliasesMap() map[ids.ID][]string {
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
|
||||
@@ -45,9 +45,6 @@ func TestChainAliasesRegistryParity(t *testing.T) {
|
||||
{"Z", ZChainAliases, []string{"Z", "zk", "zkvm"}},
|
||||
{"G", GChainAliases, []string{"G", "graph", "graphvm", "dgraph"}},
|
||||
{"K", KChainAliases, []string{"K", "key", "keyvm"}},
|
||||
{"I", IChainAliases, []string{"I", "identity", "identityvm", "id"}},
|
||||
{"O", OChainAliases, []string{"O", "oracle", "oraclevm", "feed"}},
|
||||
{"R", RChainAliases, []string{"R", "relay", "relayvm", "msg"}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -76,9 +73,6 @@ func TestVMAliasesRegistryParity(t *testing.T) {
|
||||
constants.ZKVMID: {"zkvm", "zk"},
|
||||
constants.GraphVMID: {"graphvm", "graph", "dgraph"},
|
||||
constants.KeyVMID: {"keyvm", "key"},
|
||||
constants.IdentityVMID: {"identity", "identityvm", "id"},
|
||||
constants.OracleVMID: {"oracle", "oraclevm", "feed"},
|
||||
constants.RelayVMID: {"relay", "relayvm", "msg"},
|
||||
secp256k1fx.ID: {"secp256k1fx"},
|
||||
nftfx.ID: {"nftfx"},
|
||||
propertyfx.ID: {"propertyfx"},
|
||||
|
||||
@@ -7,7 +7,7 @@ module github.com/luxfi/node
|
||||
//
|
||||
// - If updating between minor versions (e.g. 1.23.x -> 1.24.x):
|
||||
// - Consider updating the version of golangci-lint (in scripts/lint.sh).
|
||||
go 1.26.4
|
||||
go 1.26.3
|
||||
|
||||
exclude github.com/luxfi/geth v1.16.1
|
||||
|
||||
@@ -26,16 +26,16 @@ 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.25.31
|
||||
github.com/luxfi/crypto v1.19.26
|
||||
github.com/luxfi/database v1.20.4
|
||||
github.com/luxfi/ids v1.2.15
|
||||
github.com/luxfi/consensus v1.25.2
|
||||
github.com/luxfi/crypto v1.19.15
|
||||
github.com/luxfi/database v1.18.3
|
||||
github.com/luxfi/ids v1.2.10
|
||||
github.com/luxfi/keychain v1.0.2
|
||||
github.com/luxfi/log v1.4.3
|
||||
github.com/luxfi/log v1.4.1
|
||||
github.com/luxfi/math v1.4.1
|
||||
github.com/luxfi/metric v1.5.9
|
||||
github.com/luxfi/metric v1.5.5
|
||||
github.com/luxfi/mock v0.1.1
|
||||
github.com/mr-tron/base58 v1.3.0
|
||||
github.com/mr-tron/base58 v1.2.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
|
||||
@@ -49,22 +49,26 @@ require (
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/supranational/blst v0.3.16 // indirect
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a // indirect
|
||||
github.com/thepudds/fzgen v0.4.3
|
||||
go.opentelemetry.io/otel v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.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/exp v0.0.0-20260529124908-c761662dc8c9 // indirect
|
||||
golang.org/x/mod v0.36.0
|
||||
golang.org/x/net v0.55.0
|
||||
golang.org/x/crypto v0.50.0
|
||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect
|
||||
golang.org/x/mod v0.34.0
|
||||
golang.org/x/net v0.52.0
|
||||
golang.org/x/sync v0.20.0
|
||||
golang.org/x/time v0.15.0
|
||||
golang.org/x/tools v0.45.0
|
||||
golang.org/x/tools v0.43.0
|
||||
gonum.org/v1/gonum v0.17.0
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||
google.golang.org/grpc v1.80.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -78,7 +82,7 @@ require (
|
||||
github.com/cockroachdb/redact v1.1.8 // indirect
|
||||
github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/deckarep/golang-set/v2 v2.9.0 // indirect
|
||||
github.com/deckarep/golang-set/v2 v2.8.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
|
||||
@@ -88,13 +92,14 @@ require (
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
||||
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.18.5
|
||||
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
|
||||
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
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
|
||||
@@ -102,52 +107,50 @@ require (
|
||||
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
|
||||
github.com/tklauser/go-sysconf v0.4.0 // indirect
|
||||
github.com/tklauser/numcpus v0.12.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.10.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.43.0 // indirect
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cloudflare/circl v1.6.3
|
||||
github.com/consensys/gnark-crypto v0.20.1
|
||||
github.com/go-json-experiment/json v0.0.0-20260601182631-00ed12fed2a6
|
||||
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/accel v1.1.4
|
||||
github.com/luxfi/api v1.0.11
|
||||
github.com/luxfi/atomic v1.0.0
|
||||
github.com/luxfi/chains v1.3.21
|
||||
github.com/luxfi/codec v1.1.5
|
||||
github.com/luxfi/chains v1.2.3
|
||||
github.com/luxfi/compress v0.0.5
|
||||
github.com/luxfi/constants v1.5.8
|
||||
github.com/luxfi/constants v1.5.7
|
||||
github.com/luxfi/container v0.0.4
|
||||
github.com/luxfi/filesystem v0.0.1
|
||||
github.com/luxfi/genesis v1.13.16
|
||||
github.com/luxfi/genesis/pkg/genesis/security v1.13.8
|
||||
github.com/luxfi/geth v1.17.12
|
||||
github.com/luxfi/genesis v1.12.19
|
||||
github.com/luxfi/genesis/pkg/genesis/security v1.12.19
|
||||
github.com/luxfi/geth v1.16.98
|
||||
github.com/luxfi/go-bip39 v1.1.2
|
||||
github.com/luxfi/keys v1.2.0
|
||||
github.com/luxfi/lattice/v7 v7.1.4
|
||||
github.com/luxfi/lattice/v7 v7.1.0
|
||||
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/net v0.0.4
|
||||
github.com/luxfi/p2p v1.19.2
|
||||
github.com/luxfi/resource v0.0.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/sys v0.1.0
|
||||
github.com/luxfi/rpc v1.0.2
|
||||
github.com/luxfi/runtime v1.1.0
|
||||
github.com/luxfi/sdk v1.16.60
|
||||
github.com/luxfi/sys v0.0.0-20260110090042-50187ec5ffd8
|
||||
github.com/luxfi/timer v1.0.2
|
||||
github.com/luxfi/units v1.0.0
|
||||
github.com/luxfi/utils v1.2.0
|
||||
github.com/luxfi/utxo v0.3.7
|
||||
github.com/luxfi/utils v1.1.4
|
||||
github.com/luxfi/utxo v0.3.2
|
||||
github.com/luxfi/validators v1.2.0
|
||||
github.com/luxfi/vm v1.2.5
|
||||
github.com/luxfi/warp v1.19.5
|
||||
github.com/luxfi/zap v0.8.10
|
||||
github.com/luxfi/vm v1.0.40
|
||||
github.com/luxfi/warp v1.18.6
|
||||
github.com/luxfi/zwing v0.5.2
|
||||
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354
|
||||
go.uber.org/zap v1.27.1
|
||||
@@ -156,66 +159,42 @@ require (
|
||||
require (
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
filippo.io/hpke v0.4.0 // 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/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/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/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/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/cenkalti/backoff v2.2.1+incompatible // 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
|
||||
github.com/grandcat/zeroconf v1.0.0 // indirect
|
||||
github.com/gtank/merlin v0.1.1 // indirect
|
||||
github.com/gtank/ristretto255 v0.2.0 // indirect
|
||||
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/corona v0.7.9 // indirect
|
||||
github.com/luxfi/ai v0.1.0 // indirect
|
||||
github.com/luxfi/corona v0.7.5 // indirect
|
||||
github.com/luxfi/crypto/ipa v1.2.4 // indirect
|
||||
github.com/luxfi/kms v1.11.7 // indirect
|
||||
github.com/luxfi/lens v0.1.4 // indirect
|
||||
github.com/luxfi/magnetar v1.2.3 // indirect
|
||||
github.com/luxfi/mdns v0.1.1 // indirect
|
||||
github.com/luxfi/oracle v0.1.1-0.20260429020431-76258cfcddf9 // indirect
|
||||
github.com/luxfi/pq v1.0.3 // indirect
|
||||
github.com/luxfi/precompile v0.5.59 // indirect
|
||||
github.com/luxfi/pulsar v1.1.5 // indirect
|
||||
github.com/luxfi/staking v1.5.0 // indirect
|
||||
github.com/luxfi/threshold v1.9.9 // indirect
|
||||
github.com/luxfi/trace v1.1.0 // indirect
|
||||
github.com/luxfi/zapcodec v1.0.1 // indirect
|
||||
github.com/luxfi/zapdb v1.10.1 // indirect
|
||||
github.com/miekg/dns v1.1.72 // indirect
|
||||
github.com/luxfi/protocol v0.0.4 // indirect
|
||||
github.com/luxfi/pulsar v1.0.12 // indirect
|
||||
github.com/luxfi/relay v0.0.0-20260429020048-c629fe160d3c // indirect
|
||||
github.com/luxfi/staking v1.1.0 // indirect
|
||||
github.com/luxfi/threshold v1.8.5 // indirect
|
||||
github.com/luxfi/trace v0.1.4 // indirect
|
||||
github.com/luxfi/zapdb v1.10.0 // indirect
|
||||
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b // indirect
|
||||
github.com/minio/crc64nvme v1.1.1 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/minio/minio-go/v7 v7.0.100 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/tinylib/msgp v1.6.4 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.9 // indirect
|
||||
github.com/tinylib/msgp v1.6.1 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 // 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/proto v1.0.2
|
||||
github.com/luxfi/upgrade v1.0.0 // indirect
|
||||
github.com/luxfi/version v1.0.1
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
)
|
||||
@@ -223,13 +202,14 @@ require (
|
||||
require (
|
||||
github.com/ALTree/bigfloat v0.2.0 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0 // indirect
|
||||
github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect
|
||||
github.com/cronokirby/saferith v0.33.0 // indirect
|
||||
github.com/dgraph-io/ristretto/v2 v2.4.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/ethereum/c-kzg-4844/v2 v2.1.7 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.1 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/google/flatbuffers v25.12.19+incompatible // indirect
|
||||
@@ -238,13 +218,14 @@ require (
|
||||
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/codec v1.1.4
|
||||
github.com/luxfi/formatting v1.0.1
|
||||
github.com/luxfi/go-bip32 v1.0.2
|
||||
github.com/luxfi/math/big v0.1.0 // indirect
|
||||
github.com/luxfi/sampler v1.1.0 // indirect
|
||||
github.com/luxfi/sampler v1.0.0 // indirect
|
||||
github.com/luxfi/tls v1.0.3 // indirect
|
||||
github.com/mattn/go-colorable v0.1.15 // indirect
|
||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/montanaflynn/stats v0.9.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
@@ -256,3 +237,5 @@ require (
|
||||
)
|
||||
|
||||
exclude github.com/ethereum/go-ethereum v1.10.26
|
||||
|
||||
replace github.com/luxfi/corona => github.com/luxfi/corona v0.7.5
|
||||
|
||||
@@ -17,44 +17,6 @@ github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA
|
||||
github.com/StephenButtolph/canoto v0.17.3 h1:lvsnYD4b96vD1knnmp1xCmZqfYpY/jSeRozGdOfdvGI=
|
||||
github.com/StephenButtolph/canoto v0.17.3/go.mod h1:IcnAHC6nJUfQFVR9y60ko2ecUqqHHSB6UwI9NnBFZnE=
|
||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||
github.com/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/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=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.13/go.mod h1:yoTXOQKea18nrM69wGF9jBdG4WocSZA1h38A+t/MAsk=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 h1:NUS3K4BTDArQqNu2ih7yeDLaS3bmHD0YndtA6UP884g=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21/go.mod h1:YWNWJQNjKigKY1RHVJCuupeWDrrHjRqHm0N9rdrWzYI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=
|
||||
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/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/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/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/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=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.14/go.mod h1:WSvS1NLr7JaPunCXqpJnWk1Bjo7IxzZXrZi1QQCkuqM=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.18 h1:mP49nTpfKtpXLt5SLn8Uv8z6W+03jYVoOSAl/c02nog=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.18/go.mod h1:YO8TrYtFdl5w/4vmjL8zaBSsiNp3w0L1FfKVKenZT7w=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 h1:p8ogvvLugcR/zLBXTXrTkj0RYBUdErbMnAFFp12Lm/U=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10/go.mod h1:60dv0eZJfeVXfbT1tFJinbHrDfSJ2GZl4Q//OSSNAVw=
|
||||
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
|
||||
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
|
||||
@@ -85,8 +47,8 @@ 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/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 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
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/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
@@ -128,8 +90,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA=
|
||||
github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc=
|
||||
github.com/deckarep/golang-set/v2 v2.9.0 h1:prva4eP9UysWagLyKrtn074ughi0NnkIf0A4M5yOCKI=
|
||||
github.com/deckarep/golang-set/v2 v2.9.0/go.mod h1:EWknQXbs0mcFpat2QOoXV0Ee57cD+w6ZEN76BR2JVrM=
|
||||
github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ=
|
||||
github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||
@@ -160,8 +122,8 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4
|
||||
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
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/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/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/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs=
|
||||
@@ -174,8 +136,6 @@ github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxI
|
||||
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
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-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=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
@@ -208,6 +168,8 @@ 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.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
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=
|
||||
@@ -221,8 +183,6 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
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/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=
|
||||
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc h1:VBbFa1lDYWEeV5FZKUiYKYT0VxCp9twUmmaq9eb8sXw=
|
||||
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
|
||||
@@ -237,16 +197,12 @@ github.com/gorilla/rpc v1.2.1/go.mod h1:uNpOihAlF5xRFLuTYhfR0yfCTm0WTQSQttkMSptR
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
|
||||
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/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||
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/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=
|
||||
github.com/hanzos3/go-sdk v1.0.2/go.mod h1:oOp/rYVDpZIv2Mn3ZacAUiIahRNOi+V/vlGukvIcuI0=
|
||||
github.com/hashicorp/go-bexpr v0.1.16 h1:D+fKoGyUzXVS0FdjOX1ws3vIck8DVtBqQ0tsusmYDR8=
|
||||
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=
|
||||
@@ -275,8 +231,8 @@ github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlT
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||
github.com/klauspost/compress v1.18.5/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=
|
||||
@@ -290,151 +246,144 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4=
|
||||
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/accel v1.1.4 h1:UOvS/00vG6WByf2P1tGYRkoBcsUkFuCgw7o2xU03OoE=
|
||||
github.com/luxfi/accel v1.1.4/go.mod h1:K00BcnLzEYMPHwCFq8Tf/450ApmTs9xBVvYOnofJCkc=
|
||||
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:G69HbSV4R3vKEH9B0CulnRaMdSdf4RalMgP8xKmxHeI=
|
||||
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/ai v0.1.0 h1:PwTGob0GJivbdqNpUs82bvwcE8/qxyTokxfY7BZVPoo=
|
||||
github.com/luxfi/ai v0.1.0/go.mod h1:lTuY32dGjEJUgVheYuanlt1O0jHj0AZ09sUSpRsmH6M=
|
||||
github.com/luxfi/api v1.0.11 h1:t4fzN9Ox/Vy5msW2WpSXq4xCAmBXXJS0oM+zIjM9IiQ=
|
||||
github.com/luxfi/api v1.0.11/go.mod h1:q3Hh3mmkvp3cnv3aqe2+gCtmmSAL/uFjuzrqGavQLNA=
|
||||
github.com/luxfi/atomic v1.0.0 h1:xUV60MuzRvXngaQ1sM0yVC2v4TRoLlUGkkH7M9PS4yw=
|
||||
github.com/luxfi/atomic v1.0.0/go.mod h1:0G2mTlQ6TXWHICUHrUUPu1/qAiIyR4gSZ2tva9ci/bI=
|
||||
github.com/luxfi/cache v1.2.1 h1:kAzOS55/hmYeNKR+0HAKv4ma48Y6JjkI8UQeqdZ8bfI=
|
||||
github.com/luxfi/cache v1.2.1/go.mod h1:co7JTxZZHpKT31Yh01LFp5aZOxmoUg157FhBLQdQHVU=
|
||||
github.com/luxfi/chains v1.3.21 h1:Oe3/T0MnMKYBVzd+wK13R5qzRGQEMytysbs3PkCLWNc=
|
||||
github.com/luxfi/chains v1.3.21/go.mod h1:kOB+6VF6nh86E+QkuxrMOqreiKRb6olJSdgRfk819Gs=
|
||||
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/chains v1.2.3 h1:d/xrJRuLYc/AkeugR4kRTwlYHudh1dfqjIaHGZPfIcI=
|
||||
github.com/luxfi/chains v1.2.3/go.mod h1:aofU2aS8s5tHiD8VVQydcYStQCpR356f1UPHGlNi67U=
|
||||
github.com/luxfi/codec v1.1.4 h1:Yl8ZalMNkqo7cD6R9AjczAajkLOmsjyZ9+DASVYHrvg=
|
||||
github.com/luxfi/codec v1.1.4/go.mod h1:oGQ3j6E8c2P0pL0irYtWkrB1hmDUFIE0puXHK4gV5KI=
|
||||
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.25.31 h1:H/ePdadr+x7vpF0exvVjjYWLFoh8yqbERKnj0gZRLd8=
|
||||
github.com/luxfi/consensus v1.25.31/go.mod h1:cerfisfzmUJv8gbMcjcQUSdxlLpfn/+LdLY+4D95Nw0=
|
||||
github.com/luxfi/constants v1.5.8 h1:iNP9AWNUcM4Tps7jYnx49CwtCWAC9mYRxJfGou2za0g=
|
||||
github.com/luxfi/constants v1.5.8/go.mod h1:Pu5jWHdnUtQRbWC43yTUjU/pbIIKMDOd2a2yroSfo48=
|
||||
github.com/luxfi/consensus v1.25.2 h1:ee1UYG7Pa2MTGRCCOLwUhPByNW6SQM89jgRtMuVEoDY=
|
||||
github.com/luxfi/consensus v1.25.2/go.mod h1:jkKzKyIgg/JqaEumxZOJd9ofWM3pXnVVLlQp+3qo9SM=
|
||||
github.com/luxfi/constants v1.5.7 h1:a1tCMdxd+pClPMIPOaI9vcYGNy6cQIc2rubac2Trc0k=
|
||||
github.com/luxfi/constants v1.5.7/go.mod h1:z+Wc7skybZAA+xuBWNcmtv402S/BFqixL+FiSQXPg8U=
|
||||
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.7.9 h1:NQe9V/80CdKLvbaVRE2uepxvxg9KHbWfcGRKWrzLSHc=
|
||||
github.com/luxfi/corona v0.7.9/go.mod h1:SfS7xo/k4uoteEYwYy+QCMPzTU8EIEbLnbWKx5ENVCw=
|
||||
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/corona v0.7.5 h1:XqcnsKaiP/EyDJzmDhziS+kDhX34TsVQiXTM8PvRROg=
|
||||
github.com/luxfi/corona v0.7.5/go.mod h1:4aD7+ZqnlZ2aVuU/DBQ5aspIagv5ux45LW2sJ4+siY8=
|
||||
github.com/luxfi/crypto v1.19.15 h1:Tf+iPRXv08Rlj9k7iNtYy0e1tJWIgRcY0pTR4rQax6w=
|
||||
github.com/luxfi/crypto v1.19.15/go.mod h1:INjdZtke85k8hX/QAmTMAY8bbZ4gzGZQLqURg3xf6Gk=
|
||||
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.18.3 h1:gg+xwhKUxXa7fDoOD8IS91E71QqoEtcDCl2nfS61Jgg=
|
||||
github.com/luxfi/database v1.18.3/go.mod h1:sv0pYCGKlK1aNJTICxFUDpVWCJTigoLlshHmV/1pg7c=
|
||||
github.com/luxfi/filesystem v0.0.1 h1:VZ6xMFKaAPBW/ddlMsDnI2G0VU1lV5rYaVcW5d+KwEY=
|
||||
github.com/luxfi/filesystem v0.0.1/go.mod h1:OQVSU6XNwqrr1AI+MqkID2taHUclx7NYmmr3svgttec=
|
||||
github.com/luxfi/formatting v1.0.1 h1:ZnE1rAdEUds9yAegdVdGDOBGN6hLMPOv6E03Fp8IEYo=
|
||||
github.com/luxfi/formatting v1.0.1/go.mod h1:mYzNf5DJOiqSSKUPzNj5dKy4tstFbN3pZlkI5716eKc=
|
||||
github.com/luxfi/genesis v1.13.16 h1:suwWPwUu2nv1fxvx9vwHgcgJCzkCpiVMxBrJIh4S3BQ=
|
||||
github.com/luxfi/genesis v1.13.16/go.mod h1:qUa+AcTWwxv0x+CJochBsRNOMbmEBjw07HJKZLhs5c0=
|
||||
github.com/luxfi/genesis/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/genesis v1.12.19 h1:GwwbWOhiLcDJyM/17nI0s/sDaXN+PcZk9kjHYL9EFTo=
|
||||
github.com/luxfi/genesis v1.12.19/go.mod h1:SoSMZgvGMXAzYN7c7a6DYomR4JnyHIouHpyzTi+WM0k=
|
||||
github.com/luxfi/genesis/pkg/genesis/security v1.12.19 h1:UjlMWUfoMB8P/VyxeSupz5klWcfpRYgjT6eulVju8K8=
|
||||
github.com/luxfi/genesis/pkg/genesis/security v1.12.19/go.mod h1:68FLxDptmJ3fYYwS6jGIYkoa0NWMWzYhTpusf0FMeDg=
|
||||
github.com/luxfi/geth v1.16.98 h1:w187TtKuGStf3tm2bshuHVKBv2Frjx0lT54kQVXyNHA=
|
||||
github.com/luxfi/geth v1.16.98/go.mod h1:6kEzSExdk9CPQDPXALt6P3HfQqBq7KF1Jrrv9gBpxbU=
|
||||
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.2.15 h1:omE+E4+0Poj9DzM11ejSFgteaSQ3KDHi5g54iH6jcxI=
|
||||
github.com/luxfi/ids v1.2.15/go.mod h1:Fj73K5xcblvdE0SxU/ip+jE8VqNdu+80548su5KJ7xI=
|
||||
github.com/luxfi/ids v1.2.10 h1:f1WILZE199ayMuqnEyB2WP1qfMZkmozOQXSVYtB3e5k=
|
||||
github.com/luxfi/ids v1.2.10/go.mod h1:QBIwy3OHvrtskbUqKh1+OYRa6PsyR7f7oNX33sOfK7w=
|
||||
github.com/luxfi/keychain v1.0.2 h1:uQgmjs37/VBIALEiYrrszTpxvtqr07/YvS9TnmxGafs=
|
||||
github.com/luxfi/keychain v1.0.2/go.mod h1:q/4ULgZBlstKkwzOzG/0T6y73BDPgnkrcibbJyTvmbU=
|
||||
github.com/luxfi/keys v1.2.0 h1:3TAcr4twyMpwQp7J29ZRtIa5vzAoDrnXnLcPKVHJWmw=
|
||||
github.com/luxfi/keys v1.2.0/go.mod h1:SjsAaxo6sGmSp9OaHXUiVCqsknO8iPspN6jMOoEAMb8=
|
||||
github.com/luxfi/kms v1.11.7 h1:E25z8SCNTGOVvzzg5tj6pwJQ2K3FrE/nuy0KAfF+0zs=
|
||||
github.com/luxfi/kms v1.11.7/go.mod h1:XhLUVqN4RBv6j4Bj3MNgTZmHCnm74jH7RqqK0b9xbzw=
|
||||
github.com/luxfi/lattice/v7 v7.1.4 h1:hQR02M6cHTAV5+joOPi9gb9Gm+z/hKJnhJF4IlciIJs=
|
||||
github.com/luxfi/lattice/v7 v7.1.4/go.mod h1:DmIQFi3mJiehVsR235l1NKYEU0JhU649OX5p7gMEW2c=
|
||||
github.com/luxfi/lattice/v7 v7.1.0 h1:mr3HvN6olNTS2LT/xAW/JBhTqfvpsGmsopDMeR7BSJs=
|
||||
github.com/luxfi/lattice/v7 v7.1.0/go.mod h1:IaaUN+3ysnBG4BA8ILRYG0j80+qtYDP4C5lkaDb2pDE=
|
||||
github.com/luxfi/lens v0.1.4 h1:goGjGDXx2BNdjzXDunL5QT8elK2ZyCcc0z8TAbtWYrg=
|
||||
github.com/luxfi/lens v0.1.4/go.mod h1:mL+G8IK+9L41d78/2FYRgfhEzAjcr5+VEXB8SGuHbus=
|
||||
github.com/luxfi/log v1.4.3 h1:xkUKRWvQ4ZwvlUC2e0/RTtHYZOYSMvSQ9W9lbjwBmiI=
|
||||
github.com/luxfi/log v1.4.3/go.mod h1:myIkufyiQomSQH34K981kbz6cG4WUoerRUh7F4XhlQI=
|
||||
github.com/luxfi/magnetar v1.2.3 h1:n4UrJZLK+mhDDZr1HLl2H/KgA6o6v62r5oiC61R7awE=
|
||||
github.com/luxfi/magnetar v1.2.3/go.mod h1:z9PLkqzzYiaFGT/qFBQSnNoHmZrg8y7JlYGiNnHAAdk=
|
||||
github.com/luxfi/log v1.4.1 h1:rIfFRodb9jrD/w7KayaUk0Oc+37PaQQdKEEMJCjR8gw=
|
||||
github.com/luxfi/log v1.4.1/go.mod h1:64IE3xRMJcpkQwnPUfJw3pDj7wU0kRS7BZ9wM7R72jk=
|
||||
github.com/luxfi/math v1.4.1 h1:1t9bCCsEqnl9yIKrShlbs80DBKyYTWdnzkVfBqEeO7Q=
|
||||
github.com/luxfi/math v1.4.1/go.mod h1:QvbRxauQyE1w4lvbcLSe6c8yeJz2Zj1Bq1rayGgs2tA=
|
||||
github.com/luxfi/math/big v0.1.0 h1:Vz4c0RsZVPdIKPsHPgAJChH/R3p15WHRUz7LkLf+NIQ=
|
||||
github.com/luxfi/math/big v0.1.0/go.mod h1:BuxSu22RbO93xBLk5Eam5nldFponoJ73xDFz4uJ3Huk=
|
||||
github.com/luxfi/math/safe v0.0.1 h1:GfSBINV9mOFgHzd32JbgfHSLhlNn0BwnP43rteYEosc=
|
||||
github.com/luxfi/math/safe v0.0.1/go.mod h1:EejrmOJHh03YAD8+Zww8cPcMR1K3Q2I7w1dX4sMloeo=
|
||||
github.com/luxfi/mdns v0.1.1 h1:g2eRr9AXcziPkkcd24M+Qu9ApEpoKKjfI79QSNqv0rQ=
|
||||
github.com/luxfi/mdns v0.1.1/go.mod h1:dbp5f3h3aE7CGzwbaWzBM9cwdcekhmSrWhQevgYhhNA=
|
||||
github.com/luxfi/metric v1.5.9 h1:UAgXMNZf5oN/XJwwuKorf8iMaCj3nyP6thHPCwkUwY4=
|
||||
github.com/luxfi/metric v1.5.9/go.mod h1:ux+w3RZQCfF1zM8MO0wAWyNj/CsDlPd2mwTGshB9vY0=
|
||||
github.com/luxfi/metric v1.5.5 h1:JXruty5ZN/ljeRNaCSabQGg1Xr3re2E8wqajVUUs6w4=
|
||||
github.com/luxfi/metric v1.5.5/go.mod h1:CMguEhyuLi4YUWyXimJ+UHply99BDFrL0pxedB7rBqM=
|
||||
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/net v0.0.4 h1:z1d6Q5c9/79jb4vF0XwBBjlF5swH5NsgfaXA+Pgojq8=
|
||||
github.com/luxfi/net v0.0.4/go.mod h1:QvgHzCa767cVWtPpui0P7HW1IrA2+c++hhvaQ/t0yyw=
|
||||
github.com/luxfi/oracle v0.1.1-0.20260429020431-76258cfcddf9 h1:y+foW/ZJuTEb/AUUaJGrll2PVWumTiydWZuYJV4bJ/k=
|
||||
github.com/luxfi/oracle v0.1.1-0.20260429020431-76258cfcddf9/go.mod h1:ueeF3b4SrhpCNb8hUYeP/kV7qgkp/BKKjsKbUhEjg40=
|
||||
github.com/luxfi/p2p v1.19.2 h1:uqZq7ofmEDbXlTkv1QThtci01Q+dmDkNmAPeORIyP8E=
|
||||
github.com/luxfi/p2p v1.19.2/go.mod h1:tI9Bt1R0ouvVtJvXG4e20GlGeV4AR230k4mFF9Vglzk=
|
||||
github.com/luxfi/pq v1.0.3 h1:pFlQm1+5FuKTDUh2y/23bXWkN4I2Rc5iuxJypwDFFMs=
|
||||
github.com/luxfi/pq v1.0.3/go.mod h1:8bppZcRElfrVt0n3nYCZW3iX1TvhvzNbdjNdK1irgIE=
|
||||
github.com/luxfi/precompile v0.5.59 h1:eWENnhAAAhzsOpowVHXzq4nWyV9YkUO0LP9e3aqOtls=
|
||||
github.com/luxfi/precompile v0.5.59/go.mod h1:2Wb1RUHlEMHCvNzUhJZy4uyRUyuIly8G5iGMK+uecxk=
|
||||
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.1.5 h1:v6z88L31ut5PbbFUQXzmoHoJZvXJgbM8+5ZoOk25So8=
|
||||
github.com/luxfi/pulsar v1.1.5/go.mod h1:GPm+Q9ZdX604GE687vkBQhWNA1FOBvRbhYwhbbfdi2w=
|
||||
github.com/luxfi/precompile v0.5.27 h1:lEF5gXY8ny5xXp8Qg8PK0Y2RQlaRNOZeDU+wP6Sj24M=
|
||||
github.com/luxfi/precompile v0.5.27/go.mod h1:boKRJeoa4XEhCoylRnPI/0IGfBgm5bZnFNI9llDktWo=
|
||||
github.com/luxfi/proto v1.0.2 h1:kT/2c6M85nqJOzOZ9SYyZHCSGH32qexovMCB7ZPGk8g=
|
||||
github.com/luxfi/proto v1.0.2/go.mod h1:pZLKsCmhiPtmm3z7ezBbnsYT2m79q+cFSmzxvuBeANE=
|
||||
github.com/luxfi/protocol v0.0.4 h1:wf3JSyeNMEabOUG0vExtIAtjfpJAyHXlepGBwv5g9NE=
|
||||
github.com/luxfi/protocol v0.0.4/go.mod h1:9f35GLNlcHAz6LCvFBDZP5fTD2QnN0URXWGUOcD1JPU=
|
||||
github.com/luxfi/pulsar v1.0.12 h1:SD+GGat5JuqKVhRSRojdKhHhmOWr7rz74IMFiP8PPeg=
|
||||
github.com/luxfi/pulsar v1.0.12/go.mod h1:nWjIyef69Yv/y27lwy8hUfAE55lkj2GRP4zDIGZc1B0=
|
||||
github.com/luxfi/relay v0.0.0-20260429020048-c629fe160d3c h1:wiOLqdEEjIoq+Uubkm6F6GYQue+rzqnITScM3ohWGqI=
|
||||
github.com/luxfi/relay v0.0.0-20260429020048-c629fe160d3c/go.mod h1:P408Sn9NGwtcDOoGdkmWe0484dkWDvEvDDBM44Cv+s4=
|
||||
github.com/luxfi/resource v0.0.1 h1:mTh+ICWSy548GTUSSyx7V/X5dV18oEwxZeQEYGJQhD4=
|
||||
github.com/luxfi/resource v0.0.1/go.mod h1:wWpZktciYwIi6RNqA+fHwzmPrUJa7PRX7urfwT+spRE=
|
||||
github.com/luxfi/rpc v1.1.0 h1:B/PJbK399th1mHRDSufhCpVbAciZqId3LsaWhIGNWH4=
|
||||
github.com/luxfi/rpc v1.1.0/go.mod h1:s0bI7/Wg1ZdFdG/cQK+4pZNdEmUsXNBA3HeZRZ+XLeM=
|
||||
github.com/luxfi/runtime v1.1.3 h1:6Yp/PKwQCohjXmBR9GA+gamdSAp+xA2rdN6J/74Y4aw=
|
||||
github.com/luxfi/runtime v1.1.3/go.mod h1:r1uonDnxRCnPz6N6WYwaC72HW95KbFIAyChnJyxePGs=
|
||||
github.com/luxfi/sampler v1.1.0 h1:u3iRDl7V06ARh0e85h3HT+aZ1saCFo2yMMsh+dCJbqk=
|
||||
github.com/luxfi/sampler v1.1.0/go.mod h1:kJa53S3tC9+VSbuV3RFu68MmbCCBlr2UM39LOClQ/Hs=
|
||||
github.com/luxfi/sdk v1.17.9 h1:MfExzWNym7IicO2egiHg6N0WnImLtAUpjCpiD/zc2ZE=
|
||||
github.com/luxfi/sdk v1.17.9/go.mod h1:XvZuopyltjR4SvHvA1c6wtNcnO+FzLyjfm0v+FyN9sI=
|
||||
github.com/luxfi/staking v1.5.0 h1:9XLRGL2wx4D2JRnPjlWNkFh214286Ixr08efFLOQoo4=
|
||||
github.com/luxfi/staking v1.5.0/go.mod h1:OULMrYj4FYPCH7fxKOIJLNuzg5QhrSZfVyPWdGpnxG8=
|
||||
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.9.9 h1:zsEuMASTbyiLi7DkbjXBw3hsKIcqvpQt3Xu/MpZA5RQ=
|
||||
github.com/luxfi/threshold v1.9.9/go.mod h1:8zO1a2f3UMMsM1TkOVUoUbCR9h1sPhWH/ibblf13+h4=
|
||||
github.com/luxfi/rpc v1.0.2 h1:NLRcOYRW+io0d1d33RMkgOZea8nlhK09MbPgCXcU5wU=
|
||||
github.com/luxfi/rpc v1.0.2/go.mod h1:pgiHwMWgOuxYYIa0vsUBvrBI+Op6bhZ39guM9vtMUcE=
|
||||
github.com/luxfi/runtime v1.1.0 h1:6TrvzAmZVCTVbR1ebntHTO3/kVBaogPUSkxdDMnrTiw=
|
||||
github.com/luxfi/runtime v1.1.0/go.mod h1:Mfv2zlXqvfRFMS+/zXgG1TieyP9VnvtVzOGB437+o4Y=
|
||||
github.com/luxfi/sampler v1.0.0 h1:k8Sf6otW83w4pQp0jXLA+g3J/joB7w7SqXQsWmNTOV0=
|
||||
github.com/luxfi/sampler v1.0.0/go.mod h1:f96/ozlj9vFfZj+akLtrHn4VpulQahwB+MQQhpeIekk=
|
||||
github.com/luxfi/sdk v1.16.60 h1:MKIFpGAIQzbFADXNd6rV5KySlbegnvpDJyiNhtO7YZw=
|
||||
github.com/luxfi/sdk v1.16.60/go.mod h1:se4G9mlLc6WdjLQlydg6J3pFEeMlEH+MCO1puJ8sYYc=
|
||||
github.com/luxfi/staking v1.1.0 h1:oB1f86WG9duxONe6unISHpUlelnmVg8+BOg4qP83q6Y=
|
||||
github.com/luxfi/staking v1.1.0/go.mod h1:xwYacmreoKoy31ELT6QPno6b5M8q7bg4zJrmmt524X4=
|
||||
github.com/luxfi/sys v0.0.0-20260110090042-50187ec5ffd8 h1:6L5294QWwXzy9he6wAM+Dc39rV4iwyl9hex0k2pXPsg=
|
||||
github.com/luxfi/sys v0.0.0-20260110090042-50187ec5ffd8/go.mod h1:Y0UwjnEJYSbGM7IvJbKYLf8BWNVLfT8Oz00bEiDITHw=
|
||||
github.com/luxfi/threshold v1.8.5 h1:uUTzlc5z8xElxcF9HGLVY1HUX+vsseMX0uryPuOgr84=
|
||||
github.com/luxfi/threshold v1.8.5/go.mod h1:d066yAD8CjSjOeuOYkE+P1/f93F14+NKYoHoJIGfVbI=
|
||||
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/trace v0.1.4 h1:ttCRyXGwWuz232se+lIUqhWHBoTuvPLhHH/hLWyqtaM=
|
||||
github.com/luxfi/trace v0.1.4/go.mod h1:Az7HWh+PCuPftXjQu+ssjv51nKauaFu+q2un7bmZYBA=
|
||||
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/upgrade v1.0.0 h1:mM6YXvU8VzoclA7IdtuE5bmnMuFquYxjKUUW84LJz54=
|
||||
github.com/luxfi/upgrade v1.0.0/go.mod h1:DZF7dO4erhUEKf907B2e65k4/vGkCPkUngpvIEUS3eI=
|
||||
github.com/luxfi/utils v1.1.4 h1:8OY0jXCkpp6OotN1Y++6DATJkvtEwzq2k0p56imJlAk=
|
||||
github.com/luxfi/utils v1.1.4/go.mod h1:c3yz1RjzrB+cs5GZm+q1T3/2cCKElO9vxm9yRRtgSEM=
|
||||
github.com/luxfi/utxo v0.3.2 h1:fez7ptBnAoRYA5FpRz/uFWY8hcZwd4X00UYLnxuJeXE=
|
||||
github.com/luxfi/utxo v0.3.2/go.mod h1:O8Ucl6Sj0nM7l3Y+uqzucVu309yHcRQY++rg9yXmmdo=
|
||||
github.com/luxfi/validators v1.2.0 h1:VygpiBqBAdGrfkb7xzE2yrVmnXaqE+hm8FLWdGXO7G8=
|
||||
github.com/luxfi/validators v1.2.0/go.mod h1:GYLulrNXAan23ZlX7sgWVbVnLpUexeB/m2qr2ymsXok=
|
||||
github.com/luxfi/version v1.0.1 h1:T/1KYWEMmsrNQk7pN7PFPAwh/7XbeX7cFAKLBqI37Sk=
|
||||
github.com/luxfi/version v1.0.1/go.mod h1:Y5fPkQ2DB0XRBCxgSPXp4ISzL1/jptKnmFknShRJCyg=
|
||||
github.com/luxfi/vm v1.2.5 h1:L1etY/gh68f9tns1BtyDUpZcBVqc3Ng1mqU3n38GyLo=
|
||||
github.com/luxfi/vm v1.2.5/go.mod h1:TCCg4lDcQFCjxaxfXnxPIrpRSVAyyf2ucT4A4w654Hg=
|
||||
github.com/luxfi/warp v1.19.5 h1:vigIDV4JxLz8bLxQ97dOIcZVM9FAXHmrKdmIh+/WWvk=
|
||||
github.com/luxfi/warp v1.19.5/go.mod h1:t5upY5vhKvjapImmUsgPUlW8C8LZhvQpQg5WzAitQs0=
|
||||
github.com/luxfi/zap v0.8.10 h1:QKNTAsenkke+qQw/QGHVdVZdV48bzbPkoXq5SDCPhs0=
|
||||
github.com/luxfi/zap v0.8.10/go.mod h1:JfqII8VtVQYLLTX6obU1DP9sjGqf9L24vfug5ifh0b8=
|
||||
github.com/luxfi/zapcodec v1.0.1 h1:pRxLxCOi6uihQMg8A8riDjNjefU2cXZxfRVZ+obeuL8=
|
||||
github.com/luxfi/zapcodec v1.0.1/go.mod h1:txrRt2JK4O76ssTxlXIwoNVsgzyZVL0ES4mlXqGNogs=
|
||||
github.com/luxfi/zapdb v1.10.1 h1:XV3k4UTTKKxUMgbfC7woPXgUEIJd3P5nj2lGTQ88xeE=
|
||||
github.com/luxfi/zapdb v1.10.1/go.mod h1:3Y0hH2A9kvjR+Bp9N2yEbtHnhXGHhqCQOLvBRkHrrM0=
|
||||
github.com/luxfi/vm v1.0.40 h1:kA0V/9p1VdC88EjgbljWb8t+fl9qz5bCyF3IxnV20yg=
|
||||
github.com/luxfi/vm v1.0.40/go.mod h1:Y5WUKhT76PR6HnbUykHBWI2f5sjssrm5BL6/o7b6IfQ=
|
||||
github.com/luxfi/warp v1.18.6 h1:+ly7mHz77ig4yUyLax32aAjk7tiQQ6eygzOSDNfYRbQ=
|
||||
github.com/luxfi/warp v1.18.6/go.mod h1:OBN23yiGl+E4qupkPHuetBIEqsI8q5leE8WJH8soFjY=
|
||||
github.com/luxfi/zapdb v1.10.0 h1:1lLHEmkyC0BucnA/zjQYsMkUVxuEo2vQkEaQGjYfuuc=
|
||||
github.com/luxfi/zapdb v1.10.0/go.mod h1:Qukh3hDRD0MnxA6z+a28JTnXhN85AiLLgp6TYr4QAMc=
|
||||
github.com/luxfi/zwing v0.5.2 h1:2+nDKHVIdT8GvaKO79GuO3x/3DgJvX0gI32h4P+P6RU=
|
||||
github.com/luxfi/zwing v0.5.2/go.mod h1:8nixkEL3bhO2LrqVqhJ8WgT+QGUtnCPIQIhFQh9gQio=
|
||||
github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
|
||||
github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
|
||||
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
|
||||
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-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
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=
|
||||
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||
github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM=
|
||||
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b h1:QrHweqAtyJ9EwCaGHBu1fghwxIPiopAHV06JlXrMHjk=
|
||||
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b/go.mod h1:xxLb2ip6sSUts3g1irPVHyk/DGslwQsNOo9I7smJfNU=
|
||||
@@ -442,6 +391,8 @@ 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 h1:ShkWi8Tyj9RtU57OQB2HIXKz4bFgtVib0bbT1sbtLI8=
|
||||
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/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
@@ -450,8 +401,8 @@ github.com/mitchellh/pointerstructure v1.2.1 h1:ZhBBeX8tSlRpu/FFhXH4RC4OJzFlqsQh
|
||||
github.com/mitchellh/pointerstructure v1.2.1/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4=
|
||||
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/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||
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/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 h1:4kuARK6Y6FxaNu/BnU2OAaLF86eTVhP2hjTB6iMvItA=
|
||||
@@ -477,8 +428,8 @@ github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAl
|
||||
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
|
||||
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/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
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=
|
||||
@@ -510,8 +461,6 @@ github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTU
|
||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
|
||||
github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
@@ -558,8 +507,8 @@ github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSW
|
||||
github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE=
|
||||
github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a h1:1ur3QoCqvE5fl+nylMaIr9PVV1w343YRDtsy+Rwu7XI=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48=
|
||||
github.com/thepudds/fzgen v0.4.3 h1:srUP/34BulQaEwPP/uHZkdjUcUjIzL7Jkf4CBVryiP8=
|
||||
github.com/thepudds/fzgen v0.4.3/go.mod h1:BhhwtRhzgvLWAjjcHDJ9pEiLD2Z9hrVIFjBCHJ//zJ4=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
@@ -570,12 +519,12 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
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/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/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
|
||||
github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
||||
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
||||
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
||||
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/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
@@ -595,20 +544,26 @@ github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
|
||||
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
|
||||
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
||||
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
||||
go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=
|
||||
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/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||
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 v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
|
||||
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 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto=
|
||||
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 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
|
||||
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.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
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/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||
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/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
@@ -627,22 +582,19 @@ 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/exp v0.0.0-20260529124908-c761662dc8c9 h1:4d4PbuBNwaxMXkXI8yiIYjydtMU+04RHeuSxJdgKftM=
|
||||
golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
|
||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
|
||||
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.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||
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=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
@@ -651,8 +603,8 @@ 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.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
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=
|
||||
@@ -665,7 +617,6 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -682,8 +633,9 @@ 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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -691,19 +643,18 @@ 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.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
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=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
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.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||
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=
|
||||
@@ -711,6 +662,12 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
|
||||
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-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||
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=
|
||||
@@ -719,8 +676,8 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
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=
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
# Platform-native CI/CD — luxfi/node (the GitHub-Actions escape)
|
||||
#
|
||||
# Build is owned by platform.hanzo.ai (NOT GitHub Actions). On a tag push,
|
||||
# platform reads this file, schedules ONE build job per matrix entry onto the
|
||||
# self-hosted arcd long-poll fabric, and an arcd runner on the `lux-build-*`
|
||||
# pool builds + pushes the image to GHCR. There is NO GitHub-Actions build hop:
|
||||
# the legacy .github/workflows/docker.yml is retired (see RELEASE.md §retire).
|
||||
#
|
||||
# This ONE Dockerfile build produces BOTH lux release artifacts:
|
||||
# 1. the node image -> ghcr.io/luxfi/node:<tag> (luxd + all 12 VM plugins
|
||||
# baked at /luxd/build/plugins/)
|
||||
# 2. the plugin SET -> s3://lux-plugins-<env>/<pluginset>/ (published in a
|
||||
# second, decoupled step from the image's baked plugins — see
|
||||
# scripts/publish_plugin_set.sh; consumed by the operator plugin-fetch
|
||||
# init container via the LuxNetwork CR pluginSource).
|
||||
#
|
||||
# Schema reference: ~/work/hanzo/platform/docs/PLATFORM_CI.md.
|
||||
# Release runbook (the ONE canonical way): ./RELEASE.md.
|
||||
#
|
||||
# Pool resolution: <org>-build-<os>-<arch>. org=luxfi -> brand=lux, so this
|
||||
# repo's pools are `lux-build-linux-amd64` and `lux-build-linux-arm64`. These
|
||||
# MUST match the live arcd scale-set names exactly.
|
||||
#
|
||||
# Tagging: a release is a `v*` git-tag push. The webhook decoder maps the tag
|
||||
# ref to `branch=<tagname>`, so `tag-pattern: "{{git.branch}}"` yields the
|
||||
# image tag `vX.Y.Z` (immutable, semver-only — no :latest, no floating tags).
|
||||
build:
|
||||
matrix:
|
||||
- { os: linux, arch: amd64 }
|
||||
- { os: linux, arch: arm64 }
|
||||
dockerfile: ./Dockerfile
|
||||
context: .
|
||||
image: ghcr.io/luxfi/node
|
||||
tag-pattern: "{{git.branch}}"
|
||||
push: true
|
||||
dispatch: native
|
||||
# No `deploy:` block. luxd rollout is owned by the lux operator (LuxNetwork CR,
|
||||
# ~/work/lux/operator) which the hanzo operator Service-CR rollout does not
|
||||
# model. Deploy is a separate, operator-driven step (RELEASE.md §deploy).
|
||||
@@ -1,10 +1,10 @@
|
||||
#!/bin/bash
|
||||
# Import RLP blocks for chains after deployment
|
||||
# Usage: ./import-chain-rlp.sh [mainnet|testnet|both]
|
||||
# Import RLP blocks for subnet chains after deployment
|
||||
# Usage: ./import-subnet-rlp.sh [mainnet|testnet|both]
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Chains must be deployed (run deploy-chains.sh first)
|
||||
# - Nodes must be tracking the chains
|
||||
# - Subnets must be deployed (run deploy-subnets.sh first)
|
||||
# - Nodes must be tracking the subnet chains
|
||||
# - Blockchain IDs must be set in the node config
|
||||
|
||||
set -e
|
||||
@@ -37,7 +37,7 @@ import_rlp() {
|
||||
local ns="lux-$network"
|
||||
kubectl --context do-sfo3-lux-k8s exec -n "$ns" luxd-0 -- \
|
||||
wget -q -O /tmp/import.rlp "$rpc_url" 2>/dev/null || true
|
||||
echo "NOTE: For chain import, you may need to use admin.importChain RPC"
|
||||
echo "NOTE: For subnet chain import, you may need to use admin.importChain RPC"
|
||||
echo " or copy the RLP file to the node and import manually."
|
||||
}
|
||||
}
|
||||
@@ -46,20 +46,20 @@ TARGET="${1:-both}"
|
||||
|
||||
case "$TARGET" in
|
||||
mainnet)
|
||||
echo "=== Importing chain RLP blocks to mainnet ==="
|
||||
echo "=== Importing subnet RLP blocks to mainnet ==="
|
||||
import_rlp mainnet zoo-mainnet 200200 zoo-mainnet-200200.rlp "$MAINNET_RPC"
|
||||
import_rlp mainnet spc-mainnet 36911 spc-mainnet-36911.rlp "$MAINNET_RPC"
|
||||
;;
|
||||
testnet)
|
||||
echo "=== Importing chain RLP blocks to testnet ==="
|
||||
echo "=== Importing subnet RLP blocks to testnet ==="
|
||||
import_rlp testnet zoo-testnet 200201 zoo-testnet-200201.rlp "$TESTNET_RPC"
|
||||
;;
|
||||
both|all)
|
||||
echo "=== Importing chain RLP blocks to mainnet ==="
|
||||
echo "=== Importing subnet RLP blocks to mainnet ==="
|
||||
import_rlp mainnet zoo-mainnet 200200 zoo-mainnet-200200.rlp "$MAINNET_RPC"
|
||||
import_rlp mainnet spc-mainnet 36911 spc-mainnet-36911.rlp "$MAINNET_RPC"
|
||||
echo ""
|
||||
echo "=== Importing chain RLP blocks to testnet ==="
|
||||
echo "=== Importing subnet RLP blocks to testnet ==="
|
||||
import_rlp testnet zoo-testnet 200201 zoo-testnet-200201.rlp "$TESTNET_RPC"
|
||||
;;
|
||||
*)
|
||||
+1
-1
@@ -9,8 +9,8 @@ import (
|
||||
|
||||
"github.com/luxfi/formatting"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/node/utils/json"
|
||||
"github.com/luxfi/rpc"
|
||||
"github.com/luxfi/node/utils/json"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
|
||||
@@ -11,9 +11,9 @@ import (
|
||||
|
||||
"github.com/luxfi/formatting"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/node/utils/json"
|
||||
"github.com/luxfi/rpc"
|
||||
"github.com/luxfi/utils"
|
||||
"github.com/luxfi/node/utils/json"
|
||||
)
|
||||
|
||||
type mockClient struct {
|
||||
|
||||
+11
-49
@@ -1,62 +1,24 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package indexer
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"math"
|
||||
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/codec"
|
||||
"github.com/luxfi/codec/linearcodec"
|
||||
)
|
||||
|
||||
// On-disk encoding of an indexed Container. Big-endian wire layout:
|
||||
//
|
||||
// bytes 0..31 : Container.ID (ids.IDLen)
|
||||
// bytes 32..35 : len(Container.Bytes) (uint32 BE)
|
||||
// bytes 36..N : Container.Bytes (variable)
|
||||
// bytes N..N+8 : Container.Timestamp (int64 BE, two's complement)
|
||||
//
|
||||
// No codec version prefix — the indexer DB lives inside the node, so a
|
||||
// forward-only schema is fine. Older versions stored a 2-byte codec
|
||||
// version header; that format is no longer accepted (hard cut from
|
||||
// luxfi/codec rip — Wave 1D).
|
||||
const CodecVersion = 0
|
||||
|
||||
var (
|
||||
errContainerTooShort = errors.New("indexer: encoded container shorter than header")
|
||||
errContainerBytesLen = errors.New("indexer: encoded container length exceeds payload")
|
||||
)
|
||||
var Codec codec.Manager
|
||||
|
||||
// marshalContainer encodes c to its on-disk representation.
|
||||
func marshalContainer(c Container) ([]byte, error) {
|
||||
out := make([]byte, 0, ids.IDLen+4+len(c.Bytes)+8)
|
||||
out = append(out, c.ID[:]...)
|
||||
var lenBuf [4]byte
|
||||
binary.BigEndian.PutUint32(lenBuf[:], uint32(len(c.Bytes)))
|
||||
out = append(out, lenBuf[:]...)
|
||||
out = append(out, c.Bytes...)
|
||||
var tsBuf [8]byte
|
||||
binary.BigEndian.PutUint64(tsBuf[:], uint64(c.Timestamp))
|
||||
out = append(out, tsBuf[:]...)
|
||||
return out, nil
|
||||
}
|
||||
func init() {
|
||||
lc := linearcodec.NewDefault()
|
||||
Codec = codec.NewManager(math.MaxInt)
|
||||
|
||||
// unmarshalContainer decodes b into a Container.
|
||||
func unmarshalContainer(b []byte) (Container, error) {
|
||||
const headerLen = ids.IDLen + 4
|
||||
if len(b) < headerLen+8 {
|
||||
return Container{}, errContainerTooShort
|
||||
if err := Codec.RegisterCodec(CodecVersion, lc); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
var c Container
|
||||
copy(c.ID[:], b[:ids.IDLen])
|
||||
bytesLen := binary.BigEndian.Uint32(b[ids.IDLen : ids.IDLen+4])
|
||||
if uint64(len(b)) < uint64(headerLen)+uint64(bytesLen)+8 {
|
||||
return Container{}, errContainerBytesLen
|
||||
}
|
||||
payloadStart := headerLen
|
||||
payloadEnd := payloadStart + int(bytesLen)
|
||||
c.Bytes = make([]byte, bytesLen)
|
||||
copy(c.Bytes, b[payloadStart:payloadEnd])
|
||||
c.Timestamp = int64(binary.BigEndian.Uint64(b[payloadEnd : payloadEnd+8]))
|
||||
return c, nil
|
||||
}
|
||||
|
||||
+4
-4
@@ -8,13 +8,13 @@ import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/luxfi/runtime"
|
||||
"github.com/luxfi/database"
|
||||
"github.com/luxfi/database/prefixdb"
|
||||
"github.com/luxfi/database/versiondb"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
nodeconsensus "github.com/luxfi/node/consensus"
|
||||
"github.com/luxfi/runtime"
|
||||
"github.com/luxfi/timer/mockable"
|
||||
)
|
||||
|
||||
@@ -131,7 +131,7 @@ func (i *index) Accept(rt *runtime.Runtime, containerID ids.ID, containerBytes [
|
||||
)
|
||||
// Persist index --> Container
|
||||
nextAcceptedIndexBytes := database.PackUInt64(i.nextAcceptedIndex)
|
||||
bytes, err := marshalContainer(Container{
|
||||
bytes, err := Codec.Marshal(CodecVersion, Container{
|
||||
ID: containerID,
|
||||
Bytes: containerBytes,
|
||||
Timestamp: i.clock.Time().UnixNano(),
|
||||
@@ -189,8 +189,8 @@ func (i *index) getContainerByIndexBytes(indexBytes []byte) (Container, error) {
|
||||
)
|
||||
return Container{}, fmt.Errorf("couldn't read from database: %w", err)
|
||||
}
|
||||
container, err := unmarshalContainer(containerBytes)
|
||||
if err != nil {
|
||||
var container Container
|
||||
if _, err := Codec.Unmarshal(containerBytes, &container); err != nil {
|
||||
return Container{}, fmt.Errorf("couldn't unmarshal container: %w", err)
|
||||
}
|
||||
return container, nil
|
||||
|
||||
+2
-2
@@ -10,15 +10,15 @@ import (
|
||||
|
||||
"github.com/gorilla/rpc/v2"
|
||||
|
||||
"github.com/luxfi/codec/wrappers"
|
||||
"github.com/luxfi/database"
|
||||
"github.com/luxfi/database/prefixdb"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/node/server/http"
|
||||
"github.com/luxfi/node/chains"
|
||||
nodeconsensus "github.com/luxfi/node/consensus"
|
||||
"github.com/luxfi/node/server/http"
|
||||
"github.com/luxfi/node/utils/json"
|
||||
"github.com/luxfi/node/utils/wrappers"
|
||||
"github.com/luxfi/runtime"
|
||||
"github.com/luxfi/timer/mockable"
|
||||
"github.com/luxfi/vm"
|
||||
|
||||
@@ -1,604 +0,0 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
//go:build ignore
|
||||
|
||||
// Command regenfixtures regenerates wire-byte-anchored test fixtures
|
||||
// after a wire-format change.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// go run ./internal/regenfixtures -pkg ./p/block
|
||||
// go run ./internal/regenfixtures -pkg ./p/block -dry-run
|
||||
// go run ./internal/regenfixtures -pkg ./p/block,./p/state
|
||||
//
|
||||
// Algorithm:
|
||||
//
|
||||
// 1. Run `go test -v -count=1 <pkg>` and capture stderr+stdout.
|
||||
// 2. Parse each failure for `expected: []byte{...}` and `actual: []byte{...}`
|
||||
// pairs (testify produces both in Not-equal failures).
|
||||
// 3. For each `(expected, actual)` pair, scan the test files in <pkg>
|
||||
// using go/parser to find every `[]byte{...}` composite literal.
|
||||
// Decode each literal's byte values. If they match `expected`,
|
||||
// rewrite that literal in-place with `actual`'s byte values
|
||||
// formatted in canonical 16-bytes-per-line layout.
|
||||
// 4. Emit a `// Regenerated post-LP-023 ZAP cutover` comment above
|
||||
// each rewritten literal (idempotent — skip if already present).
|
||||
//
|
||||
// Design notes:
|
||||
//
|
||||
// - AST-based identification means comments inside the original
|
||||
// literal (e.g. `// Codec version`) are dropped on regenerate.
|
||||
// This is correct: the byte boundaries change with the wire
|
||||
// format, so the prior comments would lie.
|
||||
//
|
||||
// - go/printer is not used to write the new literal because we want
|
||||
// deterministic 16-bytes-per-line layout. We render the literal
|
||||
// ourselves and string-splice it into the source.
|
||||
//
|
||||
// - We disambiguate composite literals by their byte payload, not
|
||||
// their position. If the same expected bytes appear in multiple
|
||||
// literals in the same file, all are rewritten to the same actual
|
||||
// bytes. This is correct iff the test cases are uniquely keyed
|
||||
// by their fixture — which is the case for every package we
|
||||
// touch (a fixture is one block-or-tx's wire encoding).
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
pkgFlag = flag.String("pkg", "", "comma-separated package paths to regenerate (relative to module root)")
|
||||
dryRun = flag.Bool("dry-run", false, "report what would change without writing files")
|
||||
verbose = flag.Bool("v", false, "verbose output")
|
||||
annotateTag = flag.String("annotate", "Regenerated post-LP-023 ZAP cutover", "comment to add above rewritten literals")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
if *pkgFlag == "" {
|
||||
fmt.Fprintln(os.Stderr, "usage: regenfixtures -pkg <pkg1>[,<pkg2>...] [-dry-run] [-v]")
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
pkgs := strings.Split(*pkgFlag, ",")
|
||||
totalChanges := 0
|
||||
totalFails := 0
|
||||
for _, pkg := range pkgs {
|
||||
pkg = strings.TrimSpace(pkg)
|
||||
if pkg == "" {
|
||||
continue
|
||||
}
|
||||
changes, fails, err := regenPackage(pkg, *dryRun, *verbose, *annotateTag)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "regenfixtures: %s: %v\n", pkg, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
totalChanges += changes
|
||||
totalFails += fails
|
||||
fmt.Printf("[%s] rewrote %d literal(s) across %d failure(s)\n", pkg, changes, fails)
|
||||
}
|
||||
fmt.Printf("\ntotal: %d literal(s) rewritten across %d failure(s)\n", totalChanges, totalFails)
|
||||
}
|
||||
|
||||
func regenPackage(pkg string, dryRun, verbose bool, annotateTag string) (int, int, error) {
|
||||
pairs, err := collectFailures(pkg, verbose)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("collect failures: %w", err)
|
||||
}
|
||||
if len(pairs) == 0 {
|
||||
if verbose {
|
||||
fmt.Printf("[%s] no Not-equal failures captured (package may already be green or fail before assertion)\n", pkg)
|
||||
}
|
||||
return 0, 0, nil
|
||||
}
|
||||
if verbose {
|
||||
fmt.Printf("[%s] captured %d expected/actual pair(s)\n", pkg, len(pairs))
|
||||
}
|
||||
|
||||
files, err := testFilesInPkg(pkg)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("enumerate test files: %w", err)
|
||||
}
|
||||
|
||||
changes := 0
|
||||
for _, file := range files {
|
||||
n, err := rewriteFile(file, pairs, dryRun, verbose, annotateTag)
|
||||
if err != nil {
|
||||
return changes, len(pairs), fmt.Errorf("rewrite %s: %w", file, err)
|
||||
}
|
||||
changes += n
|
||||
}
|
||||
return changes, len(pairs), nil
|
||||
}
|
||||
|
||||
// pair carries one expected/actual byte slice from a test failure.
|
||||
type pair struct {
|
||||
expected []byte
|
||||
actual []byte
|
||||
src string // free-form provenance (test name, file:line)
|
||||
// kind indicates how this pair was scraped:
|
||||
// "bytes" — testify Not-equal on []byte literals
|
||||
// "string" — testify Not-equal on string literals (e.g. base64)
|
||||
kind string
|
||||
// raw versions of expected/actual as they appear in source — only
|
||||
// populated for kind="string". For kind="bytes" we compare by the
|
||||
// decoded byte payload, regardless of source formatting.
|
||||
expectedRaw string
|
||||
actualRaw string
|
||||
}
|
||||
|
||||
// fingerprint returns a fixed-string for map keying on expected bytes.
|
||||
func (p pair) fingerprint() string {
|
||||
if p.kind == "string" {
|
||||
return "s:" + p.expectedRaw
|
||||
}
|
||||
return "b:" + string(p.expected)
|
||||
}
|
||||
|
||||
// collectFailures runs `go test -v -count=1` and parses Not-equal
|
||||
// failures from the output.
|
||||
func collectFailures(pkg string, verbose bool) ([]pair, error) {
|
||||
cmd := exec.Command("go", "test", "-v", "-count=1", pkg)
|
||||
out, _ := cmd.CombinedOutput()
|
||||
if verbose {
|
||||
fmt.Printf("--- go test output (%s, %d bytes) ---\n", pkg, len(out))
|
||||
}
|
||||
return parseFailures(string(out))
|
||||
}
|
||||
|
||||
// reExpected matches `expected: []byte{...}` in testify Not-equal output.
|
||||
var reExpected = regexp.MustCompile(`expected:\s*\[\]byte\{([^}]*)\}`)
|
||||
var reActual = regexp.MustCompile(`actual\s*:\s*\[\]byte\{([^}]*)\}`)
|
||||
var reTestLine = regexp.MustCompile(`Test:\s*(\S+)`)
|
||||
var reErrTrace = regexp.MustCompile(`Error Trace:\s*(\S+)`)
|
||||
|
||||
// reExpectedString matches `expected: "..."` in testify Not-equal output.
|
||||
// Strings are typically base64-encoded byte buffers in our test suite.
|
||||
// We require the literal to be on its own line ending in `\n` to avoid
|
||||
// pulling fragments out of multi-line strings.
|
||||
var reExpectedString = regexp.MustCompile(`expected:\s*"([^"]*)"`)
|
||||
var reActualString = regexp.MustCompile(`actual\s*:\s*"([^"]*)"`)
|
||||
|
||||
func parseFailures(output string) ([]pair, error) {
|
||||
// Each failure block contains both an `expected:` and `actual:` line.
|
||||
// We split on test boundaries and pull them in order. We don't
|
||||
// require the lines to be adjacent — testify can interleave with
|
||||
// the rendered Diff — but the next `actual:` after an `expected:`
|
||||
// is its counterpart.
|
||||
expectedMatches := reExpected.FindAllStringSubmatchIndex(output, -1)
|
||||
actualMatches := reActual.FindAllStringSubmatchIndex(output, -1)
|
||||
|
||||
if len(expectedMatches) != len(actualMatches) {
|
||||
// Not fatal — we pair what we can in order.
|
||||
}
|
||||
|
||||
n := len(expectedMatches)
|
||||
if len(actualMatches) < n {
|
||||
n = len(actualMatches)
|
||||
}
|
||||
|
||||
pairs := make([]pair, 0, n)
|
||||
for i := 0; i < n; i++ {
|
||||
expSrc := output[expectedMatches[i][2]:expectedMatches[i][3]]
|
||||
actSrc := output[actualMatches[i][2]:actualMatches[i][3]]
|
||||
exp, err := decodeGoByteList(expSrc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse expected: %w", err)
|
||||
}
|
||||
act, err := decodeGoByteList(actSrc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse actual: %w", err)
|
||||
}
|
||||
// Locate test name within the surrounding window.
|
||||
windowStart := expectedMatches[i][0]
|
||||
windowEnd := len(output)
|
||||
if i+1 < len(expectedMatches) {
|
||||
windowEnd = expectedMatches[i+1][0]
|
||||
}
|
||||
window := output[windowStart:windowEnd]
|
||||
var src string
|
||||
if m := reTestLine.FindStringSubmatch(window); m != nil {
|
||||
src = m[1]
|
||||
}
|
||||
if m := reErrTrace.FindStringSubmatch(window); m != nil {
|
||||
if src != "" {
|
||||
src += " @ " + m[1]
|
||||
} else {
|
||||
src = m[1]
|
||||
}
|
||||
}
|
||||
pairs = append(pairs, pair{expected: exp, actual: act, src: src, kind: "bytes"})
|
||||
}
|
||||
|
||||
// Scrape string fixtures (e.g. base64-encoded byte buffers).
|
||||
expectedStrMatches := reExpectedString.FindAllStringSubmatchIndex(output, -1)
|
||||
actualStrMatches := reActualString.FindAllStringSubmatchIndex(output, -1)
|
||||
nStr := len(expectedStrMatches)
|
||||
if len(actualStrMatches) < nStr {
|
||||
nStr = len(actualStrMatches)
|
||||
}
|
||||
for i := 0; i < nStr; i++ {
|
||||
expSrc := output[expectedStrMatches[i][2]:expectedStrMatches[i][3]]
|
||||
actSrc := output[actualStrMatches[i][2]:actualStrMatches[i][3]]
|
||||
// Locate test name within the surrounding window.
|
||||
windowStart := expectedStrMatches[i][0]
|
||||
windowEnd := len(output)
|
||||
if i+1 < len(expectedStrMatches) {
|
||||
windowEnd = expectedStrMatches[i+1][0]
|
||||
}
|
||||
window := output[windowStart:windowEnd]
|
||||
var src string
|
||||
if m := reTestLine.FindStringSubmatch(window); m != nil {
|
||||
src = m[1]
|
||||
}
|
||||
if m := reErrTrace.FindStringSubmatch(window); m != nil {
|
||||
if src != "" {
|
||||
src += " @ " + m[1]
|
||||
} else {
|
||||
src = m[1]
|
||||
}
|
||||
}
|
||||
pairs = append(pairs, pair{
|
||||
src: src,
|
||||
kind: "string",
|
||||
expectedRaw: expSrc,
|
||||
actualRaw: actSrc,
|
||||
})
|
||||
}
|
||||
|
||||
// Deduplicate: same expected bytes can appear in many subtests
|
||||
// (e.g., len(0) buffers); we want to map the union of expected→actual
|
||||
// across all failures.
|
||||
uniq := make(map[string]pair, len(pairs))
|
||||
for _, p := range pairs {
|
||||
key := p.fingerprint()
|
||||
if existing, ok := uniq[key]; ok {
|
||||
if !bytes.Equal(existing.actual, p.actual) {
|
||||
return nil, fmt.Errorf(
|
||||
"conflict: same expected bytes map to two different actual outputs (test1=%s, test2=%s)",
|
||||
existing.src, p.src,
|
||||
)
|
||||
}
|
||||
continue
|
||||
}
|
||||
uniq[key] = p
|
||||
}
|
||||
out := make([]pair, 0, len(uniq))
|
||||
for _, p := range uniq {
|
||||
out = append(out, p)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].src < out[j].src })
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// decodeGoByteList parses comma-separated Go byte literals like "0x0, 0x1, 0xff".
|
||||
func decodeGoByteList(s string) ([]byte, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return []byte{}, nil
|
||||
}
|
||||
parts := strings.Split(s, ",")
|
||||
out := make([]byte, 0, len(parts))
|
||||
for _, raw := range parts {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
continue
|
||||
}
|
||||
// Strip Go literal prefixes/suffixes — we only see 0x..
|
||||
// or decimal here.
|
||||
v, err := strconv.ParseUint(raw, 0, 8)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("byte literal %q: %w", raw, err)
|
||||
}
|
||||
out = append(out, byte(v))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// testFilesInPkg lists *_test.go files in pkg (relative to module root).
|
||||
func testFilesInPkg(pkg string) ([]string, error) {
|
||||
dir, err := pkgDir(pkg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []string
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := e.Name()
|
||||
if strings.HasSuffix(name, "_test.go") {
|
||||
out = append(out, filepath.Join(dir, name))
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// pkgDir converts a Go package path like "./p/block" into a filesystem
|
||||
// directory. We assume the current working directory IS the module
|
||||
// root.
|
||||
func pkgDir(pkg string) (string, error) {
|
||||
pkg = strings.TrimPrefix(pkg, "./")
|
||||
if pkg == "" || strings.HasPrefix(pkg, "/") {
|
||||
return "", fmt.Errorf("invalid pkg path %q (must be relative to module root)", pkg)
|
||||
}
|
||||
if _, err := os.Stat(pkg); err != nil {
|
||||
return "", fmt.Errorf("pkg dir %q: %w", pkg, err)
|
||||
}
|
||||
return pkg, nil
|
||||
}
|
||||
|
||||
// rewriteFile rewrites every `[]byte{...}` literal in file whose decoded
|
||||
// payload matches any pair's expected bytes. Returns the number of
|
||||
// rewrites.
|
||||
func rewriteFile(file string, pairs []pair, dryRun, verbose bool, annotateTag string) (int, error) {
|
||||
src, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, file, src, parser.ParseComments)
|
||||
if err != nil {
|
||||
// Best-effort: if parsing fails (e.g. syntax error introduced
|
||||
// by earlier run), skip.
|
||||
if verbose {
|
||||
fmt.Printf(" parse %s: %v (skipping)\n", file, err)
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Walk all CompositeLit nodes whose Type encodes []byte (either
|
||||
// the literal `[]byte` or an alias resolving to it). We accept
|
||||
// `[]byte` and `[]uint8`.
|
||||
type rewrite struct {
|
||||
startOff int // byte offset where replacement starts
|
||||
endOff int // byte offset (exclusive) where replacement ends
|
||||
newBytes []byte // replacement bytes (when !isString)
|
||||
newStr string // replacement string literal (when isString)
|
||||
isString bool // true if rewrite targets a string literal
|
||||
oldHash string // for logging
|
||||
}
|
||||
var rewrites []rewrite
|
||||
|
||||
// Build a quick map of expected → actual for O(1) lookup.
|
||||
wantBytes := make(map[string][]byte, len(pairs))
|
||||
wantStrings := make(map[string]string, len(pairs))
|
||||
for _, p := range pairs {
|
||||
switch p.kind {
|
||||
case "bytes":
|
||||
wantBytes["b:"+string(p.expected)] = p.actual
|
||||
case "string":
|
||||
wantStrings["s:"+p.expectedRaw] = p.actualRaw
|
||||
}
|
||||
}
|
||||
|
||||
ast.Inspect(f, func(n ast.Node) bool {
|
||||
// Byte slice composite literals.
|
||||
if cl, ok := n.(*ast.CompositeLit); ok && cl.Type != nil && isByteSliceType(cl.Type) {
|
||||
got, ok := decodeASTByteList(cl)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
actual, found := wantBytes["b:"+string(got)]
|
||||
if !found {
|
||||
return true
|
||||
}
|
||||
if bytes.Equal(got, actual) {
|
||||
return true
|
||||
}
|
||||
lbrace := fset.Position(cl.Lbrace).Offset
|
||||
rbrace := fset.Position(cl.Rbrace).Offset
|
||||
rewrites = append(rewrites, rewrite{
|
||||
startOff: lbrace,
|
||||
endOff: rbrace + 1,
|
||||
newBytes: actual,
|
||||
oldHash: fmt.Sprintf("len=%d", len(got)),
|
||||
})
|
||||
return true
|
||||
}
|
||||
// String literals (e.g. base64-encoded byte buffers).
|
||||
if bl, ok := n.(*ast.BasicLit); ok && bl.Kind == token.STRING {
|
||||
unquoted, err := strconv.Unquote(bl.Value)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
actual, found := wantStrings["s:"+unquoted]
|
||||
if !found {
|
||||
return true
|
||||
}
|
||||
if unquoted == actual {
|
||||
return true
|
||||
}
|
||||
start := fset.Position(bl.Pos()).Offset
|
||||
end := fset.Position(bl.End()).Offset
|
||||
// Render quoted Go string literal with the new content.
|
||||
replacement := strconv.Quote(actual)
|
||||
rewrites = append(rewrites, rewrite{
|
||||
startOff: start,
|
||||
endOff: end,
|
||||
newStr: replacement,
|
||||
oldHash: fmt.Sprintf("strlen=%d", len(unquoted)),
|
||||
isString: true,
|
||||
})
|
||||
return true
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if len(rewrites) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Sort descending by start offset so we can splice in-place
|
||||
// without invalidating earlier offsets.
|
||||
sort.Slice(rewrites, func(i, j int) bool {
|
||||
return rewrites[i].startOff > rewrites[j].startOff
|
||||
})
|
||||
|
||||
if verbose {
|
||||
fmt.Printf(" %s: %d literal(s) to rewrite\n", file, len(rewrites))
|
||||
}
|
||||
|
||||
out := make([]byte, len(src))
|
||||
copy(out, src)
|
||||
for _, rw := range rewrites {
|
||||
var replacement string
|
||||
if rw.isString {
|
||||
replacement = rw.newStr
|
||||
} else {
|
||||
replacement = renderByteLiteral(rw.newBytes, computeIndent(out, rw.startOff))
|
||||
}
|
||||
out = append(out[:rw.startOff], append([]byte(replacement), out[rw.endOff:]...)...)
|
||||
}
|
||||
|
||||
// Optionally annotate: walk the literals once more from a fresh
|
||||
// parse and prepend the comment if not already present. To keep
|
||||
// the tool simple and deterministic, we skip annotation here —
|
||||
// the regenerator's commit message and this tool's docstring
|
||||
// document the wire-format change.
|
||||
_ = annotateTag
|
||||
|
||||
if dryRun {
|
||||
fmt.Printf("[dry-run] %s: would rewrite %d literal(s)\n", file, len(rewrites))
|
||||
return len(rewrites), nil
|
||||
}
|
||||
if err := os.WriteFile(file, out, 0o644); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(rewrites), nil
|
||||
}
|
||||
|
||||
// isByteSliceType returns true for `[]byte` or `[]uint8`.
|
||||
func isByteSliceType(e ast.Expr) bool {
|
||||
arr, ok := e.(*ast.ArrayType)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if arr.Len != nil {
|
||||
return false
|
||||
}
|
||||
ident, ok := arr.Elt.(*ast.Ident)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return ident.Name == "byte" || ident.Name == "uint8"
|
||||
}
|
||||
|
||||
// decodeASTByteList decodes the elements of a []byte composite literal
|
||||
// into raw bytes. Returns (bytes, true) on success. Returns false if
|
||||
// the literal contains an unparseable element (an identifier reference,
|
||||
// a slice expression, etc.). Accepts:
|
||||
// - integer literals: 0x00, 42, 0o77
|
||||
// - char literals: 'n' (decoded as rune)
|
||||
// - string literals: "abc" (decoded as UTF-8 bytes)
|
||||
func decodeASTByteList(cl *ast.CompositeLit) ([]byte, bool) {
|
||||
out := make([]byte, 0, len(cl.Elts))
|
||||
for _, elt := range cl.Elts {
|
||||
bl, ok := elt.(*ast.BasicLit)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
switch bl.Kind {
|
||||
case token.INT:
|
||||
v, err := strconv.ParseUint(bl.Value, 0, 8)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
out = append(out, byte(v))
|
||||
case token.CHAR:
|
||||
r, _, _, err := strconv.UnquoteChar(bl.Value[1:len(bl.Value)-1], '\'')
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
if r > 0xff {
|
||||
return nil, false
|
||||
}
|
||||
out = append(out, byte(r))
|
||||
case token.STRING:
|
||||
unquoted, err := strconv.Unquote(bl.Value)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
out = append(out, []byte(unquoted)...)
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
return out, true
|
||||
}
|
||||
|
||||
// computeIndent returns the whitespace from the start of the line up
|
||||
// to offset. Used to indent multi-line literal content.
|
||||
func computeIndent(src []byte, offset int) string {
|
||||
lineStart := offset
|
||||
for lineStart > 0 && src[lineStart-1] != '\n' {
|
||||
lineStart--
|
||||
}
|
||||
// Indent is everything up to the first non-tab/space character
|
||||
// on the line, OR up to offset, whichever is shorter.
|
||||
indent := []byte{}
|
||||
for i := lineStart; i < offset; i++ {
|
||||
ch := src[i]
|
||||
if ch == '\t' || ch == ' ' {
|
||||
indent = append(indent, ch)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return string(indent)
|
||||
}
|
||||
|
||||
// renderByteLiteral renders a `{ 0x.., 0x.., ... }` body with one
|
||||
// byte per line at 16 bytes per group, indented one tab deeper than
|
||||
// the literal's opening `{`.
|
||||
func renderByteLiteral(b []byte, indent string) string {
|
||||
if len(b) == 0 {
|
||||
return "{}"
|
||||
}
|
||||
innerIndent := indent + "\t"
|
||||
var sb strings.Builder
|
||||
sb.WriteString("{\n")
|
||||
for i, v := range b {
|
||||
if i%16 == 0 {
|
||||
sb.WriteString(innerIndent)
|
||||
}
|
||||
fmt.Fprintf(&sb, "0x%02x,", v)
|
||||
if i == len(b)-1 || (i+1)%16 == 0 {
|
||||
sb.WriteByte('\n')
|
||||
} else {
|
||||
sb.WriteByte(' ')
|
||||
}
|
||||
}
|
||||
sb.WriteString(indent)
|
||||
sb.WriteByte('}')
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Defensive: any unexpected runtime panic should be surfaced
|
||||
// with the tool's own banner.
|
||||
if false {
|
||||
_ = errors.New("unused")
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -6,8 +6,8 @@ package message
|
||||
import (
|
||||
"time"
|
||||
|
||||
compression "github.com/luxfi/compress"
|
||||
"github.com/luxfi/metric"
|
||||
compression "github.com/luxfi/compress"
|
||||
)
|
||||
|
||||
var _ Creator = (*creator)(nil)
|
||||
|
||||
@@ -10,10 +10,10 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
compression "github.com/luxfi/compress"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/node/proto/p2p"
|
||||
"github.com/luxfi/timer/mockable"
|
||||
compression "github.com/luxfi/compress"
|
||||
)
|
||||
|
||||
func Test_newMsgBuilder(t *testing.T) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user