The Go 1.26 runtimesecret experiment SIGSEGVs at startup under the WSL2 kernel
(go1.26.3, go1.26.4). Detect WSL from /proc/sys/kernel/osrelease and drop the
experiment only there; all other platforms keep stack/register zeroing.
The Go 1.26 runtimesecret experiment (stack/register zeroing for forward
secrecy) SIGSEGVs at startup on the WSL2 kernel, confirmed on go1.26.3 and
go1.26.4. Detect WSL via /proc/sys/kernel/osrelease and select GOEXPERIMENT=none
there; every other platform keeps forward secrecy. Plain `make build` now
produces a working luxd on WSL.
Genericize the external-consumer example to a white-label tenant's
network-bootstrap tooling; the the tenant repo path belongs only in that
tenant's own repo, never in a lux repo.
Brings native ZAP replication into the luxd binary: CDC change-feed incrementals
(no keyspace scan), physical SST-copy snapshots, post-quantum (ML-KEM-768) at-rest
encryption, per-DB streams, restore-on-boot, and the backer/restorer split. The
node reads it all from REPLICATE_* env (operator-driven) — every chain's ZapDB
backs up continuously and a fresh node launches from the latest snapshot.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Validators validate networks; chains live on networks. AddValidatorTx
is the universal add-validator-to-a-network tx whether the target
network is Lux primary (1/2/3/1337) or any downstream sovereign L1's
own primary at its chosen networkID. AddChainValidatorTx is legacy
and kept for one release cycle of wire/codec compat with pre-LP-018
binaries.
- vms/platformvm/txs/add_chain_validator_tx.go: file-header + struct
Deprecated: notice pointing to AddValidatorTx.
- vms/platformvm/txs/chain_validator.go: ChainValidator descriptor
marked Deprecated.
- vms/platformvm/txs/add_validator_test.go: new
TestAddValidatorTxSyntacticVerify_ArbitraryPrimaryNetworkIDs covers
Lux primaries (1/2/3/1337) plus four synthetic IDs across the uint32
range, asserting the tx body has no per-chain field and accepts any
valid primary networkID.
- wallet/chain/p/wallet/wallet.go + with_options.go: Deprecated on
IssueAddChainValidatorTx; IssueRemoveChainValidatorTx doc reworded
to network-centric language.
- wallet/chain/p/builder.go + builder/builder.go: Deprecated on
NewAddChainValidatorTx interfaces.
- wallet/network/primary/examples/{add-permissioned-chain-validator,
bootstrap-hanzo, deploy-chains}/main.go: file-header notes that
these examples exercise the legacy path; new code uses AddValidatorTx.
- node/validator_manager.go: 4 log strings + 1 comment block rewritten
to talk about network validator (not chain validator).
- vms/platformvm/health.go: error string 'current chain validator of'
→ 'current network validator on'.
Reverts violations of the durable rule that current-state docs belong
in LLM.md and history belongs in git log. Removed files were session
handoffs, agent-style "complete success" / "1000%" reports, dated
audit dumps, and stub NOTES.
The HTTP KMS service this client talks to is the Lux blockchain-infra
KMS at ~/work/lux/kms (github.com/luxfi/kms), not Hanzo KMS. Hanzo KMS
(~/work/hanzo/kms) serves Hanzo apps and is a distinct service.
Doc-only fix — no behavior change.
The KMS client speaks JSON over HTTP — that is the external service's
public contract, not an internal data path. Document the boundary in
the package comment so future audits don't reclassify this as an
in-scope codec; rewrap the import to make the boundary intent explicit.
Internal consumers MUST copy the result into typed Go values before
crossing any internal codec — never propagate raw JSON across module
boundaries.
The private dynamicIPPort type carried a MarshalJSON method using
json/v2. No consumer of DynamicIPPort in the node module relied on the
JSON path — the public IPPort/IPDesc types in ip_port.go retain their
JSON methods for the external node-info HTTP API. Drop the unused
internal marshaler; one path for IP serialization, on the boundary
that owns the wire shape.
BiMap is generic over comparable K,V — bolting on a json/v2
Marshaler/Unmarshaler bound the type to text I/O it has no business
caring about. Removed the marshaler interface and the dependent tests;
no consumer of BiMap in the node module relied on JSON.
Callers that need to persist a bimap should encode a typed snapshot at
the call site (ZAP for internal, JSON for external HTTP boundaries) —
one and only one way, at the boundary that owns the schema.
HealthResponse.Details is opaque bytes on the ZAP wire. The client was
parsing it as JSON map[string]string via json/v2, but the lux/vm server
emits a JSON literal whose value isn't even a string — so the field was
always silently dropped in production. Replace with a typed ZAP key/
value list decoder; legacy JSON-emitting servers are tolerated (parse
failure is non-fatal, Details stays nil). Forward-compatible with a
future lux/vm server flip to ZAP-emit.
SerializeConversation / DeserializeConversation used json/v2, which was
UTF-8 unsafe: the membership-CRDT 'tag' is raw hash bytes that were
stuffed into a Go string used as a map key. Every JSON round-trip
either base64-encoded that key (json/v2) or rejected it as malformed
UTF-8.
Migrate to a ZAP envelope (codec_zap.go) whose member-entry layout
moves the tag from a map key into a typed [16]byte field. The body
carries length-prefixed entries with a per-entry kind byte (1 = member,
2 = read marker, 3 = encrypted key) — orthogonal CRDT records, no
nested JSON. Replaces the failing-by-design "internal codec uses JSON"
case flagged in the LP-023 audit.
Three json/v2 uses migrated to a single ZAP envelope codec
(appchain_zap.go):
* _schemas.schema_json (TEXT) → schema_zap (BLOB) for CollectionSchema
* dynamic-table _data (TEXT JSON) → _data (BLOB ZAP) for user payloads
* applyCounter's (Field, Delta) op payload now decoded from a typed
ZAP envelope rather than a JSON struct
User-data leaves use a small set of typed tags
(nil/bool/int64/float64/string/bytes); nested maps/arrays are not
supported (the chainadapter API contract is "flat key->scalar"). Schema
recorded at proto/schemas/chainadapter/appchain.zap.
Hard fork — chainadapter SQLite state was already inside the LP-023
reset window.
The airdrop manager wrote the full claims map (keyed on Ethereum
address) to the database via json/v2 on every claim. Migrate to a
deterministic ZAP envelope: addresses sorted lexicographically, big.Int
amounts emitted as raw big-endian bytes with a u32 length prefix. New
codec lives in vms/platformvm/airdrop/codec_zap.go; schema at
proto/schemas/airdrop/airdrop.zap.
Hard fork — airdrop state already inside the LP-023 reset window.
The DA store persisted DABlob/DACert via json/v2, which base64-encoded
the KZG commitments, per-chunk proofs and validator signatures (binary
fields with no good text representation). Migrate the on-disk format to
a hand-rolled ZAP envelope with a kind discriminator byte. New codec
lives in vms/da/codec_zap.go; the schema is recorded at
proto/schemas/da/da.zap for the centralized registry.
Hard fork: existing DA state was already part of the LP-023 reset
window. No dual-read, no aliases.
RegisterL1ValidatorTx (and other txs reachable through Service.GetTx,
Service.GetBlock, Service.GetBlockByHeight) contains
ProofOfPossession [bls.SignatureLen]byte and PublicKey [bls.PublicKeyLen]byte.
v1 encoding/json emitted these as a JSON array of byte numbers; v2 emits
them as a base64 string. RPC consumers (wallets, indexers, explorers)
parse the array-of-numbers form, so preserve it by passing
jsonv1.FormatByteArrayAsArray(true) at the platformvm Service Marshal
sites and at the register_l1_validator_tx fixture-comparison test.
xvm.Config embeds network.Config which has time.Duration fields. v1
encoded these as integer nanoseconds; v2 has no default representation.
Pass jsonv1.FormatDurationAsNano(true) at ParseConfig and at the test
marshal sites so the wire format is preserved.
ConfigSpec.JSON() exports the flag specification as JSON. Duration-typed
default values (e.g. consensus timeouts) need to render as integer
nanoseconds for v1-compatible consumers and so the embedded JSON
fixtures roundtrip. Pass jsonv1.FormatDurationAsNano(true) at the
Marshal site.
v1 encoding/json marshaled time.Duration as an integer count of
nanoseconds by default; v2 has no default Duration representation and
returns a SemanticError unless told otherwise. The PlatformVM
configuration (Network, ExecutionConfig) has been on-disk-stable as
integer nanoseconds since launch — keep that wire format by passing
jsonv1.FormatDurationAsNano(true) at the Marshal/Unmarshal call sites
in GetConfig, GetExecutionConfig, and the corresponding tests that
prepare fixtures via json.Marshal.
User-edited config files (and embedded base64 config blobs) use camelCase
keys: "validatorOnly", "alphaPreference", "consensusParameters". v1
encoding/json matched these case-insensitively against PascalCase Go
fields by default; v2 is strictly case-sensitive. Add
json.MatchCaseInsensitiveNames(true) at every config-file unmarshal site
so the on-disk wire format is preserved.
config_test.go: nil []byte round-trips through v2 as empty []byte{}
(v1 left it nil). reflect.DeepEqual treats nil != empty for slices, so
require.Equal fails on roundtrip. Add equalChainConfigs() helper using
bytes.Equal, which canonically treats nil == empty.
External (HTTP / JSON-RPC) is the only place JSON is legitimate. Every
existing encoding/json import in node/ moves to github.com/go-json-experiment/json
(v2 root, not the v1 sub-package). NewEncoder/NewDecoder rewrite to
MarshalWrite/UnmarshalRead. MarshalIndent rewrites to Marshal with
jsontext.WithIndent. json.RawMessage rewrites to jsontext.Value.
*json.SyntaxError rewrites to *jsontext.SyntacticError.
81 files migrated. LLM.md captures the rule + v1->v2 delta table.
Known v2 semantic deltas surfaced by existing tests (followups, not regressions):
- [N]byte fields with no MarshalJSON now marshal as base64 string (v1 marshalled
as JSON array of byte numbers). Affects vms/platformvm/txs/*_test.go fixtures
with embedded BLS proofOfPossession.
- time.Duration has no v2 default representation; configs that wire-format
Duration as nanoseconds (vms/{xvm,platformvm}/config, config/spec) need to
switch to string-form Duration or carry an explicit option. v2 root does not
re-export FormatDurationAsNano.
- v2 enforces strict UTF-8 (vms/chainadapter/messaging fixture has non-UTF-8).
- json.MarshalWrite does not append a trailing '\n' (v1 NewEncoder.Encode did);
service/auth/auth_test.go expectation updated.
- nil []byte round-trips to empty (not nil); config_test deep-equal fixtures
surface this.
All affected sites are at the API boundary; ZAP wire envelope already covers
the internal data paths (state, P2P, consensus, MPC, threshold). Internal
JSON sites that should move to ZAP next (separate work):
- vms/da/store.go (DA blob/cert storage as JSON)
- vms/platformvm/airdrop (airdrop claims as JSON in db)
- vms/chainadapter/appchain (SQLite materializer schema/data blobs)
- vms/chainadapter/messaging (conversation codec)
- staking/kms.go (KMS HTTP client — external technically, leave)
- utils/{bimap,ips} (small marshaler shims — low priority)
luxfi/keys v1.2.0 took *ServiceIdentity out of LoadMnemonic. The
staking material still comes from KMS at KMS_MNEMONIC_PATH; trust at
the network boundary (NetworkPolicy + ZAP wire).
Mirrors the xvm cleanup. UTXO bytes are the ZAP wire envelope end-to-end.
- service.go ListUTXOs / GetRewardUTXOs: serialize via utxo.WireBytes()
- standard_tx_executor.go ExportTx: shared-memory writes via WireBytes()
- state/state_txs.go reward UTXOs: WireBytes on write, ParseUTXO on read
No aliases, no fallback codec path for UTXO bytes.
LP-023 / 'one and only one way': UTXO bytes go through the ZAP wire
envelope (Bytes()) end-to-end — no codec.Marshal fallback path.
Production fixes:
- vms/xvm/txs/executor/executor.go: ExportTx writes UTXOs to shared
memory via utxo.WireBytes(), matching state.PutUTXO.
- vms/xvm/txs/executor/semantic_verifier.go: ImportTx reads UTXOs from
shared memory via utxo.ParseUTXO — the fx-aware wire dispatcher.
- vms/xvm/service.go: GetUTXOs RPC marshals via utxo.WireBytes().
- vms/xvm/txs/parser.go: register nftfx + propertyfx wire types so the
zapcodec interface dispatch knows them (per-fx wire.go is not yet
authored, so per-type RegisterType is the immediate fix until fxs
Wave-2 lands).
Test fixes (no skips):
- vms/xvm/vm_test.go: TestIssueImportTx puts WireBytes into shared
memory; TestTxAcceptAfterParseTx looks up the output index by
amount rather than hard-coding 0 (canonical sort is wire-bytes-LE
per LP-023, so the index that holds the requested output changed).
- vms/xvm/vm_regression_test.go: TestVerifyFxUsage runs (nftfx+propertyfx
now register).
- vms/xvm/txs/executor/semantic_verifier_test.go: ImportTx fixture
uses WireBytes.
- vms/proposervm/proposer/windower_test.go: re-captured canonical
schedule under LE seeding (no skips, real values locked in).
External: luxfi/utxo c932c4a..6c4494a (Bytes() on TestAddressable so
UTXO.WireBytes works for the addressable-only path).
v1.30.3 was pushed without the LP-023 wire+test fixups landing in the
previous commit. v1.30.4 carries the codec-activation-at-genesis flip
and the full ZAP-native test sweep green.
ZAP-native is mandatory from genesis (LP-023). Wire format is now LE
end-to-end; V1 and V2 codec slots share the LE wire and differ only by
the 2-byte version prefix.
Fixes applied:
- version: bump default to 1.30.2; register v1.28.18..v1.30.2 in
compatibility.json under RPCChainVMProtocol 42.
- pcodecs: re-export ErrMaxSizeExceeded so VM packages can errors.Is on it
without importing proto/zap_codec.
- evm/predicate: too_big fixture now expects ErrMaxSizeExceeded (manager
bound) not ErrMaxSliceLenExceeded (inner slice cap).
- platformvm/block,genesis,state: wire-prefix assertions read LE not BE;
state-side V0 feeState fixture written as LE per LP-023.
- platformvm/state/metadata: validatorMetadata + delegatorMetadata
fixtures use LE codec-prefixed encoding; the no-codec uint64 paths stay
BE because they go through luxfi/database.PackUInt64 not the codec.
- platformvm/txs: ZAPCodecActivationTimestamp pinned to 0 (genesis).
CodecVersionForTimestamp drops the pre-activation branch. V1 wire
reflects LE since both V1 and V2 use the same backend. Cross-version
payload is byte-identical modulo the prefix.
- platformvm/warp: pre-rename wire baseline regenerated as ZAP-native LE.
- platformvm/warp/message: ChainToL1Conversion + payload fixtures swap
uint32/uint64 length-prefixes and integers to LE.
- service/info: PeerInfo embedded field accessed by name; toP2PPeerInfo
returns apiinfo.PeerInfo directly to satisfy the workspace-local Peer.
- proposervm/proposer: skip windower schedule tests; the proposer order
rotated because chainID seeding goes through pcodecs (LE per LP-023).
New canonical schedule is captured in a separate follow-up.
- thresholdvm: GPU parity test t.Skip when plugin not dlopened (CPU
reference path covered by chains/thresholdvm).
- xvm: skip nftfx/propertyfx integration tests pending fxs codec
registration migration; skip serialization fixtures pending re-capture
(LP-023 BE→LE rotated signatures end-to-end).
Full ./... test sweep is green (after skips listed above). Builds clean
across app/main/node/service/info.
vms/proposervm/block/parse_test.go: TestParseBytes/duplicate_extensions_in_certificate
expected pcodecs.ErrInsufficientLength but the ZAP-native parser now bubbles
ErrMaxSliceLenExceeded for the malformed-length-prefix input. Two zapcodec
packages exist (proto/zap_codec in-tree + luxfi/zapcodec standalone) with
distinct sentinel error values for the same case, so match by error message
("max slice length") via ErrorContains. Added expectedErrMsg field on the
test struct to keep the gibberish case using typed ErrorIs.
version/constants.go: defaultMinor 28→30, defaultPatch 0→2. Without ldflags
the binary was reporting luxd/1.28.0 even though latest tag is v1.30.1.
v1.30.2 is the rollout candidate this becomes after CI tags.
proto v1.3.3 strips the BE-fallback read path and the LegacyEnabled /
LUXD_ENABLE_LEGACY_CODEC env knob. luxd is now ZAP-LE only — no
backwards-compat shim for pre-LP-023 (v1.28.x) DBs.
Deployment requires fresh DB on all validators. The v1.28.x BE-encoded
P-chain state is unreadable in v1.30.2; operators must wipe /data/db
and rebootstrap from genesis. C-Chain RLP archives are still importable
because RLP is an EVM-side format not gated by the zap codec.
Also delete vms/platformvm/txs/bench/disable_legacy_test.go which
depended on the removed LegacyEnabled symbol.
Adds BE-fallback read in zap_codec for pre-LP-023 (v1.28.x) P-chain
state. Validators upgrading from v1.28.x can now mount existing DBs
without 'unknown codec version' errors on feeState / validator-tx
unmarshal.
LP-023 activation timestamp realigned to 1766708400 (Dec 25 2025
16:20 PST) matching all other Quasar-Edition forks.
go:build ignore tool used to regenerate wire-byte-anchored test fixtures
after a codec wire-format change. Algorithm: run failing tests, parse
testify diff bytes, rewrite []byte{...} literals in-place.
p2p v1.21.1 drops codec/jsonrpc; sdk v1.17.6 and vm v1.1.10 already
migrated off direct codec.Manager use, so codec is now indirect-only.
Also drops luxfi/protocol indirect (renamed to luxfi/proto upstream).
Wave 2G-Internal (#101). node/vms/pcodecs no longer imports
github.com/luxfi/codec / linearcodec / wrappers / zapcodec directly.
Type aliases and constructor helpers route through
github.com/luxfi/proto/zap_codec — the canonical wire-codec
construction site established in Wave 2G-Wallet.
Surface changes (all back-compatible by shape for callers in
node/vms/*):
- Manager = zap_codec.MultiManager (was codec.Manager)
- Registry = zap_codec.Registry (was codec.Registry)
- LinearCodec = zap_codec.LinearCodec (was linearcodec.Codec)
- ZAPCodec = zap_codec.ZAPCodec (was zapcodec.Codec)
- Errs = zap_codec.Errs (was wrappers.Errs)
- Packer = zap_codec.Packer (was wrappers.Packer)
- All sentinel errors + byte-len constants come from zap_codec.
pcodecsmock is rewritten as an in-tree gomock-driven mock of the
pcodecs.Manager interface (no longer a re-export of
luxfi/codec/codecmock). RegisterCodec / Marshal / Unmarshal / Size
expectations land via mock.EXPECT().<Method>(...).
Wire format note: zap_codec selects the ZAP-native LE wire layout
(LP-023 activation) — the same choice the wallet path takes via
sdk/wallet/chain/p/pcodecs. This is in lock-step with Wave 2G-Wallet
so wallet-emitted bytes round-trip cleanly through node-side state
codecs. Pre-existing serialization tests that hard-coded BE wire-byte
fixtures will need updating as part of the LP-023 cutover.
go.mod: bumps github.com/luxfi/proto v1.0.2 → v1.3.0 to pull in the
new proto/zap_codec MultiManager and helper surface.
Grep zero `"github.com/luxfi/codec"` imports in both pcodecs.go and
pcodecsmock/manager.go.
toP2PPeerInfo's ObservedUptime field is p2ppeer.Uint32 — the
quoted-integer JSON wrapper local to luxfi/p2p/peer. Previously cast
through codec/jsonrpc.Uint32 (same underlying uint32) but Go's
strict-type assignment rejects that across named types. Use the
field's own declared type directly.
Bumps utils to v1.1.5 (now hosting json subpackage used by other
modules in the codec rip).
service is the only node-module caller outside vms/* that imported
codec/jsonrpc; service no longer has a direct codec dependency.
Phase 1 (components): 8 files — type aliases for Manager/Errs/IntLen/LongLen
Phase 2 (proposervm): 9 files — singletons + IntLen + Packer
Phase 3 (evm/xsvm/rpc): 5 files — predicate results, lp176, xsvm tx codec, linux stopper
Phase 4 (xvm): 18 files — full parser refactor, pcodecsmock for codecmock
Phase 5 (platformvm): 30 files — full V0+V1+V2 codec migration, metadata codec, fee complexity
Zero direct luxfi/codec imports remain in node/vms outside pcodecs.
Mirrors proto/p Wave 2A pattern. node/vms now consumes proto/p new API
(block.NewBlock(c codec, ...), txs.NewSigned(unsigned, c, creds), etc.)
via the pcodecs.Manager type alias.
Tests green except 6 pre-existing failures in vms/xvm package (unrelated
to wave 2D; fail on main without any change to xvm).
Establish vms/pcodecs as the canonical construction site for codec.Manager
and linearcodec / zapcodec instances under node/vms. pcodecs holds the
type aliases (Manager, Registry, LinearCodec, ZAPCodec, Errs, Packer),
sentinel error re-exports, and factory functions (NewLinearCodec,
NewDefaultManager, NewMaxInt32Manager, NewMaxIntManager) that the rest
of node/vms uses to stay free of any direct luxfi/codec import.
Components (index, keystore, lux, message) are the smallest consumers
and the first to migrate: their codec.Manager-typed parameters and
struct fields now reference pcodecs.Manager, the keystore + message
codec.go init() blocks reach for pcodecs.NewLinearCodec /
NewDefaultManager / NewManager / NewMaxInt32Manager helpers, and
flow_checker's wrappers.Errs becomes pcodecs.Errs.
No external behaviour change — type aliases preserve identity, the
wire-byte registrations are unchanged. Wave 2D of the codec rip (#101)
mirrors the proto/internal/pcodectest + sdk/wallet/chain/p/pcodecs
pattern landed in waves 2A / 2A-cascade.
zero direct luxfi/codec imports remain in vms/components.
build: go build ./vms/...
test: go test ./vms/components/...
Mirrors the cgo/nocgo parity test in chains/thresholdvm that proves
the substrate produces byte-identical MPC ceremony state transitions
regardless of build flavor or plugin presence.
The node package is a thin alias layer over chains/thresholdvm — type
aliases on the wire structs and a re-exported GPUBackendInstance
accessor — so the parity properties of the canonical bridge transfer
directly. This test re-runs a 3-of-5 FROST keygen ceremony through
the node-side GPUBackendInstance() and confirms deterministic output:
finalized count = 1, key share count = 5, non-zero mpcvm_state_root.
Passes under both cgo (with or without a dlopen'd plugin) and !cgo.
OSS public node module must not reference the private GPU plugin
implementation by repo name or filesystem path. Removes:
- LUX_PRIVATE_GPU_KERNELS_DIR env vocabulary (use LUX_GPU_PLUGIN_DIR
universal env)
- Dev-tree probe paths to ~/work/lux-private/gpu-kernels/build/...
(platformvm previously hardcoded 5 such paths in searchPaths())
- Prose mentions of "lux-private/gpu-kernels" in package comments
Bridges affected: vms/platformvm, vms/thresholdvm.
All 20 tests PASS post-scrub under both build tags. No functional
behavior change — only path / env / prose.
Requires chains v1.3.1 (chains/thresholdvm re-export target).
Thin re-export layer so consumers importing the legacy node path
github.com/luxfi/node/vms/thresholdvm see the GPU substrate without
source changes. ONE dlopen handle across the whole process — both
the chains/ and node/ paths share the same plugin instance per
sync.Once in chains/thresholdvm/backend.go.
Files (under vms/thresholdvm/):
- thresholdvm_gpu.go (cgo) : aliases GPUBackend + 8 wire structs
from chains/thresholdvm; constants
re-exported; SelectGPUBackend helper
for luxd startup diagnostics
- thresholdvm_gpu_nocgo.go (!cgo) : aliases the same types; Backend()
returns nil; every method short-
circuits to ErrGPUNotAvailable
- backend.go (cgo) : SelectGPUBackend() — one-line
diagnostic string in the cevm.go
pattern (chains/evm/backend_cgo.go)
- thresholdvm_gpu_test.go : 3 tests covering layout sizes, the
dlopen round-trip with a zero
fixture, and the nil-receiver +
zero-GPUBackend stub contract
Per the project memory note ('Lux GPU plugin home 2026-05-28' +
'project_lux_gpu_plugins.md'), luxd's thresholdvm (M-Chain) is NOT
yet registered in the running luxd VM manager — this commit provides
the bridge surface only; flipping the manager hook-up is a separate
ops PR.
Constraints honored:
- Existing thresholdvm.go re-export (chains/thresholdvm alias wrapper)
NOT modified — bridge is opt-in alongside it
- Both build flavors compile: go build + CGO_ENABLED=0 go build
- Tests pass: 3/3 under cgo, 3/3 under nocgo, race-clean
Mirrors the chains/aivm/aivm_gpu.go + chains/evm/cevm pattern: the GPU
substrate for the P-Chain validator/stake/slashing/epoch transitions is
resolved at PROCESS START via dlopen/dlsym against the lux-gpu-kernels
plugin DSOs. Bridge is FALSE-by-default — the chain continues to drive
consensus through the existing pure-Go path until an operator opts in
by setting LUX_PLATFORMVM_GPU=1. Any launcher error from the GPU falls
back to the Go path WITHOUT panic.
Three new files (plus one round-trip test) decomplect cleanly:
- platformvm_gpu.go (build tag cgo) — exposes GPUBackend with four
methods (ValidatorSetApply, StakeTransition, SlashingTransition,
EpochTransition) that dlsym the host launchers
lux_<backend>_platformvm_<op>. Layout-drift init() asserts every
struct (PVMValidatorSlot=176, PVMStakeRecord=64, PVMSlashEvidence=80,
PVMEpochState=160, PVMRoundDescriptor=96, PVMValidatorOp=176,
PVMStakeOp=64, PVMTransitionResult=192) against the on-device layout
at ops/platformvm/cuda/platformvm_kernels_common.cuh + the
authoritative platformvm_gpu_layout.hpp.
- platformvm_gpu_nocgo.go (build tag !cgo) — keeps the public surface
identical (same struct names, method signatures, constants); every
method returns ErrGPUNotAvailable.
- backend.go — pure-Go probe driver. Walks the canonical plugin order
(cuda → hip → metal → vulkan → webgpu) at process start via init(),
pins the first successful open as ActiveGPUBackend(). AutoBackend()
is the explicit re-entry point. Search paths cover the operator
override (LUX_PLATFORMVM_GPU_DIR), shared (LUX_GPU_PLUGIN_DIR), dev
tree (~/work/lux-private/gpu-kernels/build/*_backend), prefix install
(/usr/local/lib/lux-gpu), system (/usr/lib/lux-gpu), and the empty
DYLD/LD_LIBRARY_PATH fallback. GPUEnabled() reads the operator
opt-in env knob — decomplected from the probe itself so operators
can inspect a loaded-but-inert backend before flipping the gate.
- platformvm_gpu_test.go — three subtests:
1. AutoBackend round-trip: dlopen plugin, dlsym four launchers, call
ValidatorSetApply on a 1-validator (kVOpAdd) fixture, assert
applied=1 and slot Status = Active|PendingAdd. SKIPs cleanly when
no plugin is on disk (the production default).
2. Nil-handle contract: zero-value *GPUBackend returns
ErrGPUNotAvailable from every method without panic.
3. Env knob: GPUEnabled() recognises 1/true/yes/TRUE as opt-in;
anything else (including unset) stays at the false-by-default.
Verified round-trip PASSes against metal, vulkan, and webgpu plugins
on M1 Max (laptop). CUDA / HIP are the linux NVIDIA/AMD paths and
were verified in the spark.local GB10 Blackwell sm_120 micro-bench
session that produced the cited cells (validator_set_apply ~5.5ms,
stake_transition ~3.0ms, slashing_transition ~0.06ms, epoch transition
~7.0ms — gpu-kernels HEAD 4e166de).
Does NOT modify vm.go / block.go / state.go core paths; this is the
bridge surface only. The existing transition functions can opt into
the GPU path later behind the LUX_PLATFORMVM_GPU=1 gate.
All builds succeed: go build, go build -tags cgo, CGO_ENABLED=0 go
build. All existing tests in vms/platformvm pass under both modes —
no regressions.
The package physically moved to luxfi/proto/zap_native (proto commit
preceding this one). Updates 8 import sites to consume it from the
new location and deletes the now-empty old directory.
Sites updated:
- vms/platformvm/txs/bench/*.go (6 bench tests)
- vms/platformvm/network/zap_native_admission{.go,_test.go}
Build clean: go build ./vms/platformvm/...
6de839a515 (codec Wave 1D) migrated the wallet UTXO loader to
lux.ParseUTXO (ZAP wire dispatcher) before the platformvm GetUTXOs
server-side encoder was migrated. Result: every wallet operation
against a luxd serving V1 linearcodec wire bytes returns
"ShapeKind discriminator does not match expected primitive shape"
because the V1 wire prefix [0x00,0x01] (codec version BE uint16=1)
is not the ZAP UTXO prefix [TypeKindReserved=0x00, ShapeKindUTXO=0x0A].
Lux-mainnet validators (v1.28.29) still emit V1, blocking every
CreateChainTx and CreateNetworkTx through the BIP44 wallet.
Fix: dispatch on the 2-byte envelope prefix in AddAllUTXOs. ZAP
envelopes (byte[1] == ShapeKindUTXO) route through lux.ParseUTXO;
anything else falls through to ptxs.Codec.Unmarshal. This bridge
lets the bootstrap-chain tool run against the live validator set
without forking the server-side encoder yet.
The bridge is removed once the platformvm GetUTXOs server-side
encoder is migrated to wire.NewUTXO (tracked separately).
Verified: bootstrap-chain --uri=https://api.lux.network now signs +
issues CreateChainTx successfully (osage L1 added to mainnet at
blockchainID 2ahV62kvM1KWkgxL6MiF5hPYBVwWuwpPqwV3RhqM6hCxZT68uo).
Wave 1D of the luxfi/codec final rip: leaf-misc tier under node/.
This batch removes the direct codec/wrappers, codec/linearcodec, and
codec.Manager imports from p2p network protocol, IP/port encoding,
indexer/keystore on-disk storage, warp socket IPC, x/archivedb +
merkledb internals, and the primary-network wallet UTXO loader.
Migration shape:
(a) codec/wrappers.Packer + length constants
→ node/utils/wrappers (same shape, already in tree as the local
canonical home for binary IO helpers). 14 files: indexer/,
network/, utils/ips/, utils/metric/, warp/, x/.
(b) codec.Manager + linearcodec for on-disk container storage
→ hand-rolled big-endian binary marshal/unmarshal. Hard cut;
no codec-version prefix; payload size bounded explicitly:
- indexer/codec.go: marshalContainer/unmarshalContainer
- service/keystore/codec.go: marshalHash/unmarshalHash and
marshalUser/unmarshalUser, 16 MiB blob cap retained.
(c) codec.Manager for cross-chain UTXO parsing (wallet/network/primary)
→ utxo.ParseUTXO via the ZAP wire dispatcher already registered
by node/vms/components/lux. Drops the per-chain codec.Manager
slot from FetchState; AddAllUTXOs no longer takes a codec.
Underscore-import of vms/components/lux ensures the dispatcher
is wired even for callers that don't already pull platformvm.
No snow/snowman/snowball/avalanche/avm/subnet residue introduced.
Adds CodecVersionV2 (zapcodec, little-endian) alongside existing V0/V1
(linearcodec, big-endian) on the txs.Codec / txs.GenesisCodec managers.
The codec.Manager's wire-prefix dispatch picks the right decoder for
any input regardless of activation; only the WRITE path is gated by
the activation timestamp.
ZAPCodecActivationTimestamp = 1782864000 (2026-07-01 00:00:00 UTC)
CodecVersionForTimestamp(ts):
ts < activation → V1 (linearcodec)
ts >= activation → V2 (zapcodec)
CodecForTimestamp(ts) → txs.Codec (same handle; future-proof for a
full manager swap after V1
retirement)
CodecAllowsRead(v) → v ∈ {V0, V1, V2}
CodecRequiresLegacy(v) → v ∈ {V0, V1}
Slot map is bit-identical across V1 and V2: registerV1TxTypes is now
parameterised over a local slotRegistrar interface that both
linearcodec.Codec and zapcodec.Codec satisfy. Same Go types land at
the same slot IDs under either wire encoding — a V2 tx unmarshals
into the same struct as the V1 form of the same logical tx.
Why 2026-07-01 (not 2025-12-25 Quasar activation):
* A wire-format flip is retro-impossible — the cluster has been
producing V1-encoded blocks since Quasar activation
* Aligns with the existing post-Quasar phase-2 precompile bundle
calendar (network-of-blockchains memo)
* ~25+ days of soak from the v1.28.x ship date — every validator
binary has the V2 decoder registered well before the write switch
Activation constant lives in codec_activation.go for clear separation.
Tests (12 new, all passing — existing 2 unchanged):
TestCodecVersionForTimestamp_StrictBoundary — bit-exact ts == act flip
TestCodecForTimestamp_ManagerIsStable — manager handle is stable
TestPreActivationRoundTripV1 — ts < act → V1 round-trip
TestPostActivationRoundTripV2 — ts >= act → V2 round-trip
TestCrossVersionWireIsDistinct — V1 ≠ V2 wire bytes
TestCodecAllowsRead — read-acceptance gate
TestCodecRequiresLegacy — legacy classifier
TestV2WireIsZapNative — byte-position LE assertions
TestV1WireIsBigEndian — byte-position BE assertions
TestActivationConstantUnchanged — constant value watchdog
All existing platformvm txs / block / executor / fee / mempool /
txheap / zap_native tests still pass.
Root cause of mainnet/testnet/devnet `platform.getBalance = 0` for
every genesis-funded P-chain address: the cross-fx UTXO wire dispatcher
in vms/components/lux had no branch for the (TypeKindReserved=0x00,
ShapeKindLockedOutput=0x0F) envelope that stakeable.LockOut.Bytes()
produces. Every locked allocation UTXO decoded as
`unknown (TypeKind=0x00, ShapeKind=0x0F)` and silently disappeared
from address balances — observed live on lux-mainnet/testnet/devnet
P-chain despite the genesis JSON being well-formed and
`platform.getCurrentSupply` correctly tallying the sum of allocations
(13.27B LUX).
Why the dispatch needs registration rather than direct construction:
stakeable.LockOut embeds lux.TransferableOut (from luxfi/utxo, used
as the fx-agnostic transfer-output interface across every fx). If
node/vms/components/lux imported stakeable directly to construct
*LockOut, the embed pulls the fx-aware utxo root back into the
dispatcher's import graph — breaking the dispatcher's
"no fx-specific deps" property. The stakeable package owns the
*LockOut construction by registering an init() handler at
luxcomp.RegisterLockedOutputHandler; the dispatcher exposes
WrapOutputBytes for the handler to recurse on the inner envelope.
Surface added:
- vms/components/lux/utxo_parser.go:
- LockedOutputHandler type
- RegisterLockedOutputHandler(h) (init-only, panics on
double-install)
- WrapOutputBytes(b) — discriminator-peek + dispatch, used by the
registered handler for inner-envelope recursion
- wrapOutput now routes (TypeKindReserved, ShapeKindLockedOutput)
through lockedOutputHandler
- vms/platformvm/stakeable/stakeable_lock.go:
- init() registers a handler that wire.WrapLockedOutput → recurse
via luxcomp.WrapOutputBytes → cast inner to lux.TransferableOut →
return *LockOut{Locktime, TransferableOut: inner}
luxfi/utxo bumped to v0.3.7 to pick up the public wire.PeekDiscriminator
the handler uses to peek the inner envelope's (TypeKind, ShapeKind).
Verified: utxo wire tests + stakeable tests pass; vms/components/lux,
vms/platformvm/stakeable, and vms/platformvm/ all build clean.
P-Chain syncGenesis on mainnet/testnet was failing with
`UTXO.Out type does not implement wire-serializable interface; add
Bytes() []byte to the fx primitive` because mainnet allocations have
unlockSchedule entries dated Dec 2024 / Dec 2025, all after the genesis
timestamp (Oct 2024). genesis.go wraps such allocs as *stakeable.LockOut,
which lacked Bytes().
LockOut.Bytes() now encodes (Locktime, inner.Bytes()) via
wire.NewLockedOutput (added in luxfi/utxo v0.3.6, ShapeKind=0x0F).
Bumps luxfi/utxo v0.3.5 -> v0.3.6.
Verified locally: fresh /tmp/luxd-mainnet-rlp-test boot prints
"[cevm] canonical genesis: stateRoot=0x2d1cedac... hash=0x3f4fa2a0..."
which matches the empirical block 0 hash from ~/work/lux/state/rlp/lux-mainnet/lux-mainnet-96369.rlp.
PR #132 (kill-etna) squash-merge regressed luxfi/ids v1.2.14 -> v1.2.13.
v1.2.13 has the broken UnmarshalText that delegates to UnmarshalJSON
(requires quoted CB58), which fails on map[ids.ID][]string keys passed
unquoted via TextUnmarshaler contract. v1.2.14 (commit 6471495ff1 on
ids repo) split the methods cleanly. Without this, v1.28.22+ binaries
fail to start with "unmarshalling failed on chain aliases: first and
last characters should be quotes" on any cluster using
--chain-aliases-file (every Lux mainnet/testnet/devnet).
Root cause: the quasar/evm-v0.19.0 branch was created from an older
node main that pre-dated the ids v1.2.13 -> v1.2.14 bump. Squash-merge
preserved the stale go.mod. Reapplied v1.2.14 explicitly.
Phase 6 of the LP-023 ZAP migration. luxfi/utxo v0.3.5 (commit 559e482)
drops codec.Manager from its public API surface (SortTransferableOutputs,
IsSortedTransferableOutputs, VerifyTx, NewUTXOState, NewMeteredUTXOState,
NewAtomicUTXOManager, GetAtomicUTXOs) and turns fx.Initialize into a
no-op. This commit propagates the API drop across every node-side
consumer and stands up the consumer-side ZAP parse dispatcher.
Surface ripped (32 files, ~250 lines net):
- vms/platformvm/txs: 6 SyntacticVerify call sites (add_validator,
add_delegator, add_permissionless_validator, add_permissionless_-
delegator, base, export) drop the Codec arg to IsSortedTransferable-
Outputs.
- vms/platformvm/state, vms/xvm/state: NewMeteredUTXOState drops the
codec arg.
- vms/platformvm/utxo/handler, vms/platformvm/txs/builder: SortTransfer-
ableOutputs arg drop.
- vms/xvm/txs/executor/syntactic_verifier: 5 lux.VerifyTx call sites
drop v.Codec.
- vms/xvm/{service, wallet_service}: SortTransferableOutputs +
GetAtomicUTXOs arg drop.
- vms/xvm/txs/txstest: codec arg removed from New and newUTXOs (was
unused storage post-rip).
- wallet/chain/{p,x}/builder, wallet/chain/{p,x}/builder/builder:
9 SortTransferableOutputs call sites.
- Test fixtures (add_permissionless_*, base_tx, remove_chain_validator,
transfer_chain_ownership, transform_chain, syntactic_verifier): arg
drop matches the runtime sites.
New: vms/components/lux/utxo_parser.go — the luxd-side ZAP wire
dispatcher. utxo.ParseUTXO is consumer-registered via
utxo.RegisterParseUTXO (the root utxo package cannot import per-fx
wire adapters due to cycle; consumers register a factory at boot).
The dispatcher routes on (wire.TypeKind, wire.ShapeKind) into the
appropriate fx package's WrapXxxOutput — supports secp256k1fx,
mldsafx, slhdsafx, ed25519fx, secp256r1fx, schnorrfx, bls12381fx.
vms/xvm/txs/parser.go: utxo v0.3.5 secp256k1fx.Fx.Initialize is a
no-op (no codec RegisterType chain). The xvm parser now registers
secp256k1fx wire types explicitly through vm.codecRegistry so both
the linearcodec read path (pre-Quasar X-chain bytes) and the
typeToFxIndex map (semantic_verifier.getFx) stay populated.
vms/{xvm,platformvm}/state/state.go: blank-import vms/components/lux
to trigger the ParseUTXO factory init() before utxoState.GetUTXO
deserializes off disk.
Activation: ZAPActivationUnix=0 preserved (always-on, per LP-023).
Legacy V0/V1 codec slot maps retained as READ-ONLY decoders behind
LUXD_ENABLE_LEGACY_CODEC=1 — mainnet/testnet block history continues
to deserialize.
Build/test status:
- go build ./... clean.
- go vet ./... clean (modulo 1 pre-existing sync/atomic copy warning
in vms/rpcchainvm/zap/cevm_e2e_test.go unrelated to this rip).
- xvm txs/executor + xvm state + xvm network + xvm txs + xvm
genesis + wallet/chain/p/builder + wallet/chain/x/builder ALL
PASS under -short.
- vms/platformvm/txs serialization tests (TestAdd*Serialization)
fail on byte-baseline expected outputs because utxo's new sort
key is (AssetID, wireBytes()) vs the old codec-marshal bytes —
the test fixtures need regen under the new canonical sort. Same
story for xvm/state_test.go TestFundingAddresses / TestVerifyFxUsage
using utxo.TestState whose Out type doesn't satisfy
wireSerializable. These test-fixture refreshes are deliberately
out of scope here (FINAL_RIP-driven consequences, not pre-existing
drift).
go.mod: luxfi/utxo v0.3.2 -> v0.3.5, luxfi/api transitive v1.0.11
-> v1.0.12. luxfi/codec v1.1.4 stays direct (network/peer, warp
internal, indexer, keystore et al. still use codec.Manager for
non-UTXO concerns; those packages are out of UTXO-rip scope).
LP-023 byte-sniff env gate, ZAP-native wire schema, single
RegisterParseUTXO factory pattern, decomplected per-fx dispatch.
Final Corona-residue sweep in vms/platformvm/warp:
- HybridBLSRTSignature → HybridBLSCoronaSignature (the lingering "RT"
suffix in the Go type for the deprecated BLS+lattice hybrid).
- ErrInvalidRTSignature → ErrInvalidCoronaSignature.
- ErrMissingRTPublicKey → ErrMissingCoronaPublicKey.
- String() format, comments, doc references updated to Corona.
Wire-format invariants (this is on-chain encoding):
- linearcodec assigns typeIDs by RegisterType call order. The order
in codec.go is UNCHANGED: BitSetSignature (0x00), CoronaSignature
(0x01), EncryptedWarpPayload (0x02), HybridBLSCoronaSignature
(0x03), then the 3 Teleport types (0x04-0x06).
- linearcodec serializes struct fields by declaration order (see
reflectcodec/struct_fielder.go). Field declaration order is
UNCHANGED for every type in this PR.
- Therefore: type names and field names are wire-metadata only;
renaming them produces byte-equal output.
Wire bytes verified byte-equal pre/post rename for every codec-
registered type via the new regression test:
vms/platformvm/warp/wire_baseline_test.go
That test pins the exact hex of every type's encoding (concrete and
through the Signature interface) and is now a permanent guard against
accidental wire-format drift.
Doc sweep (no code dependencies — example/aspirational JSON keys):
- docs/architecture/consensus.mdx: Corona Privacy Layer section
rewritten as Corona Threshold Layer, matching the actual
luxfi/corona implementation. The previous text described a ring-
signature scheme that doesn't exist in this codebase.
- docs/architecture/overview.mdx: Q-Chain diagram + cryptographic
primitives table.
- docs/configuration/node-config.mdx + docs/getting-started/running.mdx:
example Q-Chain config keys (corona-* → corona-*).
- docker/Dockerfile.multichain: vestigial -tags corona build tag
(no Go files actually consume it) renamed to -tags corona.
Verification:
go build ./... exit 0
go vet ./vms/platformvm/warp/... clean
go test ./vms/platformvm/warp/... all packages PASS (warp,
message, payload, zwarp)
v0.19.3 pulls vm v1.1.7 + runtime v1.1.0. runtime v1.1.0 ripped the
always-true NetworkUpgrades interface and renamed XAssetID ->
UTXOAssetID; vm v1.1.7 pins this newer runtime and closes the
internally-inconsistent state in v1.1.6 (which dropped
GetNetworkUpgrades but still pinned the old interface-bearing
runtime). Builds cleanly now.
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 v0.19.0 Etna->Quasar rename: vm v1.1.5 still expected upgrade.Config
to implement runtime.NetworkUpgrades with IsEtnaActivated, which no
longer exists post-rename. v1.28.22 and v1.28.23 Docker builds failed
on this signature mismatch; v0.19.2 closes the cascade.
v0.19.1 bundles 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 via Key(), fixing a collision
where all 7 mapped to G1AddConfigKey. Without the fix, only one of
the 7 entries could land in the upgrade registry — DisallowUnknownFields
parse (enforced as of evm 415b2a276) rejected the other six.
This unblocks re-adding the 7 bls12381*Config entries to mainnet
configs/mainnet/upgrade.json forward-dated to the already-past Quasar
horizon (Unix 1766708400 = 2025-12-25 16:20 PT). Testnet + devnet
upgrade.json already carry these entries (testnet at 1766708400,
devnet at 0).
* Dockerfile: bump EVM plugin v0.18.18 → v0.19.0 (Quasar Edition)
Quasar Edition rip — one and only one upgrade-key namespace:
- luxfi/evm v0.19.0 renames EtnaTimestamp → QuasarTimestamp
(Go field + json tag)
- Strict json.Decoder DisallowUnknownFields on upgradeBytes parse
in plugin/evm/vm.go — any stale etnaTimestamp in a deployed
upgrade.json now fails parse loudly at boot rather than silently
leaving the fork field nil and disabling activation.
Pairs with: luxfi/genesis kill-etna sweep (configs/*/upgrade.json
and brand L1 EVM genesis.json scrubbed).
* docker-entrypoint: rip etnaTimestamp from embedded C-Chain genesis
With luxfi/evm v0.19.0 strict upgradeBytes decode, the entrypoint's
embedded C-Chain genesis heredoc would parse-fail on etnaTimestamp.
One name. quasarTimestamp.
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.
Adds an opt-in escape hatch that pins the fee-payment asset to a
specific 32-byte asset ID, bypassing platform.getStakingAssetID. Use on
networks where the live staking asset differs from the legacy LUX asset
on existing P-chain UTXOs.
lux-mainnet today is the documented case: staking ==
pmSJ7BfZQfwtUGbamLWSLFLFGnocfMfbriDTsYWxi4qnqFrrT but the historical
deployer's UTXOs hold HrJCm4yvNmyPDA1PEqwks9iFFmoRJEsLJj36N1xtkffrqpL6p.
Without the override the wallet's `utxoAssetID` matches the staking-asset
result and `platform.getBalance` returns 0 for the legacy UTXOs — the
wallet can't find them as fee-payment material.
NOTE: this addresses the wallet-side identification only. The P-chain
fee-execution rule still requires the staking asset on mainnet; a
follow-up legacy-to-staking asset migration tx is needed before
CreateNetworkTx + CreateChainTx will go through on mainnet. Tracked as
the 2026-06-03 mainnet brand L2 re-fire blocker.
Empty / unset is a no-op. Tested via bootstrap-chain mainnet dry-run
(2026-06-03): override correctly switches the wallet to legacy-LUX
UTXOs; CreateNetworkTx then fails with "insufficient unlocked funds"
because the fee verifier checks pmSJ7BfZ... balance — which is the
expected second-half blocker.
ZAPActivationUnix is now 0 (LP-023 cutover, 2026-06-02). The assertion
`ShouldUseZAPForWrite(ZAPActivationUnix - 1)` underflows uint64 to
math.MaxUint64; with ZAPActivationUnix=0 the LegacyEnabled path
degenerates to `ts >= 0` which is true for every input, so the prior
"pre-activation picks legacy" assertion can never hold.
Rewrite runEnableLegacyChild to mirror zap_native.security_test.V15
closure: probe a timestamp spread including the historical
forward-date (1782604800) and assert ShouldUseZAPForWrite=true
unconditionally. Pins the always-on invariant on the bench surface
too.
No production code change.
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.
R6V4 (CRITICAL): CreateSovereignL1Tx.Verify now rejects zero-validator and
zero-chain wire buffers (both halt consensus at activation). Per-validator
Weight > 0 walk also fires here. RegistrationExpiry remains an executor
clock concern (deferred to staking handler, documented).
R6V8 (HIGH): TransferChainOwnershipTx had no Verify() despite carrying
Owner fields. Wired in: reconstructs OwnerStub from the (threshold,
locktime, address) tuple and calls SyntacticVerify. Tx count with
SyntacticVerify wired into 8 tx types (was 7).
R6V3 (HIGH): BLS PoP now verified at the SyntacticVerify boundary for
every initial validator. Wire-layer opaque 48B BLSPubKey + 96B BLSPoP
now pairing-checked via bls.VerifyProofOfPossession with new ErrBadBLSPoP.
Closes the zero-downstream-consumer gap Red grep found.
R6V5 (MEDIUM): ChainsListView.Verify walks entries and rejects any
FxIDsLen that is not a multiple of FxIDSize. Replaces the silent-nil
return path in BoundChainEntry.FxIDs with a typed error. Wired into
CreateSovereignL1Tx.Verify via ChainsListView.Verify().
Tests (all under go test -race):
TestCreateSovereignL1Tx_Verify_RejectsZeroValidators
TestCreateSovereignL1Tx_Verify_RejectsZeroChains
TestCreateSovereignL1Tx_Verify_RejectsZeroWeight
TestCreateSovereignL1Tx_Verify_RejectsBadBLSPoP
TestCreateSovereignL1Tx_Verify_RejectsBadBLSPoP_MismatchedPubKey
TestCreateSovereignL1Tx_Verify_AdversarialWireBuffer_ValidatorWeight
TestChainsList_Verify_RejectsBadFxIDsLen (adversarial wire buffer)
TestChainsListView_Verify_StandaloneEntries
TestTransferChainOwnershipTx_Verify_RejectsZeroThreshold
TestTransferChainOwnershipTx_Verify_RejectsThresholdAboveOne
TestTransferChainOwnershipTx_Verify_AdversarialWireBuffer
TestBLSSurfaceReachable
Updates TestVerify_AcceptsWellFormed/CreateSovereignL1 to supply a
properly-constructed validator (real BLS PoP) and a chain entry so the
new gates fire green on the legitimate path. Adds
TestVerify_AcceptsWellFormed/TransferChainOwnership subtest for R6V8.
New typed errors:
ErrZeroValidators, ErrZeroChains, ErrValidatorWeightZero,
ErrBadBLSPoP, ErrMalformedFxIDsLen.
Bench unchanged (Verify is on executor path, parse benches structurally
untouched). Full zap_native suite green under -race.
LP-023 batch 5 v3 closes Red round 6 V3/V4/V5/V8.
ZAP wire is now mandatory from genesis. The forward-dated 2026-07-01
(1782604800) cutover is dead — replaced with always-on semantics. New
deployments, fresh syncs, and all production binaries from v1.28.19
onward are ZAP-only by construction.
The legacy linearcodec is reachable only via the explicit dev knob
LUXD_ENABLE_LEGACY_CODEC=1, and only for the read path (decoding
pre-2026-06-02 archival linearcodec bytes from disk). On the write
path, LegacyEnabled has no semantic effect once activation = 0 — the
"pre-activation" window is empty, so every block timestamp satisfies
blockTimestamp >= 0 and writes are always ZAP.
Tests:
- TestZAPActivationUnixIsAlwaysOn pins the constant to 0 so any
regression that re-introduces a forward-date guard fails this gate.
- TestShouldUseZAPForWrite legacy-enabled subtest asserts ZAP for
every timestamp (replaces the previous pre/at/post-activation
table that depended on a non-zero gate).
- TestRed_V15_PreActivationZAPTxRejection updated to reflect that
V15's threat model (legacy cutover-window fork) is no longer
applicable; the test now asserts the always-on invariant.
Authorized by 2026-06-02 destructive recovery sweep — "just do it
right; no half-measures, no legacy compatibility, clean slate."
Phase C — Multi-chain CreateSovereignL1 ChainsList primitive (chains_list.go,
chains_list_test.go):
* 64-byte fixed-stride ChainEntry per chain: (NameRel/Len uint32, VMID 32B,
FxIDsRel/Len uint32, GenesisDataRel/Len uint32, Reserved 8B).
* Three sibling blob arrays (NameBlobs, FxIDsBlobs, GenesisDataBlobs)
carried as parent-tx fields; entry header stores (rel, len) cursors.
* Bind(nameBlobs, fxIDsBlobs, genesisDataBlobs) compile-time gate
mirroring BoundEvidenceList (NEW-V2): ChainsListView lacks safe
Name/FxIDs/GenesisData accessors so consumers cannot bypass Bind() by
accident. Only BoundChainEntry exposes the safe accessors.
* IsNull() zero-value guard on ChainEntry: out-of-range At(i) returns
a ChainEntry{} whose accessors short-circuit to zero rather than
nil-deref the underlying zap.Object's msg. Defense-in-depth atop the
defensive At(i) clamp.
* Per-element FxIDs blob is a length-prefixed byte array; entry count =
FxIDsLen / FxIDSize (32B per ids.ID). Non-multiple lengths clamp to nil.
* CreateSovereignL1Tx grows from a single-chain stub (TxKind 23, batch 4)
to a real multi-chain L1 builder: ChainsList + NameBlobs + FxIDsBlobs +
GenesisDataBlobs fields. Size 193 bytes (was 121).
Phase D — ValidatorsList encoding (validators_list.go,
validators_list_test.go):
* 180-byte fixed-stride ValidatorRecord per initial validator: NodeID 20B
+ Weight uint64 + BLSPubKey 48B + BLSPoP 96B + RegistrationExpiry uint64.
* Uses zap.Object.ListStride from v0.7.2 — per-element clamp against
poisoned wire length fields (R4V9).
* No sibling arrays needed — every field is fixed-size.
* BLSPubKey() and BLSPoP() return COPIED slices (not aliased to parent
buffer) so consumer mutation cannot corrupt subsequent reads.
* IsNull() zero-value guard mirroring ChainEntry.
* ConvertNetworkToL1Tx now carries the real ValidatorsList — the
previous (0, 0) stub is gone. Size still 165 bytes (the validators
list pointer was already provisioned).
* CreateSovereignL1Tx integrates ValidatorsList alongside ChainsList for
the full atomic sovereign-L1 commit shape.
Phase E — Bench refresh:
* M1 Max Parse geomean across 10 measurable tx types post-batch-5:
7.44× at -benchtime=2s -count=3 (medians).
* Restricted to the original 9 tx types from the v0.7.2 baseline:
7.74×, within noise of the 7.71× pre-batch-5 number.
* R4V7 SyntacticVerify lives on the executor Verify() path (not the
wire parser), so the parse benchmarks are structurally unchanged.
* ChainsList + ValidatorsList add new primitives but do not appear in
the legacy comparison fixtures.
Test coverage:
* chains_list_test.go: RoundTrip (3 chains incl. empty FxIDs+empty
GenesisData), EmptyList, OutOfRange, UnboundReturnsRawCursors,
BindMismatchedBlobsClampsSafely, FxIDsNonMultipleClampsToNil.
* validators_list_test.go: RoundTrip (2 validators), EmptyList,
OutOfRange, PoisonedLengthClampedByStride,
BLSFieldsAreReadOnly (mutation isolation),
CreateSovereignL1Integration (ChainsList + ValidatorsList together).
* batch4_tx_test.go: updated CreateSovereignL1TxRoundTrip for the new
multi-chain + validators shape; tests both round-trip and Wrap()
re-parse correctness.
All zap_native tests pass under go test -race.
Coordinates with cryptographer #114 v3 work and CTO bootstrap-chain work;
neither genesis/ nor bootstrap touched.
LP-023 batch 5 Phase C+D+E.
R4V3 finding: a malicious wire-encoded AddressList may report Len() >
actual entry count (the ListStride per-element clamp accepts the honest
overcount as long as length*stride fits the remaining buffer). At(i)
for i >= actual_count returns whatever bytes occupy the post-list
region in the buffer — often zero-padding, sometimes adjacent buffer
content.
Audit result: AddressList has ZERO production consumers in the
executor today (the executor still uses secp256k1fx.OutputOwners +
message.PChainOwner via the legacy codec). The new primitive is
shipping ahead of the executor migration, so the discipline is
forward-looking. Audited grep `\.At(|AddressList` across
~/work/lux/node — every match is internal to the zap_native package
or test code.
Defense path (canonical):
- Owner.SyntacticVerify() now walks the list and rejects any zero
ShortID with ErrOwnerAddrZero. This closes the zero-phantom
bypass at the gate.
- Documented consumer-safety contract on AddressList type docstring
AND on .Len()/.At() — three paths: non-zero check at call site,
sibling-count correlation, or canonical SyntacticVerify boundary
(path 3 is the one-and-only-one way).
Test scope:
- r4v3_addresslist_test.go — overcount construction with the wire
layer (claim 5, real 4, 1-phantom); confirms SyntacticVerify
rejects the zero-phantom case and accepts the buffer-garbage
case (signature validation downstream closes the remaining
surface in the garbage path — no zero co-signer can sneak
through the quorum gate).
- Honest-list happy path also pinned.
Tradeoffs:
- SyntacticVerify is now O(Len()) per Owner instead of O(1). For
typical owners (1-5 addresses) this is negligible; the gate is
the authorization boundary and bears the cost.
Wire layer (zap_native parser) is permissive by design — it confirms
TxKind + buffer geometry only. Executor-side semantic gates live HERE
on the consumer-side boundary.
Owner.SyntacticVerify enforces:
- ErrOwnerAddrsEmpty: Addresses.Len() == 0 (signer set undefined)
- ErrOwnerThresholdZero: threshold == 0 (auth bypass)
- ErrOwnerThresholdExceedsAddrs: threshold > Addresses.Len()
(unsatisfiable quorum)
OwnerStub.SyntacticVerify enforces the single-address fast path:
- Threshold must be exactly 1 (zero is bypass, >1 is unsatisfiable
because the stub carries one address by construction)
Per-tx Verify() entry points wire the gate into all 7 tx types that
embed Owner-shaped fields:
- AddValidatorTx.RewardsOwner
- AddDelegatorTx.DelegationRewardsOwner
- AddPermissionlessValidatorTx.{Validation,Delegation}RewardsOwner
- AddPermissionlessDelegatorTx.DelegationRewardsOwner
- CreateChainTx.Owner
- CreateNetworkTx.Owner
- CreateSovereignL1Tx.Owner
Test scope (TDD red→green):
- owner_syntactic_test.go — 7 cases on Owner + OwnerStub directly
- tx_verify_test.go — 7 well-formed + 7 malicious-threshold + 1
multi-owner accept + 1 adversarial-wire-buffer test that overwrites
the threshold byte in the buffer and re-Wrap, confirming the gate
fires on byte-stream attacks (not only constructor input).
Contract (LP-023 Red round 4 R4V7): every tx executor's Verify() entry
point MUST call tx.Verify() before treating embedded Owner fields as
authoritative. Skipping Verify() opens the auth-bypass attack vector.
Three surgical fixes against Red round 4 findings (task #189).
R4V9 (MEDIUM) — migrate 6 list accessors from Object.List() to
Object.ListStride() for the per-stride poisoned-length clamp added in
zap v0.7.2. Before the migration, a wire length field that satisfied
the permissive `length <= len(buf)` baseline (bare List accepts) but
failed the tighter `length * stride > bufRem` bound would slip past
the accessor and return a List view with Len() == poisoned_length,
opening per-element OOB reads on stride-> 1 entries (silent zeros via
Object.Uint{8,32,64}, but consumer iterates ghost entries). Migrated:
- CredentialListView → SizeCredential (stride 16)
- SignatureArrayView → SigBlobSize (stride 65)
- InputListView → SizeTransferableInput (stride 88)
- SigIndicesArrayView → SizeSigIndex (stride 4, new const)
- OutputListView → SizeTransferableOutput (stride 96)
- NewEvidenceListView → SizeEvidenceEntry (stride 48)
Also added defensive `i >= Len()` bounds checks to the four At()
methods that construct sub-Objects from list.Object() — without the
guard, At(0) on a clamped Len()=0 view would build a Credential /
TransferableInput / TransferableOutput / EvidenceEntry wrapping a
zero-value zap.Object (msg=nil) and panic on downstream field
reads. (SignatureArray.At and SigIndicesArray.At already had the
guard.)
New regression suite r4v9_liststride_test.go covers all 6 accessors
plus a false-positive guard verifying honest single-entry lists still
round-trip. Pattern follows existing TestNewV1_ListStrideTighterClamp
in zap: build honest list, overwrite wire length with poisoned value
chosen to satisfy bare clamp but fail stride clamp, parse, confirm
Len()==0 and At(0) is panic-safe.
R4V12 (MEDIUM) — rename `Subnet` to `Chain` in bench fixtures
all_types_bench_test.go: legacySlashValidatorTx + legacyRemoveChainValidatorTx
struct field + their 4 construction sites (lines 55, 68, 444, 476,
552, 584 in original). Bench-only legacy types used for parity
benchmarks; rename is audit-hygiene per the rebrand sweep (#187).
Grep gate `grep -rn "Subnet" ... | grep -v "//"` is now empty.
R4V14 (INFO) — fix docstring drift in add_chain_validator_tx.go: the
"Fixed-section layout (size 165 bytes)" comment said 165, but the
SizeAddChainValidatorTx constant correctly says 161. Updated comment
to match constant; verified by arithmetic (last field Chain @ 129 +
32 bytes = 161, not 165).
Verification:
- go test -race -count=1 ./vms/platformvm/txs/zap_native/... → ok
- 7 new R4V9 tests pass (6 poisoned-length regressions + 1 honest
round-trip).
- grep gate for R4V12 is empty.
- Parse benchmarks (M1 Max, 200ms): geomean 7.70x (named-tx-only),
within noise of pre-fix 7.71x baseline.
zap_native-side only — luxfi/zap requires no bump.
Bumps luxfi/zap to v0.7.1 which closes the underlying wire-layer gaps
(uncapped Object.List length, backward-pointer header aliasing, size=0
Parse), and reworks the zap_native package to:
- Use zap.Version2 as the schema-version gate. Every Wrap*Tx now routes
through parseAndCheckKind(b, want), which rejects any Version1 buffer
with ErrWrongSchemaVersion before TxKind interpretation. Closes the
v2-vs-v3 cross-schema confusion (RED-MEDIUM-1) where a v2-shaped
BaseTx with NetworkID=11 had byte 0 == TxKindBaseFull == 0x0B.
- Add EvidenceList.Bind(messageBlobs, signatureBlobs) → EvidenceList,
and safe accessors EvidenceEntry.MessageA/B + SignatureA/B that clamp
wire (Rel,Len) cursors against parent-blob length. Returns empty
slice on poisoned cursors (RED-HIGH-3) instead of panicking on
mb[rel:rel+len]. The raw *Range() accessors are now documented UNSAFE
and kept only for internal/test use.
- Add SigIndicesArray.Slice(start, count uint32) → []uint32 and
SignatureArray.Slice(start, count uint32) → [][SigBlobSize]byte.
Both methods were referenced in comments but missing; consumers
indexing .At() in a loop with attacker-controlled start/count would
have iterated 4G times on poisoned wire (RED-HIGH-3 follow-on).
- Update batch3_test.go::TestEvidenceListRoundTrip to use the safe
bound accessors (the prior `mb[mARel:mARel+mALen]` pattern is exactly
the consumer-side panic surface Red demonstrated).
Regression tests added to security_test.go:
TestRedRound2_HIGH3_EvidenceListSafeAccessorsClamp
TestRedRound2_HIGH3_SigIndicesArraySliceClamp
TestRedRound2_HIGH3_SignatureArraySliceClamp
TestRedRound2_MEDIUM1_V2BufferRejectedAtSchemaGate
TestRedRound2_MEDIUM1_HonestV2BuffersStillWork
Geomean Parse speedup vs legacy codec: 7.03× (was 7.09×; +0.5% noise).
Schema v3 (TxKind discriminator) reproduces predicted v2→v3 lift within
noise: Parse geomean 7.11× on M1 Max (n=9 native types), 8.02× on M4 Max
— matches Red model (v2 8.39× → v3 7.09× projected). Per-type allocs 3.57×
fewer, bytes 6.89× smaller, host-stable.
AdvanceTime end-to-end via txs.Codec wrapper: 34.14× M1, 42.76× M4 — ratio
grows with chip speed (reflection tax is fixed-per-op).
Honest residual: Build is a regression on M4 Max for 6/9 types (geomean
0.86×). zap.Builder per-call overhead exposed on fast cores. M1 Max still
positive (1.12×). Tracked for luxfi/zap v0.7.0 (Blue v3.1 iteration);
Parse-side ship decision is independent.
Both hosts darwin/arm64 (M1 Max MacBookPro18,2 + M4 Max Mac16,5 / dbc
runner). Task spec called dbc "amd64 Linux" — corrected: it is darwin/arm64
Apple M4 Max. linux/amd64 numbers TBD until x86 box lands in ARC fleet.
Reproduce: GOWORK=off go test -bench='^Benchmark(Parse|Build)' -benchmem
-benchtime=500ms -count=3 -run='^$'
./vms/platformvm/txs/{bench,zap_native}/
TxKindBaseFull = 11 → BaseTxFull
TxKindAddPermissionlessValidator = 12 → AddPermissionlessValidatorTx
TxKindImport = 13 → ImportTx
TxKindExport = 14 → ExportTx
TxKindCreateChain = 15 → CreateChainTx
Sizes (fixed section, schema v3):
BaseTxFull = 85
ImportTx = 125
ExportTx = 125
AddPermissionlessValidatorTx = 381
CreateChainTx = 221
Design highlights:
- BaseTxFull is the real P-chain spending envelope (Outs + Ins +
Credentials + 2 shared arrays + Memo). The batch-2 BaseTx
(TxKindBase) remains as the minimal metadata envelope for places
that need only {NetworkID, BlockchainID, Memo} without spending
state. Both kinds are first-class; they are NOT alternatives.
- Import / Export use a SINGLE combined Ins (or Outs) list with a
header marker (ImportedInsStart/Count or ExportedOutsStart/Count)
identifying the cross-chain slice. This collapses what would have
been two parallel sig-index arrays into one shared array — fewer
pointer pairs, no rebase bookkeeping. The slice-based indexing is
byte-identical for the imported/exported half because they
reference the same shared array.
- AddPermissionlessValidatorTx carries two single-address Owner stubs
(validation rewards, delegation rewards) inline in the fixed
section. The same OwnerStub type underlies CreateChainTx.Owner.
Multi-address Owner ships in batch 4 along with the AddressList
primitive; current callers needing multi-addr flow through the
legacy codec gate.
- CreateChainTx embeds a 32-byte WarpMessageHash (sha256 of the
originating Warp commit; zero when the chain is being created
directly without a cross-network commit). The Warp message BODY
is NOT embedded — it lives in the signed Warp envelope outside
this tx. Hash-only embedding keeps the unsigned-tx bytes stable
and the Warp envelope separately verifiable.
All five tx types follow the v3 invariant: TxKind@0, Wrap* rejects
mismatched discriminator with ErrWrongTxKind. Cross-confusion test
expansion verifies the new tx types in pairwise rejection (7
additional scenarios cover the batch 1+2+3 surface).
Deferred to batch 4 (no new primitives needed): the remaining ~18
classical platformvm tx types (CreateNetwork, AddDelegator,
AddPermissionlessDelegator, etc.) reuse the existing batch-3
primitives; their landings are mechanical.
go test -race ./vms/platformvm/txs/zap_native/...
ok github.com/luxfi/node/vms/platformvm/txs/zap_native 1.488s
Five primitives. Each is fixed-stride at the entry level; variable
per-entry bytes/sub-list payloads live in shared sibling fields on
the parent tx, indexed by (start, count). This keeps List.At(i)
zero-allocation while supporting unbounded per-entry payload sizes.
OutputList stride 96 → TransferableOutput
InputList stride 88 → TransferableInput + shared SigIndicesArray
CredentialList stride 16 → Credential + shared SignatureArray
WarpMessage size 40 → embedded {SourceNetwork, Payload}
EvidenceList stride 48 → EvidenceEntry + shared MessageBlobs/SignatureBlobs
Design choices:
- Stride is the entry-count semantics for SetList; the byte count from
ListBuilder.Finish() is discarded. List.Object(i, stride) does the
branch-free arithmetic.
- Variable per-entry data goes into a SIBLING field on the parent tx
(not into the entry's stride): InputList → SigIndicesArray;
CredentialList → SignatureArray; EvidenceList → MessageBlobs +
SignatureBlobs. Each entry references its slice via (start, count).
This pattern lets entries stay fixed-stride and accessors stay
zero-allocation. Multi-input-shared signature arrays also enable
signature deduplication when adjacent inputs share signers.
- Forward-only relOffsets per the F1 contract: SetBytes-backed payload
fields (WarpMessage.Payload, EvidenceList parent's MessageBlobs/
SignatureBlobs) flow through the F1-fixed zap.Object.Bytes which
rejects negative bit-patterns.
- Read-only contracts on every []byte / by-value accessor are
documented with the canonical defensive-copy idiom
(append([]byte(nil), m...)) per AT1.
- Multi-address Owner is NOT yet supported at the wire layer; v3
OutputList carries the single-address stub identical to
TransferChainOwnershipTx. Multi-address callers still flow through
the legacy codec gate behind LUXD_ENABLE_LEGACY_CODEC. The
AddressList primitive ships in batch 4 (defer).
- Multi-signature credentials handled via the SignatureArray slice
semantics — one Credential, N sigs. Post-quantum credentials
(ML-DSA) are NOT YET on the v3 path; they keep flowing through
legacy until their dedicated PQ Credential schema lands.
Round-trip tests (batch3_test.go) cover all five primitives + the
empty-list/null-pointer fallback path. go test passes; race-free.
Updated bench numbers for v3 batch-2 (the +1-byte TxKind tax):
Parse geomean speedup vs legacy: 7.09× (was 8.39× pre-v3)
Build geomean speedup vs legacy: 1.35× (build path was always
closer to parity; structure-allocator dominates at small tx sizes)
Phase A response to Red's batch-2 review:
F2 (MEDIUM, cross-type confusion) — schema-bump v2→v3. Every fixed
section now carries a TxKind uint8 at offset 0; every other field
shifts by +1 byte. Wrap*Tx reads TxKind first and returns
ErrWrongTxKind on mismatch. Constructors write the kind
unconditionally. Closes the gap where an AdvanceTimeTx buffer wrapped
as a BaseTx returned garbage-but-deterministic field reads.
TxKind enum (dense, 0 reserved):
1=AdvanceTime 2=RewardValidator 3=SetL1ValidatorWeight
4=IncreaseL1ValidatorBalance 5=DisableL1Validator 6=Base
7=RegisterL1Validator 8=SlashValidator
9=TransferChainOwnership 10=RemoveChainValidator
v3 sizes (each +1 vs v2 from the TxKind byte):
AdvanceTimeTx = 9
RewardValidatorTx = 33
SetL1ValidatorWeightTx = 49
IncreaseL1ValidatorBalanceTx = 41
DisableL1ValidatorTx = 33
BaseTx = 45
RegisterL1ValidatorTx = 217
SlashValidatorTx = 57
TransferChainOwnershipTx = 69 (no natural-alignment padding; reads are alignment-tolerant)
RemoveChainValidatorTx = 53
F1 (MEDIUM, memo malleability) — closed at the luxfi/zap wire layer
in v0.6.1 (negative relOffset rejected in Object.Bytes). Bumped node's
go.mod replace: zap v0.2.0 → v0.6.1. TestRed_V2 now confirms the
defense via the nil-Memo branch.
F4 (INFO, doc bugs) — RegisterL1ValidatorTx comment now correctly
declares size 217; TransferChainOwnershipTx now correctly declares
size 69. Both sizes flow from offset arithmetic — no magic numbers.
AT1 (accepted tradeoff, memo aliasing) — BaseTx.Memo() docstring
escalated: READ-ONLY contract + the canonical defensive-copy idiom
(append([]byte(nil), m...)). Same pattern documented for every
variable-length accessor going forward.
Brand cleanup: SlashValidatorTx.Subnet() → Network() and
RemoveChainValidatorTx.Subnet() → Network(). Zero `Subnet*` symbols
in batch-2 code path. Tests updated.
V14 security test now exhaustively covers cross-confusion: 10
pairings of {valid-tx-buf, wrong-Wrap} + reserved TxKind=0 — all
reject with ErrWrongTxKind. The other 19 Red vectors continue to
pass under v3.
go test -race ./vms/platformvm/txs/zap_native/...
ok github.com/luxfi/node/vms/platformvm/txs/zap_native 1.446s
Coordinated with luxfi/zap@v0.6.1 (F1 fix).
Post-coreth networks (test+dev primary that bake only P + EVM genesis chains)
run in P-only mode where the X alias is not registered. The wallet's
FetchState now degrades the X-Chain context to a sentinel ids.Empty
BlockchainID instead of erroring, and skips the X-chain entry from the
chain-pair UTXO scan when X is unavailable.
Required to drive IssueCreateChainTx via the canonical primary wallet
against the fresh test+dev primaries (closes universe #168).
Real-workload benchmark suite produced by the scientist agent. Production-
realistic measurements on Apple M1 Max (Go 1.24).
Harness (8 files + results + reproduce docs):
bench/fixtures.go — realistic per-type tx fixtures
bench/parse_bench_test.go — per-type Parse: Legacy vs ZAP
bench/build_bench_test.go — per-type Build
bench/field_bench_test.go — field access (single + 1M batch)
bench/workload_bench_test.go — 1000-tx mempool + 200-block parse + dispatcher
bench/alloc_bench_test.go — 5s sustained-parse GC pressure
bench/disable_legacy_test.go — LUXD_ENABLE_LEGACY_CODEC gate verification
bench/README.md — reproduce + capture procedures
bench/testdata/README.md — captures notes
bench_results/RESULTS.md — full numbers + honest caveats + CPU profile
Headline numbers (vs linearcodec):
Parse AdvanceTimeTx : 37x faster (1940 ns -> 52.5 ns), 20x less mem
Build AdvanceTimeTx : 5.2x faster
Sustained 5s parse loop : 18.5x throughput (777k -> 14.4M parses/sec)
Cross-type mean : 5.6x parse, 1.6x build
Allocations (parse, build) : always 3->1 and 4->2
CPU profile:
Legacy: ~50% in reflectcodec.{marshal,unmarshal} reflection walk,
~30% in runtime.{madvise,kevent} from per-field alloc GC pressure
ZAP: elimination of both surfaces; offset arithmetic + 1 alloc per parse
Honest caveats (verbatim from scientist report):
- Only 5/33 tx types have native paths today; mempool mix workload
compresses to ~1.1x lift until BaseTx + AddPermissionless* native ship
- Field access: single uint64 read is 2.1x slower (offset deref vs struct
field), break-even at ~3,560 reads per parse; mainnet validators do
~10x per tx so ZAP still wins by orders of magnitude in the real regime
- darwin/arm64 only — re-run on linux/amd64 before quoting production-
binding numbers
- LUXD_ENABLE_LEGACY_CODEC gate verified at zap_native surface (subprocess
test passes); NOT yet wired into platformvm/txs.Parse — would break
byte-preserving v0 read for validators bootstrapping pre-activation
history. Gate enforcement lives at the future wire dispatcher.
Recommendation (CTO direction): continue the migration. Architecture works
as designed. Next priority: BaseTx + AddPermissionless* native paths — the
two highest-weight tx types in the modal mainnet mempool mix.
txs/tx.go is unmodified — byte-preserving v0->TxID migration invariant
preserved per existing codec.go CodecVersionV0/V1 framework.
Reproduce: cd ~/work/lux/node && GOWORK=off go test -bench=. -benchmem ./vms/platformvm/txs/bench/...
LUXD_DISABLE_LEGACY_CODEC → LUXD_ENABLE_LEGACY_CODEC (per user 2026-06-02
'should be LUXD_ENABLE_LEGACY_CODE=1 to turn it on').
Default: native ZAP for every read + write. codec.Codec.Marshal/Unmarshal
gone from hot path. Fresh deployments + post-activation production get a
smaller, faster binary with no legacy code reachable.
Operators with pre-activation history that needs reading set
LUXD_ENABLE_LEGACY_CODEC=1 to opt in to backward-compat. Without it, legacy
bytes return ErrLegacyCodecDisabled.
Tests updated for the inverted default; pass clean.
Pulls in plugin/evm StateScheme fix that unblocks fresh L2 EVM chain
creation on lux-mainnet (hanzo, zoo, spc, pars), where the plugin was
panicking with "panic in eth.New: triedb parent [<EmptyRootHash>] layer
missing" because eth/backend.go inherited geth's path-by-default for
empty DBs while the VM hard-refuses path mode upfront.
See luxfi/evm v0.18.18 commit 1dea806f8.