main pinned consensus v1.36.9, which contains the silent-rebuild-storm bug that
took devnet down for four and a half days. Any network that upgraded from HEAD
would have wedged the same way.
The bug: buildBlocksLocked keeps pendingBuildBlocks after a failed build, and
proposervm truncates the block timestamp to a whole second — so every rebuild
inside that second re-mints an IDENTICAL blkID. consensus.AddBlock rejects it as
"already exists" and a bare `continue` skips Propose/RequestVotes entirely. The
block is never sent, never voted, never decided, while pending txs keep
re-notifying. Measured on devnet: one node re-built the same blkID 2,287 times
at height 512, with ZERO "proposed block to validators" and ZERO "finalized
block via quorum cert" lines across 200k log lines fleet-wide.
It also leaves a durable trap. The on-disk vote-once guard binds those storm
blkIDs to heights that can never be re-proposed, so reserveSlotForSign refuses
every later block at those heights — the chain stays wedged even after the
binary is fixed. Devnet needed a hand-repaired guard on all five nodes to
recover; that repair has no code path yet (the voteguard docstring points at
engine/chain/lock_migration.go, which does not exist).
This is a REGRESSION, not a stale pin: v1.36.24 and v1.36.25 both pinned
v1.36.10 and finalize normally — devnet runs v1.36.25 today and is producing
blocks. v1.36.26, v1.36.27, v1.36.28 and HEAD went back to v1.36.9.
v1.36.10 is the latest consensus release, so this moves forward. Verified
against an EMPTY module cache (the cold path the in-cluster builder takes), and
./vms/... and ./chains/... build clean.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
TestCurrentRPCChainVMCompatible has been red since v1.36.11: every release
since then bumped defaultPatch without registering the version against
RPCChainVMProtocol 42, so version.Current was absent from its own
compatibility set. Protocol 42 has not moved across that range — the list was
simply never updated. Backfilled through v1.36.31 (v1.36.29 omitted: that tag
was pushed off a pre-rebase commit and deleted, it is not a release).
v1.36.30 carries the health fix but was tagged before this backfill, so its
tree still fails ./version. v1.36.31 is the green tag and the rollout target.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
v1.36.29 was tagged off a pre-rebase commit that never reached main and has
been deleted. Forward only: the release is v1.36.30, on main.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
/v1/health's `bootstrapped` check publishes chains.Nets.Bootstrapping()
verbatim as its message. Nets.chains is keyed by NET id, and the aggregate
appended the map key rather than asking the net which of its chains had not
converged. So every stuck primary-network chain surfaced as one ID,
11111111111111111111111111111111LpoYY — constants.PrimaryNetworkID, i.e.
ids.Empty — a "chain" the chain manager has never heard of. An operator who
went looking for it got "there is no chain with alias/ID", and N stuck chains
collapsed into a single indistinguishable entry that named none of them.
Measured on lux-devnet luxd-0 (v1.36.23), POST health.health:
"bootstrapped":{"message":["11111111111111111111111111111111LpoYY"],
"error":"chains not bootstrapped","contiguousFailures":3213}
The net owns the bootstrapping set, so the net is what can name those chains:
nets.Net grows Bootstrapping() []ids.ID and chains.Nets aggregates those
instead of its own keys. One place holds the set; one place reads it.
The existing TestNetsBootstrapping asserted the defect (require.Contains
bootstrapping, netID) — that assertion is why it survived review. It now
demands the chain ID and refuses the net ID.
This also cuts the first tag containing dada5a31, the GET /v1/health encoder
fix: apihealth.APIReply carries a time.Duration per check, jsonv2 has no
default representation for it, so every GET degraded to
{"healthy":…,"error":"health reply encode failed"} while the status code still
looked right. Reproduced here directly —
json: cannot marshal from Go time.Duration within "/checks/network/duration"
That fix landed on main on 2026-07-25 but no tag ever carried it: v1.36.28
points at 52de3948, seven commits behind. The fleet runs v1.36.2/23/24, all of
which predate it.
Tests (GOWORK=off CGO_ENABLED=0):
chains ok — TestNetsBootstrappingReportsChainsNotNets fails against the
old aggregate with exactly ids.Empty in the list
nets ok — TestNetBootstrappingNamesTheChains
health ok — the three handler tests fail against the jsonv2 encoder
(checks decode empty) and pass against encoding/json
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Two endpoints were answering with the right status code and an empty
truth, so every fleet looked instrumented while reporting nothing.
GET /v1/health encoded apihealth.APIReply through jsonv2, which has no
default representation for time.Duration — and every Result carries one.
The marshal therefore failed on every node, every time, and the handler
fell back to {"healthy":…,"error":"health reply encode failed"}. The
status code is written before the encode, so k8s probes and dashboards
stayed green while the body carried no checks at all. Measured on Zoo,
Hanzo and Pars mainnet, node v1.34.9 and v1.36.2 alike.
The type is defined with encoding/json tags and the POST (jsonrpc) path
already encodes it through that codec, which is why POST returned the
full check set while GET returned nothing. One wire type deserves one
encoder: GET now uses encoding/json too, so the two paths agree by
construction. encoding/json also maps invalid UTF-8 in check Details to
U+FFFD instead of failing, which is the behaviour the old jsontext
AllowInvalidUTF8 option was reaching for. The buffer stays: it keeps a
partial encode from shipping a torn body.
/v1/metrics answered 200 with zero bytes for the same shape of reason.
luxfi/metric resolves NewRegistry() to a no-op registry unless the
binary is built with -tags metrics (registry_noop.go, //go:build
!metrics), and only the `full` profile passed it. Images build with the
default `minimal` profile, so every metric in luxd registered into a
black hole while --api-metrics-enabled=true still advertised metrics as
on. Whether metrics are served is the runtime flag's call; a build tag
must not silently overrule it. `metrics` becomes a base tag on every
profile.
Verified: the three new handler tests fail against the jsonv2 encoder
(checks decode empty, error field nil — the exact production symptom)
and pass after. `go tool nm` shows (*registry).Gather absent from a
default build and present once the tag is set.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
M-Chain's committee is the validator set, and RunKeygen refuses a policy
needing more parties than the committee has. mainnet's mchain.json asked
for 7-of-10 against five genesis validators, so a custody key could never
have been generated — the failure would have surfaced the first time
someone tried to bridge, not at deploy.
Adds the invariant as a test over every network's built genesis, and
picks up luxfi/genesis v1.16.4 where the policies are sized to fit
(3-of-5 on mainnet/testnet/devnet, 2-of-3 on localnet).
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The build installed M-Chain's plugin binary as
tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t — the CB58 of the
retired `thresholdvm` identifier — while the genesis builder declares the
chain with constants.MPCVMID (qCURact1n41FcoNBch8iMVBwc9AWie48D118ZNJ5tBdWrvryS).
The plugin registry resolves a CreateChainTx's vmID to an implementation
by filename, so the two never met: M-Chain was declared in genesis and no
node could start it. The Dockerfile comment had already been renamed to
"mpcvm" without the CB58 being recomputed, which is why it read as correct.
Fixes all five sites (plugin build target, the build-verify list, the
runtime COPY, the comment, and publish_plugin_set.sh) and adds
genesis/builder/mchain_test.go, which pins the vmID literally and asserts
M-Chain is present in the height-0 chain set for mainnet, testnet and
local. A vmID is an immutable one-way door once a chain is created with
it, so it is now covered by a test rather than by five copies of a string.
Bumps luxfi/genesis to v1.16.3, where mchain.json states its quorum as
"policy": "7-of-10" instead of a bare mpcThreshold that reads as the
signer count to an operator and as the polynomial degree to a library.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Two things kept the cevm backend from ever reaching a running node.
The fetch pointed at luxcpp/cevm v0.19.0, which does not exist — the libs
publish from the private lux-private/cevm repo, and unauthenticated downloads
404 there. Point it at v0.51.10 in that repo and authenticate with the same
`ghtok` secret the private go modules and lux-accel already use. It stays
best-effort, so a build without the token behaves exactly as before.
Separately, the C-Chain plugin builds with CGO_ENABLED=0 and no tags, so it is
always the pure-Go EVM regardless of what luxd itself links — which is why
production images carry no cevm at all. That build now honours EVM_CGO and
EVM_TAGS, defaulting to today exact behaviour; EVM_CGO=1 EVM_TAGS=cevm is what
makes AutoEVM resolve to CppEVM. The libraries are already in this stage (the
plugin section shares the builder stage rather than starting a new FROM).
Defaults unchanged, so the in-flight v1.36.12 fleet build is byte-identical.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Each removed package was a SECOND way to do something that already has a
canonical first way. Zero importers workspace-wide; verified identical build
output before/after (pre-existing keyutil breakage unchanged).
- chains/rpc 1250 lines whose own header says 'This replaces lines
941-990' of chains/manager.go. The replacement never
happened; manager.go still registers handlers inline.
- node/config.go 265-line shadow of the canonical node/config/node/config.go.
- vms/mpcvm self-described 'thin backward-compatibility alias' wrappers
- vms/dexvm over github.com/luxfi/chains/*. No backwards compatibility,
only forwards perfection.
Simple made easy: one name, one home, one path.
- LAUNCH_CHECKLIST.md: pre-launch plan for v1.24.11, 154/154 boxes
unchecked, zero references; superseded by LLM.md/CHANGELOG.md/RELEASE.md
and NETWORKS.yaml + genesis/configs for the chain-ID/port tables.
- rename_app.sh, replace_imports.sh: spent one-off sed migrations. Both
are now actively harmful — replace_imports.sh would rewrite the live
github.com/luxfi/vm/manager import in vms/manager.go.
- gen_zoo_addr: stray 3.4MB darwin/arm64 build artifact committed at root;
source preserved at cmd/gen_zoo_addr/gen_zoo_addr.go (.gitignore already
covers the intended output path).
- .ci-status-check.md, .ci-trigger: dated CI-poke stamps, no workflow reads
them.
Also removed (untracked): .claude/worktrees/ agent scratch — a 73MB
whole-tree duplicate that polluted every grep. Detached via git worktree
remove; node HEAD cf8e4c6f51 was an ancestor of main, and the nested
lux/evm worktree HEAD 7156c44f6 is release tag v1.104.12, so no work lost.
Deleted the two fully-merged worktree-agent-* branches it left behind.
No Go source, config, manifest, or .github/ file was touched.
Disconnect/updateUptimeLocked now return (mutated bool, err); VM.Disconnected commits only when the flush wrote uptime state. Before StartTracking (bootstrap churn) and for non-validator peers the flush is a no-op, so the prior unconditional Commit was an empty full-write+fsync on every such disconnect. Skipping it is correct: every writer under stateLock commits its own diff, so no orphaned write depends on the disconnect path.
RED round-2 verdict: SHIP-READY (H1 phantom verified, mutated-gate correct, H2 pre-existing/not-worsened, block-height orthogonal). Isolated from the uncommitted staking-KMS WIP, which is quarantined on feat/staking-kms-native pending its own Blue->Red.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The uptime event-delivery plumbing (node/chain_router.go + chains/manager.go
blockHandler) dropped two invariants avalanchego's handler upholds — the
consensus lock and the peer version — each a mainnet-fleet crash. The uptime
tracker itself (vms/platformvm/uptime_tracker.go) is RED-cleared and UNCHANGED.
CRITICAL #2 — P-chain state race (concurrent map writes -> fatal):
chainRouter dispatched Connected/Disconnected on the peer-lifecycle goroutine,
where VM.Disconnected -> tracker.Disconnect (state.SetUptime) + state.Commit
(state.write) ran concurrently with the block acceptor's state.CommitBatch
(state.write) on the engine accept goroutine — no shared lock, so ordinary peer
churn triggered Go "concurrent map writes" and crashed the P-chain node.
Fix: one VM-owned stateLock serializes every commit of shared platform state
that originates outside the engine's lock-free VM.Accept call-out —
- block DECISION: block/executor Block.Accept/Reject hold &vm.stateLock
(supplied to executor.NewManager) around the whole acceptor visit;
- peer/lifecycle: VM.Disconnected and the onReady/onBootstrapStarted/Shutdown
Start/StopTracking uptime flushes hold vm.stateLock.
This is avalanchego's ctx.Lock invariant (accept serialized with
engine.Connected/Disconnected), scoped to the state the platform VM owns and
implemented at the VM: the Lux engine invokes VM.Accept as a lock-free call-out
(no ctx.Lock over accept exists) and a chain-agnostic blockHandler cannot reach a
per-VM accept lock, so routing "through the engine" is not feasible.
CRITICAL #1 — C-Chain nil-version panic on state sync:
blockHandler.Connected passed connector.Connected(ctx, nodeID, nil). proposervm
promotes Connected to coreth, whose state-sync peer tracker compares peer
versions; a nil version deref panics any C-Chain node running state sync (a fresh
join OR a validator rejoining after falling behind — the launch's core invariant).
Fix: plumb the REAL peer version through a versionedConnector capability —
chainRouter.Connected converts the node peer version (luxfi/node/version) to the
VM boundary type (luxfi/version = chain.VersionInfo) and delivers it via
blockHandler.ConnectedWithVersion -> connector.Connected. Audit: geth in-process
Connected is a no-op stub (nil-safe); xvm stores the pointer without deref
(nil-safe); the real coreth plugin derefs -> fixed by feeding the real version.
Tests (SDKROOT + CGO_ENABLED=1, -race):
- vms/platformvm/uptime_state_race_test.go: state.Commit (VM.Disconnected path)
concurrent with state.CommitBatch (acceptor path) under the shared lock — no
race/fatal; verified meaningful (an unlocked probe trips DATA RACE).
- chains/blockhandler_connected_version_test.go + node/chain_router_connected_version_test.go:
the real, converted version reaches the connector non-nil (dedup covered).
Uptime tracker 13/13 and the block/executor reward gate untouched and green;
go build ./... and go vet clean.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
All 5 mainnet validators read uptime=0.0000 / connected=null even for peers
connected 24h+, so prefersCommit (block/executor/options.go) always saw
0% < threshold and withheld staking rewards (~165M LUX gate).
Root causes (four, all fixed):
1. StartTracking never existed/called. The custom uptimeTracker was created
late (onReady) and never baselined validator records, so a long-running
validator's stored upDuration stayed 0 and CalculateUptimePercentFrom
returned 0/total = 0.
2. upDuration flushed only on Disconnect/Shutdown, so a continuously-connected
validator's persisted uptime never grew.
3. service.go left `connected` hard-nil ("IsConnected no longer exists").
4. ROOT: VM.Connected never fired. network.Connected -> chainRouter.Connected
only added to a connectedPeers set and logged; it never dispatched to chain
handlers, and blockHandler.Connected was a no-op. So tracker.Connect was
never called and the connected map was always empty — nothing to accrue.
Fix (faithful port of avalanchego snow/uptime.Manager semantics, luxfi pkgs):
- vms/platformvm/uptime_tracker.go: rewrite as a startedTracking-gated tracker.
StartTracking/StopTracking/StartedTracking/IsConnected added; CalculateUptime
folds the live connected session forward to now using the persisted
lastUpdated (reconstructed from the second-granular uptime.State encoding), so
a connected validator accrues uptime WITHOUT a Disconnect. Before tracking, a
validator is assumed online since its last update (avalanchego baseline);
after tracking, only genuine sessions accrue. Second-granular clock matches
storage. CalculateUptimePercentFrom is the clean upDuration/(now-from) form,
clamped to [0,1].
- vms/platformvm/vm.go: create+register the tracker at Initialize (so bootstrap
Connect events are captured), StartTracking(primary validators) at onReady,
StopTracking on re-bootstrap and Shutdown. Mirrors avalanchego's
onNormalOperationsStarted lifecycle.
- vms/platformvm/service.go: populate `connected` via tracker.IsConnected.
- node/chain_router.go: chainRouter.Connected/Disconnected now dispatch to every
registered chain handler (snapshot under lock, call outside).
- chains/manager.go: blockHandler forwards Connected/Disconnected to its chain's
VM (engineVM) exactly once (dedup set), so the P-chain uptime tracker — and
every VM's peer set — finally observes connectivity.
Consensus safety: prefersCommit is a per-node PREFERENCE feeding oracle-block
voting, not a deterministic value. Fixing the local uptime measurement (which
was uniformly 0) changes only which reward option each node prefers; consensus
still converges by preference voting. No staking/reward math changed.
Tests (vms/platformvm/uptime_tracker_test.go, -race green):
- TestUptimeTrackerLongRunningValidatorAccruesUptime: a 30-day validator reports
~100%. Uses only the shared Calculator API; run against the OLD tracker it
returns 0.0 (FAIL — the exact mainnet symptom), against the fix 1.0 (PASS).
- Continuously-connected-climbs, StartTracking-baselines, flush-on-disconnect,
never-connected-gets-zero, StopTracking-flushes, idempotent double-connect,
dedup, concurrency. block/executor (reward gate) suite green.
Follow-on (NOT in this commit): devnet 5-node uptime-climb verification ->
gated release -> owner-gated one-at-a-time mainnet roll. No image built, no
deploy, no live validator touched.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Propagates two just-published upstream fixes into the node (luxd) binary:
- consensus v1.36.7 → v1.36.9: the chain engine no longer os.Exit(1)-kills a
validator on a SetPreference orphan-refusal — it reconciles the VM to the
certified block when the diverged tip is provably uncertified, and halts
fail-closed only on a genuine double-finalization (v1.36.8). Also unifies
consensus's transitive geth pin to v1.20.1, matching node/evm (v1.36.9). The
new PreferenceReconciler VM interface is optional, so node's VMs are
unaffected (they take the non-fatal defer path); no code change required here.
- chains v1.7.7 → v1.7.9: every VM (aivm/keyvm/quantumvm/dexvm) now serializes
through native luxfi/zap struct-is-wire, with canonical-id hardening.
go.mod/go.sum only — dependency-graph resolution, no node code change. Transitive
indirects move forward within-major (bft, cockroachdb/errors, sentry-go,
prometheus/common, go-internal); no major-version bumps. geth stays v1.20.1
(already node's pin). Verified: GOWORK=off CGO_ENABLED=0 go build ./... clean.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The proposervm block wire stored the timestamp as Unix SECONDS (timestamp.Unix()), so the
sub-second window/granularity work was silently truncated to whole seconds when written to the
block: buildChild computed the proposer slot from sub-second time, but verify read back
second-resolution and computed a DIFFERENT slot → errUnexpectedProposer verify-drops at any
window < 1s. Store timestamp.UnixMilli() and read time.UnixMilli() so sub-second timestamps
round-trip and build/verify agree on the slot. Measured at a 100ms window: errUnexpectedProposer
frequent→0, 108→149 TPS, 2.8s→2.2s cadence, 5/5 byte-identical. Forward-only wire change (the
outer proposervm timestamp; the inner EVM block timestamp is independent). Remaining sub-second
floor is now the EVM targetBlockRate, not the proposervm.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Two coupled knobs for differentiated cadence — co-located D-Chain/DEX (fast) vs public
C-Chain (standard):
1. MinBlkDelay was hardcoded to the 1s DefaultMinBlockDelay in chains/manager.go, ignoring
its own --proposervm-min-block-delay flag. Now wired node.Config → ManagerConfig →
proposervm.Config (0 ⇒ 1s default). High-throughput nets set it low.
2. Block timestamps were Truncate(time.Second) at 4 sites, quantizing cadence to 1s
regardless of WindowDuration — so a sub-second window inflated slot numbers without finer
time resolution and could NOT produce blocks faster than 1/s. Truncation now tracks
proposer.TimestampGranularity() = min(1s, WindowDuration): mainnet (>=1s) keeps exact
1-second block times; sub-second windows get matching sub-second timestamps.
Measured: 1s window/delay → ~95-104 TPS, byte-identical 5/5 finality (unchanged from before,
no regression). NOTE: true sub-second cadence additionally requires a slot-handoff fix — at
100ms the slot recomputed in buildChild from a drifted 'now' can differ from the slot
timeToBuild scheduled, causing errUnexpectedProposer verify drops; that + multi-sender
saturation is the next step. These knobs are the necessary foundation.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Two changes, both node-local:
1. CADENCE — proposer.WindowDuration (the proposer-slot spacing, a validator's min build
delay = slot index x WindowDuration) was a hardcoded 5s const tuned for mainnet-scale
validator sets, flooring small/local block cadence at 5s per slot. It is now a
startup-configurable var (default 5s, unchanged for mainnet) set via the new
--proposervm-window-duration flag → node.Config → ManagerConfig → proposervm.Config →
proposer.SetWindowDuration at VM init. Read by both the windower delay math and
TimeToSlot, so they stay consistent. Measured on a 5-node strict-PQ net: 5s→1s took
sustained C-Chain from 44 TPS / 14s-per-block to 104 TPS / 3.8s-per-block, still 5/5
byte-identical finality.
2. SECURITY (cryptographer MEDIUM-1) — postForkCommonComponents.Verify now refuses a block
carrying a CLASSICAL secp256k1 proposer identity when the chain is strict-PQ
(StakingMLDSASigner set), UNCONDITIONALLY (before the consensusState==Ready gate, so it
also holds during bootstrap/state-sync). Fills the documented-but-unwired 'proposer'
SchemeGate site: the downgrade defense is now an explicit fail-closed in-perimeter gate,
not merely emergent from 20-byte NodeID collision-resistance + upstream enforcement.
Adds SignedBlock.HasClassicalProposer(). Mirrors contract.RefuseUnderStrictPQ.
Pins consensus v1.36.7 (consume-on-error build loop — kills the non-leader BuildBlock spin).
Block + proposervm VM tests green.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Under strict-PQ the canonical NodeID is ML-DSA-65-derived (config/node DeriveNodeID →
DeriveMLDSA), so the P-chain validator set and the proposervm windower are ML-DSA-keyed.
But proposervm signed post-fork blocks with the classical TLS leaf and derived the block
Proposer() via ids.NodeIDFromCert — a different value than DeriveMLDSA — so every signed
block (height ≥ 2) failed verifyPostDurangoBlockDelay with errUnexpectedProposer, was
dropped, and rebuilt in an unbounded storm (block 1 survived only because the unsigned
transition block skips the proposer comparison).
The block's offCert slot now carries a scheme-tagged proposer identity [scheme:1B|identity]:
0x90 classical (cert DER → NodeIDFromCert, ECDSA verify) or 0x42 strict-PQ (raw ML-DSA-65
pubkey → DeriveMLDSA(ids.Empty,pub), ML-DSA verify). ML-DSA signing/verification uses a
FIPS 204 §5.2 domain-separation context ('lux-proposervm-block-v1') so a proposer signature
can never be replayed as another ML-DSA message. proposervm.Config gains StakingMLDSASigner
/StakingMLDSAPub, plumbed from StakingConfig through ManagerConfig; exactly one scheme is
active per chain. K=1 nets are unaffected (transition + no-window blocks are unsigned).
Proven on a 5-node strict-PQ local net: sustained multi-block production, every block
built once, gossiped, and finalized byte-identical on all 5 (no automine). Pins consensus
v1.36.6 (engine logger + logged build-drops) which made this diagnosable.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The 9 VM lifecycle ops (Initialize, Linearize, SetState, CreateHandlers,
router AddChain, state-sync) were each bounded at a hardcoded 30s. A cold
coreth 'Regenerate historical state' pass after an unclean shutdown takes
67-134s on the mainnet C-Chain, blowing that budget → context cancelled
mid-init → VM marked failed → C-Chain route never registered → the recurring
post-restart 404 that required a second restart to clear. One named bounded
constant (10m) covers regen with margin while still surfacing a truly-hung VM.
Stop stays 10s. This is the third stacked restart-fragility bug after
skip-bootstrap frontier (v1.36.11) and rejoin discriminator (v1.36.13).
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The #66/#74 durable rejoin fix was INERT for the C-Chain — the chain whose 40h
mainnet freeze motivated it. chains/manager.go gated expectsStakedBeacons on
ids.IsNativeChain(chainParams.ID) (the blockchain ID), but ids.IsNativeChain only
matches the symbolic 111...C alias; every deployed C/X/Q has a HASH blockchain ID
(devnet 21HieZng, mainnet 2wRdZG), so isNativeChain was ALWAYS false and under the
production --skip-bootstrap=true the beacons were emptied -> a behind C-Chain named
its stale local tip the frontier and never caught up. Verified on devnet v1.36.12:
C-Chain wedged at height 0 ('using empty beacons for single-node mode').
Fix: discriminate on the VALIDATING NET (chainParams.ChainID == PrimaryNetworkID)
via new chainValidatesOnPrimaryNetwork — PrimaryNetworkID for C/X/Q, the sovereign
net ID for L2s. C/X/Q now keep staked beacons under --skip-bootstrap (peer-sync a
behind validator); L2s keep the empty-beacon single-node path. New regression
TestChainValidatesOnPrimaryNetwork_RealHashChainID exercises the real discriminator
with hash IDs; TestRED_EmptyStakedSetFailsSafe still green (forged-frontier gate intact).
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Captures the devnet-canary findings so the gated rollout is resumable: the
v1.36.11 EVM-plugin api skew (fixed in v1.36.12), the v1.36.2->v1.36.x P-Chain
codec migration (one-time wipe + proven cross-version re-bootstrap), the durable
rejoin mechanism, per-net RewardManager addresses/config shape, and the
one-at-a-time verify-tip roll protocol. Also folds in the pre-existing
RewardManager->DAO-Safe C-Chain design note.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
v1.36.11's node binary is correct (durable rejoin fix fe23efd1f2), but its image
baked VM plugins built against a stale luxfi/api: the C-Chain EVM plugin from
luxfi/evm@v1.104.8 and the D-Chain dexvm plugin from luxfi/dex@v1.5.15 both resolve
api v1.0.15, while the node pins api v1.0.16. api v1.0.16 APPENDED
InitializeResponse.Capabilities (uint64) for the Quasar-export handshake (api
1f2dc5a). The node decodes that field; the stale plugins never encode it, so
vms/rpcchainvm/zap/client.go fails every EVM VM Initialize with
'zap decode initialize response: unexpected EOF'. Native VMs (P/X/Q) are unaffected;
every EVM chain (C, D, and the L2 EVMs) fails to boot. Verified on devnet (published
v1.36.11 digest c3cf92a6): P/X re-bootstrap fine, C+D deterministically EOF.
Fix (image-only; node source unchanged beyond the version bump):
- EVM_VERSION v1.104.8 -> v1.104.9 (api v1.0.15 -> v1.0.16, indirect via luxfi/vm)
- force luxfi/api@v1.0.16 in the dexvm build stage (no dex release pins v1.0.16 yet)
- CHAINS_REF v1.7.6 already carries api v1.0.16 (the other 10 VMs were fine)
The api bump is code-free for plugins (chains v1.7.4->v1.7.5 adopted it go.mod-only).
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
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>
Durable root-cause fix for the behind-validator rejoin wedge (mainnet luxd-0:
C-Chain frozen ~40h at a stale height, only a manual chaindata wipe unstuck it).
Root cause: buildChain computed the bootstrap frontier-sync discriminator as
`expectsStakedBeacons := !m.SkipBootstrap && native && !platform` AND emptied the
beacon set whenever `m.SkipBootstrap`. Production validators hardcode
--skip-bootstrap=true (to skip the initial bootstrap WAIT), so a real multi-
validator native chain (C/X/Q) got expectsStakedBeacons=false + EMPTY beacons.
FrontierTip then reported FrontierNoBeacons ("nothing to sync to"), the node named
its STALE local last-accepted the network frontier, transitioned the VM to normal
operation there, and never fetched the gap from its 4 healthy peers — the wedge
survived every restart because --skip-bootstrap is persistent config. The entire
peer-sync/self-heal machinery (bootstrap_sync.go) was dead code under skip-bootstrap.
Fix: drive the discriminator from SYBIL PROTECTION (the true "real staked network"
signal, already wired into ManagerConfig), not --skip-bootstrap. A sybil-protected
native non-platform chain now keeps its staked beacon set and expectsStakedBeacons
even under --skip-bootstrap, so a behind validator always catches up from peers.
A genuine single-node / dev net runs sybil-protection OFF and still takes the
empty-beacon immediate-start path. A single-VALIDATOR staked net (self-only set) is
handled by a new FrontierTip hasExternalBeacons rule (placed after the P-ready gate),
so it immediate-starts without a Connecting hang while a >=2 set runs the quorum.
This matches the proven live remediation (flipping skip-bootstrap=false via the
.allow-bootstrap marker caught luxd-0 up to tip in ~90s) and preserves every RED
safety invariant (empty staked set still fails safe — TestRED_EmptyStakedSetFailsSafe).
Tests: TestChainExpectsStakedBeacons_SybilDrivesNotSkipBootstrap,
TestNodeBootstrap_SelfOnlyStakedSet_ReportsNoBeacons, and the headline
TestNodeBootstrap_BehindValidator_StakedSet_CatchesUpNoWipe (N-blocks-behind →
restart → catches up from peers to tip, no wipe). Full chains suite green.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Both executor backends re-declared a local SharedMemory interface with the
weak Apply(map[ids.ID]interface{}, ...interface{}) signature — but nothing
referenced it. The real atomic path uses Runtime.SharedMemory (= the narrow
atomic.SharedMemory: Apply(map[ids.ID]*atomic.Requests, ...database.Batch)).
Pure dead code; removed both. 28 platformvm packages green.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The fx set is a CLOSED sum type (secp256k1fx | nftfx | propertyfx), fixed at
compile time — no hot-loading, no genesis-configured fx. So the runtime
'which fx owns this value' dispatch needs no reflection: it is a total match on
the variant tag. Replaced reflect.TypeOf(val) + map[reflect.Type]int with:
- fxKindOf(val): a Go type switch over the closed set of fx primitive types
(compiler-checked exhaustive; lowers to a jump on the interface type tag),
returning the value's wire.TypeKind — the same family tag the wire envelope
already carries.
- FxIndex: a dense [16]int array indexed by that TypeKind (one bounds-checked
load; -1 = unregistered), filled by the SAME fx.(type) switch NewCustomParser
already ran — no separate reflect registration.
getFx (semantic verifier + tx_init) is now fxKindOf → array index: zero reflect,
zero map-hash, compile-time-checked. Deleted registerFxTypes + all
map[reflect.Type]int fields/params (parser, block/parser, vm, backend). Node-only
(uses already-imported utxo fx types + wire.TypeKind); no dep cascade.
Full xvm suite green (11 pkgs — secp/nft/property verify dispatch exercised).
This removes the LAST reflection from the X-chain tx/verify path.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Superset of transitive module hashes go resolves at v1.36.7; -mod=readonly build
+ full 155-package test suite green. No go.mod change.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
zap v1.2.2 (zero-copy SetBytes + Builder pool) + utxo v0.5.3 (pooled wire
builders + SetBytesFixed) cut X-chain tx wire composition ~1.9x (2551->1345ns,
37->19 allocs on the isolated composite; the X build path benefits proportionally
without touching P-chain parse, which stays at 705ns/3allocs). P/X/xvm/
components-lux suites all green against the new deps.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Supersedes v1.36.4's build recipe which used a build-time 'replace node => /build'
(forbidden: no local replace directives). Instead, chains v1.7.5 pins the real,
published node v1.36.4 tag, so the baked VM plugins resolve node the normal way —
Dockerfile just clones chains and builds; CHAINS_REF v1.7.4 -> v1.7.5; go.mod
chains v1.7.4 -> v1.7.5; genproto realigned. Zero replace directives anywhere.
tidy + -mod=readonly + cold 'go mod download all' clean.
Note: luxfi node/geth/utxo git tags are being periodically wiped by an external
tag-sync (root cause of the 'unknown revision' failures); all four pinned tags
(node v1.36.4, chains v1.7.5, utxo v0.5.1, geth v1.17.12) re-verified present
immediately before this build races the sync window.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The plugin stage cloned chains and built each VM's cmd/plugin standalone, which
resolved chains' pinned github.com/luxfi/node@v1.30.6 — a wiped/disjoint release
tag (325 orphan commits, no merge-base with main) absent from the remote, so the
hermetic build died with 'unknown revision v1.30.6' and the required bridgevm/
mpcvm/zkvm plugins went missing (FATAL).
Fix: inject 'replace github.com/luxfi/node => /build' (the exact vX.Y.Z node source
being built) into every /tmp/chains go.mod before building. This is what the
existing 'plugins in lockstep with the host node' intent actually requires — the
baked plugins now match the node they run in instead of an ancient pin. chains is
the main module during the plugin build, so node(/build)'s own require of chains
resolves back to /tmp/chains (main-module-wins) — no version fetch, no loop. Also
bumped CHAINS_REF default v1.7.2 -> v1.7.4 to match node's go.mod chains pin.
Verified: all 10 required plugins build cold (fresh GOMODCACHE, CGO_ENABLED=0) into
real binaries against local node v1.36.4.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The xvm codec kill depends on utxo/wire's TransferableOut/In + nftfx/propertyfx
envelopes, which were only in the local ~/work/lux/utxo working tree (pinned via
'replace github.com/luxfi/utxo => ../utxo'). That local-path replace works for a
local build but breaks the hermetic Docker/CI build ('open /utxo/go.mod: no such
file or directory'). Published those two additive wire commits as utxo v0.5.1
(clean ff on utxo main, +2 over v0.5.0) and pin it here — the exact code node was
built+tested against. Dropped the replace. Cold-cache 'go mod download all' is now
clean; xvm/components/lux/wallet tests green against v0.5.1.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The codec kill is complete. Every node VM (platformvm, xvm, warp, proposervm,
components/lux) and every chains app-chain VM (bridgevm, zkvm, mpcvm — now native
in chains v1.7.4) marshals via native ZAP struct-is-wire. Nothing imports
node/vms/pcodecs anymore, so the package — the last reflection/serialize-tag
dispatcher (a thin alias over proto/zap_codec's LinearCodec) — is deleted.
- rm vms/pcodecs + vms/pcodecs/pcodecsmock (zero consumers; verified whole tree).
- go.mod: chains v1.7.2 -> v1.7.4 (the pcodecs-free chains), precompile
v0.19.0 -> v0.19.1. Realigned genproto so the split googleapis/rpc module
resolves by longest-prefix (no monolith ambiguity); tidy clean, -mod=readonly
build clean.
- version -> v1.36.4 (constants.go defaultPatch, compatibility.json under the
same RPCChainVM protocol as v1.36.3 — no protocol change, only a codec rip).
There is one and only one way to put a struct on the wire: ZAP.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Tail of the xvm codec kill (ddb3fbca93): the X-chain wallet builder + signer
now rebuild signed wire bytes as unsigned ‖ fx credential envelopes over the
native luxfi/utxo/wire form, and components/lux parses fx Inputs from their wire
envelope by concrete type. No linearcodec, no reflection on the wallet path.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The non-codec main changes (Dockerfile EVM_VERSION v1.104.8, chains/manager
GetContext size-chunking for behind-validator resync + Quasar EXPORT frontier
bridge, warp/signature, rpcchainvm/zap client) that the branch lacked. The
codec-era main files (codec.go/parse.go/tx.go etc.) are intentionally
superseded by this branch's native-ZAP struct-is-wire — not reintroduced.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
RED-verified consensus-safety fix: zap.Parse truncates to the header size
field, so a block buffer with extra tail bytes wraps the SAME message but
ID=hash(bytes) differs — a block-hash malleability / fork vector. setID now
rejects msg.Size() != len(bytes) with ErrExtraSpace. (The P-chain TX envelope
already guards this at tx.go:66; only block Parse had the gap.)
Renamed codec_determinism_test.go -> block_determinism_test.go and converted
its assertions to native ZAP: New*Block + Parse(b) (no Codec), native golden
AbortBlock bytes (65B: zap header + kind/parent/height/time object), byte-
stability + BlockID-stability + trailing-bytes-rejected. Block + txs green.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
defaultMinor/Patch -> 36/3 (Dockerfile injects no version ldflags, so the
default IS the released binary's self-reported version; v1.36.0-2 shipped
self-reporting 1.32.11). v1.36.3 registered under RPCChainVM protocol 42.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Node builds + full test sweep green (155 ok / 0 FAIL) against PUBLISHED
modules only: consensus v1.36.1, zap v1.2.0, utxo v0.3.7. The local zap/utxo
replaces are gone; the upstream nftfx/propertyfx wire (utxo e790d39) ships as
v0.3.8 with the xvm struct-is-wire patch, which is the only remaining pcodecs
consumer and lands in the next release.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Route registration, client URL builders, and log lines all move to /v1/
(wallet primary api, chains/rpc handler_manager+chain_integration, xvm
client+wallet_client, xsvm api client, multi-network example, manager logs).
Zero /ext/ literals remain. One endpoint namespace, no transition alias.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Consumes the upstream fx-wire addition (utxo commit e790d39: TypeKindNFT/
Property + 6 shapes + NextEnvelope) needed for the xvm struct-is-wire
migration. Local replace like zap; publish as utxo v0.3.8 with the node
release.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
v1.36.1 bundled staged-but-runtime-untested ZAP work (api v1.0.15->v1.0.16,
vm v1.2.5->v1.2.7, EVM v1.104.7->v1.104.8, platformvm sole-codec cutover,
rpcchainvm Quasar-export plugin-boundary wiring). That bump breaks VM
initialization on EVERY chain at boot:
failed to initialize VM: zap decode initialize response: unexpected EOF
— a node<->plugin ZAP handshake mismatch (the plugins were not synced to the
new api/vm ZAP wire). Mainnet would not boot on v1.36.1.
v1.36.2 drops that unfinished ZAP transition and ships ONLY the orthogonal
consensus finality fix (v1.36.1 — close the intermediate-ancestor load-livelock,
RED-SHIP 0 crit/high/med) on the known-good v1.36.0 dependency set (api v1.0.15,
vm v1.2.5, EVM v1.104.7). Boots clean AND carries the finality fix. luxd builds
green (-mod=mod). The ZAP Quasar-export / codec cutover ships separately once the
plugins are synced to the api/vm bump and it is runtime-validated.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
All 3 warp codec registries deleted (codec.go, message/codec.go,
payload/codec.go). Every dispatched type now carries an EXPLICIT 1-byte
discriminator at object offset 0 — the invariant is DATA, not registration
order:
warp wkind: 0x00 BitSetSignature, 0x01 CoronaSignature,
0x02 EncryptedWarpPayload, 0x03 HybridBLSCoronaSignature,
0x04 TeleportMessage, 0x05 TeleportTransferPayload,
0x06 TeleportAttestPayload (ids = old registration order)
message mkind: 0 ChainToL1Conversion, 1 RegisterL1Validator,
2 L1ValidatorRegistration, 3 L1ValidatorWeight
payload pkind: 0 Hash, 1 AddressedCall
CONSENSUS-CRITICAL invariant made structural: ChainToL1ConversionID =
sha256(ChainToL1ConversionData.Marshal()) — the ID calls the SAME encoder, so
ID/Marshal skew (the bug in the reverted agent attempt) is impossible. The
native hash preimage is pinned as a golden with field-offset assertions
(verified by the L1 staking contract => consensus surface).
Message = {unsigned bytes, wkind-tagged sig bytes} container object;
UnsignedMessage = {networkID u32, sourceChainID 32B, payload} @44B header.
wire_baseline_test.go rewritten: pins the native hex golden for all 7 wkinds
+ structural discriminator checks + signature dispatch round-trips. Old-codec
sentinels -> zap errors. Whole node builds; warp+platformvm+proposervm green.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Bump github.com/luxfi/consensus v1.36.0 -> v1.36.1 — canonicalizes the
intermediate-ancestor walk (pathFromTip alias-collapse) that livelocked finality
under sustained saturation (the one finality path the mainnet-644 canonicalization
missed). RED-SHIP, 0 crit/high/med: alias-collapse can only stand in a
byte-identical inner execution (canonicalRep = CanonicalID, a state-root-binding
hash), so no fork risk HEAD lacked. Carries the v1.36.0 platformvm sole-codec
cutover + ZAP Quasar export already on main.
luxd builds green against consensus v1.36.1 (GOFLAGS=-mod=mod, CI parity).
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Folds the native-ZAP codec cutover (platformvm sole-codec) + the Quasar EXPORT-frontier
carry across the rpcchainvm ZAP boundary. Whole-node build green.
Collapse P-Chain tx/block/state serialization to a single ZAP-native codec
(CodecVersion=1). This is the gate before the Nova re-genesis: one write
path, one read path, no version dispatch.
Removed the whole multi-version surface:
- txs: registerV0TxTypes, CodecVersionV0/V1/V2, CodecVersionForTimestamp,
CodecForTimestamp, CodecAllowsRead, CodecRequiresLegacy, the Version
alias, and codec_activation.go (ZAPCodecActivationTimestamp).
- block: v0Codec/v0GenesisCodec, the block/v0 package, the lift_v0 path;
Parse is now single-version.
- state: the v0-probe codec_helpers (it Marshal'd at version 0, which
would fire a false warning every boot); GenesisCodec.Unmarshal inlined.
- genesis: single-version alias + comment cleanup.
- warp/bench/network: stale "linearcodec"/"reflectcodec" comments corrected
(all already ride the ZAP-backed pcodecs shim; wire unchanged).
- wallet/chain/p: txs.Version -> txs.CodecVersion.
Value 1 is retained (not renumbered) so no tx ID, block ID, or state root
changes: the surviving codec is exactly the ZAP-native slot the chain
already writes. Every existing serialization golden stays byte-for-byte.
Determinism proof (codec_zap_test.go, block/codec_determinism_test.go):
golden tx/block bytes + IDs, marshal idempotency, round-trip byte-stability
for every tx and block type, and trailing/truncated/wrong-version
rejection. grep -rnE 'linearcodec|reflectcodec|CodecVersion(ForTimestamp|V0|V1)'
vms/platformvm/ is empty.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Closes the deploy-blocking gap in two-tier consensus v1.36: the C-Chain EVM runs
as a SEPARATE rpcchainvm plugin process, so the chain manager's vmTyped is the
rpcchainvm *Client — which did NOT implement SetLastQuasarFinalized /
LastQuasarHeight (those live only on the concrete *evm.VM, the plugin SERVER
side). The manager's capability assert therefore returned !ok in production, the
QuasarObserver stayed nil, and finalized/safe + the warp export gate stuck at
genesis. Nova consensus was unaffected (node-side).
rpcchainvm/zap client (*Client):
- SetLastQuasarFinalized / LastQuasarHeight: ZAP-call the plugin (Msg 60/61).
Both short-circuit if the plugin did not advertise CapQuasarExport, so wiring
them is harmless for a generic plugin (no per-finalization no-op RPC).
- SupportsQuasarExport: reports the capability captured (atomically, once) from
the Initialize handshake's InitializeResponse.Capabilities.
- Fire-and-forget Set (logged, not returned — the caller is the consensus
observer); Height returns 0 on any failure (boot re-seed treats it as empty).
chains/manager createChain:
- quasarExportVM interface (values-not-places: the capability is a value, not a
static type property). Gate the observer + boot re-seed on the capability:
a *Client reports it via SupportsQuasarExport (false → Nova-only, exactly the
old !ok semantics — no cross-process spam); a VM WITHOUT the probe (an
in-process VM whose concrete methods we hold directly) is treated as capable,
preserving the direct-wire path. Observer is set BEFORE NewRuntime captures
netCfg; the seed runs after, carried by the exportVM value.
Test (client_quasar_test.go, -race): the REAL cross-process path — node *Client
-> ZAP wire -> luxfi/vm/rpc server -> a fake VM that satisfies the SAME
capability interface as *evm.VM. Capability captured from the handshake; a pushed
height crosses the wire into the VM; the VM's height round-trips back; a
non-capable plugin is a graceful Nova-only no-op.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Converts the per-type P-chain tx tests to native New*Tx + accessor methods +
roundTrip/SyntacticVerify (agents, verified): add_validator, add_chain_validator,
create_blockchain, disable/increase/register/set_weight/remove_chain/
transfer_ownership L1, add_permissionless_delegator, transform_chain. Every
error-path sentinel preserved by driving the bad value THROUGH the constructor
(pure byte-writer). Mock-based cases (fxmock/luxmock/verifymock, impossible on
an immutable zap buffer) reproduced with REAL unspendable owners / unsorted
inputs → real sentinels (ErrOutputUnspendable, ErrInputIndicesNotSortedUnique).
Only the un-reproducible 'already verified' cached-flag case dropped per file.
NEW staker_dispatch_guard_test.go pins the interface-satisfaction invariant the
staker type-switches depend on: struct-is-wire made *AddValidatorTx satisfy both
ValidatorTx+DelegatorTx (safe — ValidatorTx checked first everywhere), but
*AddDelegatorTx must NOT satisfy ValidatorTx (verified: lacks
ValidationRewardsOwner/Shares) or delegators would mis-route. Guard fails loudly
if a future edit breaks either property.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
v1.36 upstreamed round-scoped view-change INTO the engine (internal prevote/POL
in attestation/reconcile/cert), dropping the external hooks config.Parameters.
ViewChange + Runtime.HandleIncomingPrevote. Ripped the node's now-redundant
external plumbing: the ViewChange enable block (LUX_CONSENSUS_VIEW_CHANGE gate),
the quorumKindPrevote gossip routing + HandleIncomingPrevote call, the dead
BroadcastPrevote gossiper method, and the quorumKindPrevote envelope kind. The
engine owns view-change natively now (fail-secure halt on 2a-n>f unchanged).
Whole node builds EXIT 0 on v1.36.1; luxd binary compiles (55M).
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The shared UTXO value tree (UTXO/TransferableOutput/TransferableInput/Asset/
UTXOID/BaseTx/Metadata + OutputOwners path) gets native struct-is-wire
Marshal/Unmarshal (new marshal.go), replacing every pcodecs.Manager. Codec
param dropped from utxo_state / atomic_utxos / transferables (Sort* sorts on
inner fx wire Bytes()); flow_checker pcodecs.Errs -> errors.Join.
Node consensus persistence uses luxfi/utxo (unchanged); components/lux is the
tx-builder/#58 surface — encoding-only change, type tree NOT collapsed (#58
respected). Both P and X share the encoding => internally consistent, re-genesis
safe. Whole node builds EXIT 0; 8 components/lux round-trips + P-chain txs/block
tests green.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Last P-chain codec consumer. go build ./... = EXIT 0. The platformvm codec
(pcodecs/txs.Codec/serialize tags) is fully dead; P-chain tx + block + state are
native ZAP struct-is-wire end to end. Remaining codec surface is X-chain +
proposervm + warp (Wave A), which still carry their own (intact) codecs.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
CreateNetworkTx no longer carries genesis chains — the executor never created
them, so tx.Chains() was a latent stub (declared, verified, silently dropped).
Model A resolves it by decomplection, not by implementing a second chain path:
- CreateNetworkTx = ∅→Network birth (owner + security.Mode + own validator set).
- CreateChainTx = the ONE chain constructor. An L1 spawn is a BLOCK of
CreateNetworkTx + N CreateChainTx (block-atomic, not tx-atomic).
- Removed NetworkChain, its wire (writeNetworkChains/readNetworkChains/
ncStride/sliceIDs), the chain error set, MaxNetworkChains, the chains param.
- managerChainIdx (index into genesis chains) -> managerChainID (direct
ids.ID), now SYMMETRIC with ConvertNetworkTx's manager ref. ids.Empty =>
P-Chain-governed; a set chainID => Contract-governed staking-contract host.
Net: less code (write as little as possible), one-and-one-way chain creation,
no stub. platformvm build+vet PASS; SovereignL1/InheritedL2/HybridL2/Convert
round-trips PASS.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
vms/platformvm/... build + vet PASS with pcodecs/txs.Codec gone from the VM.
Executor semantics (standard_tx_executor):
- registerOwnSet(): shared primitive — per-validator state.L1Validator with
native owner blobs (txs.MarshalOwner/UnmarshalOwner, no codec), active-set
capacity check, EndAccumulatedFee=balance+accruedFees, SetNetToL1Conversion
manager-authority recording (byte-for-byte legacy tail).
- ConvertNetworkTx: promote endomorphism (owner-authorized, folds old
ConvertNetworkToL1Tx), gated by security.Mode.Manager.
- CreateNetworkTx: base (AddNet/SetNetOwner) + sovereign path registers own set.
- Deleted CreateSovereignL1Tx + ConvertNetworkToL1Tx.
- txs.UnmarshalOwner added: canonical owner marshal/unmarshal pair, no codec.
All ~13 txs.Visitor impls carry ConvertNetworkTx; gossip/block Parse(b) codec-free;
wallet/network/primary off txs.Codec.
KNOWN GAP (pending design decision, NOT silently green): CreateNetworkTx reads
tx.Chains() only to derive managerChainID — it does NOT create the genesis
chains (no AddChain). Atomic-spawn-with-chains vs decomplect-to-CreateChainTx is
the open call. 17 codec-era _test.go parked as .bak for follow-up rewrite.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
LuxAddress in airdrop.AirdropClaim + bridgevmroot.SignerLeaf is the NATIVE
20-byte Lux address (ids.ShortID / [20]byte), held beside the EVM common.Address
it disambiguates. 'Lux' prefix is banned place-in-value naming; the canonical
pair is EVMAddr / UTXOAddr. Named by address KIND, not brand.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Pull the network security model out of the tx wire into a small orthogonal
package (vms/platformvm/security): pure values, stdlib-only, no zap/tx/state
deps. One definition — security.Mode{RestakeParent, Admission, Threshold,
Manager} + Sovereign() + Valid() — composed by tx wire, executor, and state
(Rich Hickey: values not places; Rob Pike: no stutter, small orthogonal pkg).
Two orthogonal axes replace the flat Inherited/Sovereign byte:
- RestakeParent: lean on parent's validator set
- own set: Admission(NoOwnSet|Open|Gated) + Manager(PChain|Contract)
Their product spans every mode incl. HYBRID L2 (restake AND additive own set),
which the flat byte could not express. Invariant RestakeParent || own-set on
Mode.Valid(); Sovereign derived, never flagged.
CreateNetworkTx + ConvertNetworkTx carry security.Mode on the wire via shared
setSecurity/readSecurity. Round-trip green incl. new HybridL2 case + Convert
target-mode. Wallet builders pass the explicit restaked-L2 Mode.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Blocks are now the zap buffer: kind + parentID@1 + height@33 + time@41 +
tx-length-list@49 + tx-blob@57 (+ proposal-tx@65). commonZapBlock embedded base
mirrors spendingTx. Parse = zap.Parse + kind dispatch (no codec, no version).
Deleted block/codec.go + block/v0 + lift_v0. Round-trip green for abort/commit/
proposal/standard incl real signed txs. go build + go test ./block/ = green.
FINDING: block.GenesisCodec was double-duty — also serialized NON-block STATE
values (feeState, owners, L1Validator, metadata, chains). That's a THIRD codec
surface (state DB serialization) still to migrate for the full kill.
Create and Convert are orthogonal arrows: CreateNetworkTx = ∅→Network (birth,
sovereign-or-inherited); ConvertNetworkTx = Network→Network (promote an
existing network: inherited→sovereign, re-anchor parent — L2→L1, L3→L1, with
owner auth). Only CreateSovereignL1 stays folded (it was birth+sovereign, not
a distinct op). Both reuse the NetworkValidator component. Round-trip green
incl L2→L1 promote.
CreateNetworkTx{parent, owner, security, validators, chains, manager} creates a
network at ANY level in one tx — parent is the level axis (Primary⇒L1, L1⇒L2,
recurse; level=depth, never stored). security is a coproduct: SecuritySovereign
(own validators+manager) | SecurityInherited (restaked from parent). manager
lives on the Network so an inherited L2 holds local admin. NetworkValidator +
NetworkChain are shared value components (reused by CreateChainTx). Deleted
ConvertNetworkToL1Tx + CreateSovereignL1Tx (folded / migration artifacts).
Round-trip green: sovereign L1 (own validators+chains+manager) AND inherited L2
(parent recorded, no own validators, local manager) both survive. One and one
way to make a network at any depth.
zap.ListBuilder.AddBytes counts BYTES not elements; fixed-stride lists were
storing byte-inflated counts -> stride clamp rejected them. Fixed every write
helper to store the real element count (len(entries)). Round-trip test now
GREEN across the hardest cases: multisig+stakeable outputs/inputs, the nested
ConvertNetworkToL1Validator (NodeID+BLS PoP+2 owners), SovereignL1Chain
manifest, and signed unsigned‖creds. go build ./vms/platformvm/txs = exit 0.
The P-chain tx wire is now struct-IS-wire, codec-free, and verified correct.
All 21 real P-chain tx types are now the zap buffer — no codec, no marshal/
unmarshal, no Manager, no version, decompounded, on luxfi/zap. ConvertNetworkToL1Tx
+ CreateSovereignL1Tx hand-written with a shared ConvertNetworkToL1Validator
nested encoder (fixed-stride records + shared NodeID/addr pools) and a
SovereignL1Chain encoder. go build ./vms/platformvm/txs/ = exit 0.
Next: round-trip correctness test, then the external consumer flip.
- SlashValidatorTx ripped (unwired stub; Avalanche has no slashing; consensus
only detects, never enforces). Type+executor+tests+Visitor surface removed.
- CreateAssetTx + OperationTx ripped from P-CHAIN (X-chain/AVM types; LP-0130:
asset creation + fx ops are X-chain money-rail, not P staking-rail; no P
producer). xvm/avm untouched.
- Staker interfaces (StakerTx/ValidatorTx/DelegatorTx/ScheduledStaker) restored
on the 5 validator types via pure accessors + FxID-on-stake preserved.
- Remaining real complex types: ConvertNetworkToL1Tx + CreateSovereignL1Tx.
Wires the consensus EXPORT (Quasar, two-thirds-stake) frontier into the C-Chain VM so
the EVM finalized/safe tags and the warp cross-chain gate resolve to the Quasar tip.
Consensus v1.35.38 -> v1.36.0.
tx.go: Tx holds Unsigned (zap-backed) + Creds; Parse wraps signed bytes and
splits unsigned/creds at the self-delimiting boundary; Sign builds unsigned‖creds;
no codec.Manager param anywhere. Deleted codec.go (V0/V1/V2 reflection Manager)
and codec_activation.go. WIP: consumer sites that passed txs.Codec / called the
old Parse(codec, bytes) flip next.
Parse wraps the buffer zero-copy and dispatches on the 1-byte kind. Signed =
unsigned ‖ creds (both self-delimiting), so unsigned is a byte-prefix of
signed. writeCredsBuf/parseCredsBuf encode credentials natively (shared 65B
sig-blob array). No Marshal/Unmarshal/Manager. WIP: references the 24 pure
type constructors (5 hand-written + 18 in parallel conversion + 1 template).
Wire the two-tier consensus export boundary through the node so the EVM `finalized`/`safe` tags and
the warp export gate track the ⅔-stake Quasar tip, never the reorgable Nova/accept tip:
- Set NetworkConfig.QuasarObserver to push each EXPORT (Quasar) frontier advance into the raw inner
VM (SetLastQuasarFinalized) — the eth/warp backends live there, not on the proposervm wrapper.
Interface-gated: only a VM exposing the export sink (the C-Chain EVM) participates.
- On boot, re-seed the consensus export frontier from the VM's DURABLE Quasar height
(SyncQuasarFrontier) so GetQuasarTip/QuasarHeight do not regress on restart.
Also remove the node's dead references to the v1.36-deleted Tendermint braid (the consensus engine
dropped it in 174af3c31), which no longer compile against the v1.36 engine:
- the LUX_CONSENSUS_VIEW_CHANGE opt-in (params.ViewChange is gone — Nova is the sole decider),
- the quorumKindPrevote gossip kind + BroadcastPrevote + HandleIncomingPrevote routing (no prevotes;
the ⅔ Quasar attestation rides the ordinary accept-vote gossip). Keep the braid dead.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Shared delta-field encoders every non-proposal tx composes on the spending
envelope: owner (fx.Owner), auth (*secp256k1fx.Input), inline Validator (44B)
+ Signer (145B: kind+BLS pubkey+PoP), id lists, extra spending lists. Built on
luxfi/zap. With spending.go, the full reusable foundation — type files are now
pure compositions.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The envelope (NetworkID/BlockchainID/Outs/Ins/Memo) + multisig/stakeable
Output/Input entries + shared owner-address/sig-index arrays, built on
luxfi/zap generic primitives — no codec, no zap_native package. writeSpending/
setEnvelope build inside New*Tx; readEnvelope reads lazily. Polymorphism
(TransferOutput/LockOut, TransferInput/LockIn) handled in explode/assemble.
Foundation every non-proposal tx composes.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
kind.go: 1-byte discriminator @ object offset 0 = the whole dispatch. No
codec, no version, no slot map. AdvanceTimeTx converted to the pure model:
holds *zap.Message, Time() is an offset read, NewAdvanceTimeTx builds once,
Bytes() returns the buffer. No marshal/unmarshal anywhere.
Template for the remaining 21 types. Package migrates atomically (codec.go +
all types + Parse/Sign together), so it compiles green again only when the
whole set + concentrated consumers (executor/builder/fee/api, ~63 New sites)
are converted. WIP.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
ZAP has no serialization step. The bridge copied fields between plain txs.*
structs and the buffer, which is exactly the codec ZAP deletes. Removed.
The right way: tx type IS the zap buffer (accessors, Parse=wrap, Bytes=buffer),
no codec / manager / compat / version. Wire primitives stay in luxfi/zap.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Benchmark-proven live mainnet bug (250-trader DEX load): a validator that falls behind cannot
resync the C-Chain. GetContext (the catch-up "context response", wire = Ancestors) bounded the
response ONLY by block COUNT (maxContextBlocks=256). Under heavy DEX load 256 blocks summed to
3.4-5.7 MB, exceeding the 2 MB peer message cap (the zstd compressor refuses uncompressed input
above constants.DefaultMaxMessageSize to prevent a decompression bomb), so msgCreator.Ancestors
FAILED to build and the behind validator received NOTHING — permanently stuck while the tip
advanced (luxd-3 stuck at 256 while tip went 293→300, looping empty-block builds).
Fix (piece 1/3 of the layered design — defense in depth): GetContext now ALSO bounds the
response by serialized SIZE. It stops adding blocks before the accumulated payload would exceed
byteBudget (cap − 128 KiB envelope margin), but ALWAYS includes at least one block so a behind
node makes progress every round; the requester re-requests the remaining gap (context fetch is
already a multi-round oldest-first fill). A single block that alone exceeds the budget is still
served (best-effort) so the walk never deadlocks — the forthcoming trust-tiered validator cap
(pieces 2/3) gives such a block the send headroom; a stranger's tight cap correctly rejects it.
Tests (chains/context_chunk_test.go): TestGetContext_ChunksBySize_FitsUnderCap (100×150 KiB
chain → 12 blocks / 1.84 MB, under budget, vs ~15 MB packed by count — fail-on-old);
TestGetContext_SingleOversizeBlock_StillServed (one oversize block still served, no deadlock).
Pieces 2/3 to follow: trust-tiered message cap (validator peers get headroom, strangers keep
2 MB) + confirm the inbound throttler/benchlist penalizes oversized-message senders.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Shared spending envelope (NetworkID/BlockchainID/Outs/Ins/Memo) + polymorphic
converters bridging the txs struct graph to proto/zap_native primitives with
zero reflection: TransferOutput/stakeable.LockOut, TransferInput/stakeable.LockIn,
secp256k1fx.Credential. BaseTx (TxKindBaseFull) bridged; creds travel in the
separate creds buffer of the unsigned‖creds envelope.
Round-trip GREEN: 2-of-3 multisig + owner-locktime output, stakeable.LockOut,
stakeable.LockIn input, memo, AND signed secp256k1 credentials all survive
Marshal->Unmarshal with exact field equality; unsigned stays a byte-prefix of
signed. Every embedding tx type now composes this envelope + its delta fields.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Fixes the 'restart-recovery exhausted' mainnet killer: a slipped validator
logged 'cert REFUSED (behind; fetch and retry)' but NEVER fetched, because
HandleIncomingCert only triggered a catchup when the cert's OWN block was
untracked — never for a missing INTERMEDIATE ancestor. v1.35.38 (3c09ecb94)
surfaces the specific missing ancestor as a typed error and the cert handler
fires exactly one requestCatchup for it. The node-layer fetch machinery
(networkCatchup.RequestAncestors -> requestContext -> GetAncestors ->
AcceptCatchupBlock) was already fully wired — it was just never called on this
path. With --skip-bootstrap=true (which disables the beacon-quorum frontier
backstop) this cert-trigger was the ONLY self-heal path, so its absence was
fatal: a behind node couldn't rejoin -> effective 4/5 -> any flap -> stall.
Consensus-side only, no node code change. -race: launch-gate invariant PASS
(rejoin 2.75s, no fork); self-heal + typed-error tests PASS, no data races.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Adds nativeManager implementing pcodecs.Manager by bridging txs structs to
proto/zap_native buffers — no reflection, no serialize-tag walk, no version
dispatch. Signed wire = unsigned_zap_buffer ‖ creds_zap_buffer (ZAP buffers
are self-delimiting), preserving tx.go's unsigned-is-prefix-of-signed
invariant with no tx.go change. AdvanceTimeTx + RewardValidatorTx bridged
and round-trip green (build->Marshal->Unmarshal field equality, byte-stable,
prefix invariant, non-ZAP reject). Added alongside the reflection Codec;
flip + reflection deletion lands once all registered types are bridged.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Third-layer mainnet finality fix. v1.34.27 (consensus v1.35.36) proved the
dynamic committee (alpha=4 n=5) and topology.go cert-receive canonical resolve
work live — consensus climbed past 1085755 to round-height 1085761 — but hit the
next layer: a stale-alias prevote-lock. viewForLocked seeds lockBlock from a
pre-canonical-fix durable committedSlot (an OUTER proposervm-wrapper id); a
round-0 lock on an outer alias makes prevoteTarget compare the stale outer id
against the inner-canonical winner by raw id, mismatch forever, no POL, freeze.
v1.35.37 (742696baf): stepViewChange rebases a stale-alias lock onto the inner
canonical winner IFF it resolves (via the same vmCanonicalResolver the cert
rebase uses) to exactly winnerCanon — a genuinely different inner (real fork) or
unresolvable lock is left fail-closed frozen, never merged. lockRound preserved,
2a-n>f untouched, alpha inner-precommits were always inner (CanonicalVoteMessage
binds inner, excludes outer). Full engine/chain green (356s, 0 fail); -race
clean; RED safety gate proves divergent inners still refused.
Roll note: luxd-4 first releases its lock (log 'vc stale-alias lock REBASED');
finality past 1085755 needs >=4 nodes on this build, so height climbs as 3->2->
1->0 come up. EVM stays v1.104.7 (head-pin).
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Unsticks mainnet C-Chain finality. Root cause (consensus-layer, proven): under
pChainHeight=0 anyone-can-propose, each validator wraps the same inner block in
its own outer proposervm envelope; votes are canonical-keyed so an α-of-K cert
forms network-wide, but HandleIncomingCert did a strict envelope-id lookup — a
node holding a different alias of the same inner block missed it and requested
catchup instead of finalizing the local wrapper it held, so VM.Accept was never
called and the EVM head froze at 1085755 while consensus logged 'finalized
voters=4'. v1.35.34 (66438a23b) resolves the local wrapper by canonical id and
rebases the verified cert (outer ids unsigned; α votes verify unchanged), no
fork (per-height gate keys on canonical id, idempotent).
Also: dynamic committee 1→N proven (v1.35.35, effectiveCommittee sizes cert +
view-change from live validator count; n=1→1/1,2→2/2,3→3/3,4→3/4,5→4/5 — the
BFT-safe ⌊2n/3⌋+1); view-change safety gate realigned to the effective
committee (RED#2); snowman comment-purge (Quasar/Nova); dynamic committee logs.
Node commits: 4e71814e58 (proposervm→EVM Accept-cascade test, RED#1),
c13681108f (purge), a02611413e (logs). EVM stays v1.104.7 (head-pin). Full
engine/chain suite green under -race (369s, 0 fail/0 race); RED adversarial
suite proves scale-invariance 1→1M + RLP seam byte-exact on real mainnet state.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The mainnet 1085755 freeze had consensus finalizing an outer proposervm block while the
inner EVM lastAccepted stayed frozen. The consensus engine's ledger advancing on VM.Accept
is proven in engine/chain; this proves the OTHER half at the NODE layer — that VM.Accept on
a proposervm wrapper cascades to the inner block's Accept:
postForkBlock.Accept → acceptOuterBlk → acceptInnerBlk → Tree.Accept(innerBlk) →
innerBlk.Accept ⇒ inner VM lastAccepted advances
- TestAcceptCascade_InnerHeadAdvances_AtScale: 1000 heights through the REAL Accept path;
the inner EVM head + proposervm head advance in lock-step at every height.
- TestAcceptCascade_SiblingStorm_WinnerAdvancesInnerOnce: five distinct outer envelopes wrap
ONE inner block (the anyone-can-propose alias set); accepting the finalized wrapper advances
the shared inner exactly once.
Confirms the propagation was sound, not a node-layer bug — the freeze was the consensus-layer
storm-alias resolution gap (fixed in consensus v1.35.34). The real luxfi/evm RLP-import→produce
→tip byte-exactness is proven separately in evm/core rlp_seam_red_test.go; composed, they cover
the full engine→proposervm→EVM Accept path.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Removes the forbidden Avalanche term from code comments in chains/manager.go,
chains/quorum.go, and vms/proposervm/proposer/windower_determinism_test.go. Reworded to
preserve exact meaning; comments only, no identifier/behavior change.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The "round-scoped view-change ENABLED for chain K=21 alpha=15" line read as if K=21/α=15
were the finality committee — misleading. It is the Snowman SAMPLE preset. The α-of-K
cert and the view-change POL/precommit are sized to the LIVE validator set at runtime
(effectiveCommittee/bftCommittee; 5 validators → K=5/α=4), and the engine already logs
the effective (K,α) on each committee re-clamp. Relabel the fields presetK/presetAlpha
and add a note pointing at the runtime committee-clamp log. Log-only; no behavior change.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Restores mainnet C-Chain finality. The prior stall: with 5 live validators but
MainnetParams K=21/alpha=15, BOTH finality gates (assembleCertLocked cert-alpha
AND view-change POL) required 15 distinct votes — impossible from 5 validators,
so the chain froze (safe, no fork). consensus v1.35.33 sizes both gates from the
live validator count via effectiveCommittee (alpha=4 for n=5), inheriting the
minBFTCommittee K=4/alpha=3 floor (1085013 self-finality guard preserved);
Snowman K=21 sample untouched. node chains/quorum.go adds
validatorStakeSource.ValidatorCount (height-indexed, deterministic). EVM stays
v1.104.7 (head-state pin). Test: 5-validator MainnetParams control freezes,
fix converges in 0.35s.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>