Node has ZERO .proto files and ZERO .pb.go — the wire is hand-written ZAP schemas
(proto/{platformvm,vm,p2p,sync}/*_zap.go). The buf/protoc tooling around it was
orphaned: removed proto/{buf.yaml,buf.gen.yaml,Dockerfile.buf,buf.md,README.md},
scripts/protobuf_codegen.sh, the Taskfile generate-protobuf + check-generate-protobuf
targets, ci.yml buf-lint + check_generated_protobuf jobs (the latter ran the deleted
script → would fail CI), and the buf-lint.yml workflow. defaultPatch 10→11 (dev
version string; image already correct via ldflags). Binary unchanged — tooling/CI only.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
arm64 validators (spark GB10, arm64 nodes generally) could not pull
ghcr.io/luxfi/node — the Docker workflow built linux/amd64 only. The
Dockerfile already cross-compiles via TARGETARCH with CGO_ENABLED=0, so
buildx produces arm64 on the amd64 lux-build pool without QEMU-emulating
the compile. Adds a workflow_dispatch `tag` input to (re)build an existing
release tag (e.g. v1.32.1) as a multi-arch manifest.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The routing + insteadOf fixes got goreleaser onto lux-build with git auth,
but it still failed: 'could not read Password ... exit 128' fetching
luxfi/{container,crypto,database,proto}. github.token is repo-scoped and
cannot read OTHER private luxfi repos. Switch to the same cross-org PAT
docker.yml uses (GH_TOKEN/UNIVERSE_PAT) so cross-repo module fetches
authenticate. node already carries UNIVERSE_PAT as a repo secret.
goreleaser shells out to go build, which fetches private luxfi modules
(container, crypto, proto, ...). setup-go-for-project sets GOPRIVATE but
only wires git auth when passed a github-token (build.yml passes none), so
the fetch hit 'could not read Username for https://github.com' and the job
failed even on lux-build. Add the same explicit 'Configure Git for private
modules' step ci.yml uses (git config url.insteadOf with github.token).
This is the real root cause of the 'Build on supported platforms' red-X;
the routing-to-lux-build commit was necessary but not sufficient.
The v1.30.28 docker.yml run published node:v1.30.28 (sha-c36527c) fine on
the in-cluster lux-build ARC pool, but two sibling workflows red-X'd:
- build.yml 'goreleaser' (runs on every push)
- release.yml 'validate-version' (cascaded skips to every platform build)
Root cause: both pinned runs-on: ubuntu-latest. This org disables
GitHub-hosted runners (NO GITHUB BUILDERS) — ubuntu-latest jobs never get a
runner and fail at provisioning, not at compile. Source is sound:
GOWORK=off CGO_ENABLED=0 go build ./... is clean and the cross-layer
dexsettle_guard test passes (extras.DexSettleActivationTime ==
dex.DexSettleActivationTime == 1766704800 = 2025-12-25T23:20:00Z).
Route goreleaser, validate-version, and create-release to lux-build (the
org-scoped autoscalingrunnerset in lux-k8s that already builds the image).
GoReleaser cross-compiles every GOOS/GOARCH from one linux/amd64 host with
CGO disabled, so a single amd64 runner suffices.
(The per-platform binary reusables build-{ubuntu,linux,macos,win}-* still
pin classic [self-hosted,linux,amd64]/windows-2022 labels — a pre-existing
routing issue tracked separately; the node *image* path is unaffected.)
The evo classic self-hosted runner is offline and GitHub-hosted runners are
billing-blocked (and disallowed by the NO-GITHUB-BUILDERS policy). Point the
image build at the lux-build autoscalingrunnerset (lux-k8s, amd64 DOKS, DinD)
— it serves the whole luxfi org and matches on the scale-set name. No node
binary/dep change vs v1.30.19; this is a build-infra patch.
Replaces non-canonical scale-set / org-prefixed labels with the
existing labels every arcd host registers with. Matches evo for
amd64 and spark for arm64. No new labels added.
The zap_native package was relocated out of this repo to
github.com/luxfi/proto/zap_native (commit 9ba108d4b "relocate zap_native
import paths to proto/zap_native"; node now imports it as a dependency,
pinned luxfi/proto v1.3.4). zap-audit.yml still grepped/cd'd into
vms/platformvm/txs/zap_native/, a directory that no longer exists here —
so the workflow could never run its gates against real code, and a YAML
block-scalar bug (a column-1 line inside `run: |`) made it fail to parse
at 0s on every push.
The four invariants it enforced (AddressList.At non-production,
ChainsList/ValidatorsList MustVerify, Owner-bearing SyntacticVerify) are
preserved at the source: luxfi/proto/zap_native/audit_test.go contains
the Go-test mirrors (TestAuditGate_*) that run in luxfi/proto's CI, next
to the code they guard. The audit belongs in the repo that owns the code,
not here pointing at a phantom path. Not a weakened gate — a dead
workflow removed; coverage unchanged.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Blue batch 5 v3.7 closes the 5 target gaps from Red round 5 follow-up:
1. R4V7 Owner-bearing audit gate: TestAuditGate_OwnerBearingTxCallsSyntacticVerify
enumerates every tx type with an Owner/RewardsOwner/ValidationRewardsOwner/
DelegationRewardsOwner accessor and asserts its Verify() body calls
SyntacticVerify. CI mirror: owner-bearing-syntacticverify-gate job
in .github/workflows/zap-audit.yml. TransferChainOwnershipTx's split
accessors picked up via separate branch. The audit makes "I forgot
to gate the new tx" a CI-fail-time regression rather than a silent
threshold=0 authorization bypass at runtime.
2. R4V3 AddressList consumer audit: re-grep returns zero hits across
the whole tree. Documented inline at tx_verify.go header with the
reproducer grep. No regressions since batch 5 v3.5.
3. ChainsList MaxChainsPerL1 = 16: hard cap enforced in
ChainsListView.MustVerify. New ErrTooManyChains typed error.
Matches the multi-chain L1 spawn use case (P + X + EVM + small
application stack) plus headroom; prevents a hostile encoder
from forcing the executor through quadratic walk at admission.
Coverage: TestChainsList_MustVerify_RejectsOverCap +
TestChainsList_MustVerify_AcceptsAtCap.
4. ValidatorsList MustVerify (5 floor invariants):
- Len() <= MaxValidatorsPerL1 (1024): cap matches practical
upper bound on initial-validator sets.
- Weight > 0: lifts the per-validator gate from tx_verify.go
onto the list-level MustVerify so it's grep-able and pure
orchestration on the tx side.
- BLSPubKey not all-zero: structural floor before R6V3 pairing.
- BLSPoP not all-zero: structural floor before R6V3 pairing.
- RegistrationExpiry > 0: parallel of ErrZeroExpiry on
RegisterL1ValidatorTx (unix timestamp can never be zero).
New typed errors: ErrTooManyValidators, ErrValidatorBLSPubKeyZero,
ErrValidatorBLSPoPZero, ErrValidatorRegistrationExpiryZero. Wired
into CreateSovereignL1Tx.Verify and ConvertNetworkToL1Tx.Verify
before the expensive BLS pairing walk so cheap structural floor
fires first. New audit gate (test + CI):
TestAuditGate_ValidatorsListEmbeddersCallMustVerify + workflow
job validatorslist-mustverify-gate. Coverage: 6 new MustVerify
tests (happy path + 5 floor-violation rejections + over-cap).
5. Bench refresh -benchtime=2s (M1 Max):
- Parse geomean (n=9): 7.50× vs v3.6 7.74× — within 4% noise.
- Build geomean (n=9): 1.03× vs v3.6 1.12× — within 9% noise.
- Allocs/op unchanged: Parse 1/24, Build 2 (down from 4-7).
- No regression > 5%. v3.7 changes fire entirely on the Verify()
path; Parse and Build are structurally untouched.
Full numbers in vms/platformvm/txs/bench_results/RESULTS.md.
All 4 audit gates pass locally:
- TestAuditGate_AddressListNoProductionConsumers
- TestAuditGate_ChainsListEmbeddersCallMustVerify
- TestAuditGate_ValidatorsListEmbeddersCallMustVerify (NEW)
- TestAuditGate_OwnerBearingTxCallsSyntacticVerify (NEW)
Full zap_native test suite: 0.715s, all green.
Red round 7 closes the activation gap so Neo's ZAPActivationUnix=0 image can
land without zombie txs from not-yet-implemented zap_native executors.
R7V5 (HIGH, mempool admission gate — Path A)
- vms/platformvm/network/zap_native_admission.go: new file. Wraps
TxVerifier with NewZapNativeAdmissionGate; refuses
*txs.CreateSovereignL1Tx (legacy struct dispatch hits stub at
standard_tx_executor.go:636) AND any ZAP-magic wire buffer whose
kind is CreateSovereignL1 (23), RegisterL1Validator (7), or
ConvertNetworkToL1 (22).
- vms/platformvm/network/network.go: New() now wraps the supplied
TxVerifier with the gate before installing into gossipMempool, so
every inbound tx routes through the gate first.
- Typed error ErrZapNativeNotYetExecutable for errors.Is matching.
- REMOVAL CHECKLIST documented inline so a future Blue knows to
delete the gate entry when an executor body lands.
- Tests in zap_native_admission_test.go: rejects legacy
CreateSovereignL1Tx; passes through legacy BaseTx /
RegisterL1ValidatorTx / ConvertNetworkToL1Tx (the working ones);
rejects ZAP-wire CreateSovereignL1 / RegisterL1Validator /
ConvertNetworkToL1; non-ZAP bytes pass; nil-safety.
R7V7 (HIGH, Verify() for two missing tx types)
- RegisterL1ValidatorTx.Verify(): BLS PoP pairing + zero-Expiry gate
(ErrZeroExpiry). RemainingBalanceOwnerID treated as optional v3
placeholder.
- ConvertNetworkToL1Tx.Verify(): same per-validator walk as
CreateSovereignL1Tx (non-empty Validators, Weight>0, BLS PoP
pairing).
- Tests in r7v7_register_convert_verify_test.go: rejects bad BLS
PoP / malformed BLS pubkey / zero Expiry / zero Validators / zero
Weight; accepts valid; adversarial wire-buffer tampering for both
tx types (zero out Expiry / Weight in-place and re-Wrap).
R7V8 (MEDIUM, MustVerify rename + CI gate)
- chains_list.go: ChainsListView.Verify renamed to MustVerify so
the per-list gate is grep-able from CI. The previous Verify()
name collided with the tx-level Verify() convention.
- tx_verify.go: CreateSovereignL1Tx.Verify call site updated.
- r6_verify_test.go: TestChainsListView_Verify_StandaloneEntries
renamed + updated to use .MustVerify().
- audit_test.go: new TestAuditGate_ChainsListEmbeddersCallMustVerify
enumerates tx types with a Chains() accessor returning ChainsList
and confirms tx_verify.go has a corresponding Verify() body that
calls .MustVerify().
- .github/workflows/zap-audit.yml: new chainslist-verify-gate job
mirroring the local audit_test.go invariant.
Test results (GOWORK=off, race enabled):
./vms/platformvm/network/... PASS (8 new + all existing)
./vms/platformvm/txs/zap_native/ PASS (8 new + all existing)
./vms/platformvm/txs/executor/ PASS
./vms/platformvm/txs/ PASS
./vms/platformvm/block/... PASS
R6-4 (RESERVED zero-gate)
- ChainEntry RESERVED bytes [56..64) gated by ChainsListView.Verify.
- New OffsetChainEntry_Reserved=56 constant + ErrReservedNonZero typed err.
- Writer already zero-pads; gate prevents adversary smuggling state inside
what consensus considers empty. Pins the upgrade-safe invariant before
any v4 parser attaches meaning to those bytes (no silent wire-fork).
- Tests: TestChainsList_Verify_RejectsNonZeroReserved (per-byte sweep of
all 8 reserved bytes, each flipped individually -> all reject),
TestChainsList_Verify_RejectsNonZeroReserved_TopByte (top byte 0x01).
R6-6 (AddressList.At CI audit gate)
- .github/workflows/zap-audit.yml: grep-based gate on PR + push to
main/dev. Fails if any new production caller of AddressList.At()
appears outside the allowlist (_test.go, tx_verify.go, owner.go,
audit_test.go).
- vms/platformvm/txs/zap_native/audit_test.go: local mirror so
`go test ./vms/platformvm/txs/zap_native/` reproduces CI behavior.
Verified locally: clean -> PASS; injected violator -> trips correctly.
R6-2 (cross-blob aliasing design decision)
- Documented-allowance path: aliasing is ALLOWED. Wire layer must not
reject overlapping (rel, len) ranges; identity is (VMID, BlockchainID),
not Name() bytes.
- Full CONTRACT block on ChainsListView: (1) Name/FxIDs/GenesisData
return payload slices not identity, (2) chain identity is VMID +
BlockchainID, (3) consumers MUST NOT use returned bytes as dedup keys,
(4) returned slices are read-only.
- Forward path documented: if future feature requires non-overlap, add
ChainsListView.VerifyNonOverlappingRanges() and wire it from tx.Verify.
- Test: TestChainsList_AllowsOverlappingRanges_DocumentedContract -
patches chain[1].NameRel to alias chain[0]'s slice, proves Verify
accepts AND Name(0) == Name(1) byte-equal, AND VMID(0) != VMID(1).
go test -race ./vms/platformvm/txs/zap_native/...: ok 1.4s
Builds on top of v3 commit d5c305d440.
GitHub-hosted macos-13 queues block the release pipeline for hours.
The build is already pure Go cross-compile (CGO_ENABLED=0 GOOS=darwin
GOARCH=$arch) so there's no reason to use a Mac runner. Route through
the self-hosted lux-build ARC pool (fast, plentiful) instead.
Replace 7z (not on ubuntu) with apt-installed zip in the same step.
Output filename + artifact name are unchanged.
The v1.27.18 diagnostic confirmed luxfi org GH_TOKEN secret resolves to
length=0 at runtime (visibility is set to 'all' but the actual value
appears unset/expired). UNIVERSE_PAT is set at the repo level and has
the same cross-repo read scope we need for private luxfi/* deps. Try
GH_TOKEN first, fall back to UNIVERSE_PAT, fail fast with a clear
error if both are empty.
Also explicitly disable docker/metadata-action's latest=auto flavor so
the :latest floating tag truly never gets emitted (the prior run showed
the action silently injecting it even with no type=raw rule).
The repeated 'terminal prompts disabled' fatal at go mod download in
v1.27.15..v1.27.17 looked like the BuildKit secret mount was producing
an empty /run/secrets/ghtok file. Add a pre-build step that fails fast
if secrets.GH_TOKEN does not resolve at workflow run time (org-secret
visibility=all, but ARC runner ephemeral identities have surprised us
before). The token value is never echoed — only its byte length.
Default workflow GITHUB_TOKEN only has read access to the running repo
(luxfi/node), so go mod download fails on private cross-repo deps such
as luxfi/corona. Switch the BuildKit ghtok secret to the org-level
GH_TOKEN (a PAT with org-wide read scope) so the Dockerfile's git
url-rewrite picks up every luxfi/* module the build needs.
Avalanche/legacy genesis JSONs (testnet, mainnet) set initialAmount as the
sum of unlockSchedule (two views of one total). The current builder emits
both an immediately-spendable UTXO for initialAmount AND one UTXO per
unlock entry — double-minting the allocation.
Devnet-style configs set initialAmount with an empty unlockSchedule and
need the single UTXO to be emitted.
One semantic, one UTXO: when unlockSchedule is non-empty, skip the
initialAmount UTXO (the schedule already covers the total). When
unlockSchedule is empty, emit the initialAmount UTXO as before.
Also:
- ci/docker: switch back to self-hosted `lux-build` ARC pool (DOKS hanzo-k8s)
- ci/docker: drop floating `:latest` tag — semver/sha-only policy
The lux-arm64-runners EKS cluster is unreachable (i/o timeout on
control plane); no Docker builds since the runner pool dropped.
Pinning to ubuntu-latest until ARC is restored. Switch back to
lux-build in a follow-up once the EKS cluster is back up.
luxfi/corona went private; the Dockerfile builder's `go mod download`
was failing with "could not read Username for 'https://github.com'"
on the ARC runner. Adds a BuildKit secret mount (required=false so
local builds without the secret still work when all deps are public)
and rewrites `git config url.insteadOf` to inject the token only for
the download step. Token is unset after to keep the layer clean.
The workflow passes ${{ secrets.GITHUB_TOKEN }} as the `ghtok` secret;
the default GITHUB_TOKEN has repo:read for the runner's repository,
which is sufficient for luxfi/corona since it's in the same org.
Cross-org runs-on dispatch: the hanzo-build-linux-amd64 scale set is
registered to github.com/hanzoai and does NOT pick up jobs from
github.com/luxfi — the four queued v1.27.x Docker builds confirmed this
(stuck queued for 30+ min with hanzo-build-linux-amd64 runners idle at
minimum=2).
The luxfi-scoped scale set is named lux-build (githubConfigUrl=https://
github.com/luxfi, max 20). Switch docker.yml, build-linux-binaries.yml,
and the ubuntu release builders to it.
Next tagged release (v1.27.5+) will dispatch correctly. The four pending
v1.27.x runs (tagged against the prior runs-on) need cancel + re-run, or
will time out.
- build-linux-binaries.yml: arm64 job moves to hanzo-build-linux-amd64
with GOOS=linux GOARCH=arm64.
- build-ubuntu-arm64-release.yml: both jammy/focal jobs cross-compile
on the amd64 self-hosted runner.
- build-macos-release.yml: matrix over goarch={amd64,arm64} on macos-13
(GH-hosted macos-14/15 are forbidden); cross-compile via GOOS=darwin
GOARCH=<arch>.
- build-and-test-mac-windows.yml: pin macos-latest -> macos-13.
- ci.yml: drop macos-14 from CI matrix (use macos-13).
- actionlint.yml: drop hanzo-build-linux-arm64 + ubuntu-24.04-arm from
the self-hosted-runner whitelist.
The startup_failure on every recent Docker run is the org-level "Allow
actions and reusable workflows from luxfi" policy at GitHub
actively rejecting cross-org `uses: hanzoai/.github/.../docker-build.yml`
calls. Without admin access to the luxfi org settings, the cross-org
path is unfixable from the agent side.
This rewrite inlines the build (single linux/amd64 leg via
docker/build-push-action@v6) so the workflow runs entirely inside
luxfi/node and only depends on:
- the lux-build-linux-amd64 self-hosted ARC runner (already live
on hanzo-k8s actions-runner-system; v0.2.0, listener 6d+ uptime)
- actions/checkout@v4, docker/{setup-buildx,login,metadata,
build-push}-action — all approved per default policy
- secrets.GITHUB_TOKEN (auto-injected; no `secrets: inherit` needed)
- secrets.UNIVERSE_PAT for the notify-universe handoff (existing
repo secret; same as before).
Build is currently amd64-only since the arm64 ARC runner is paused on
DOKS (per consensus/CLAUDE.md memory). Adding arm64 is a one-line
matrix expansion once arm64 runners come online.
The reusable hanzoai/.github/docker-build.yml requires cross-org callers
to override runner labels — hanzoai org scale-sets aren't visible from
luxfi org. All v1.26.x CI runs have failed with startup_failure since
the upstream workflow added this requirement (~2026-05).
Per the reusable workflow's docstring:
Cross-org callers (luxfi/*, zooai/*, <tenant>/*) MUST override
these with their own org's ARC scale-set labels.
Switch to lux-build-linux-{amd64,arm64} labels which exist on the
luxfi ARC scale-set.