The Lux X-chain wire is ZAP. The Go struct IS the wire — there is no
codec, no marshal step, no encode or decode. A BaseTx is a buffer;
accessors are offset reads on that buffer; Bytes() returns the
buffer.
Activated at the genesis of the new final Lux network:
2025-12-25 16:20 Pacific (unix 1766708400).
There is no other wire on this network. No legacy. No backwards
compatibility. The pre-Quasar Edition Lux network (2020–2025) is a
separate network and is out of scope.
type BaseTxWire struct {
msg *zap.Message // the wire buffer itself
obj zap.Object // typed window into msg
}
func (t BaseTxWire) NetworkID() uint32 { return t.obj.Uint32(OffsetNetworkID) }
func (t BaseTxWire) Bytes() []byte { return t.msg.Bytes() }
There is no Marshal. There is no Unmarshal. There is no Encode.
There is no Decode. There is no Serialize. Parse(b) wraps b in
a typed accessor — it does not copy and does not decode. Build(...)
writes fields into a buffer at known offsets and the buffer is the
wire bytes — there is no encoding pass over it.
tx.Bytes() returns the underlying buffer literally. WrapXxxTx(b)
wraps b in a typed accessor. TxID = sha256(buffer). No re-encoding
step anywhere in the hot path.
~/work/lux/node/vms/xvm/ is the package; the X-chain native ZAP wire
types live directly at the root of xvm (not in a zap_native/
subdir). Each Go struct embeds a *zap.Message; the buffer IS the
value.
Schema files:
wire_kind.go TxKind discriminator + parseAndCheckKind
wire_base_tx.go BaseTxWire
wire_create_asset_tx.go CreateAssetTxWire + InitialState primitive
wire_operation_tx.go OperationTxWire + Operation + UTXOID primitive
wire_import_tx.go ImportTxWire
wire_export_tx.go ExportTxWire
wire_input_list.go TransferableInput + InputList + SigIndicesArray
wire_output_list.go TransferableOutput + OutputList
wire_credential_list.go Credential + CredentialList + SignatureArray
wire_encoding.go little-endian stride writers (putU32/putU64)
wire_txid.go sha256-of-buffer convenience
Every X-chain tx fixed section starts with a 1-byte TxKind at offset
0. WrapXxxTx checks the discriminator before returning a typed
accessor and rejects with ErrWrongTxKind on mismatch. This closes
the cross-tx-type confusion surface where, e.g., an ImportTx buffer
could be Wrap'd as a BaseTx and return garbage-but-deterministic
field reads.
Mirror of P-chain wire (LP-023). Same offsets, same strides, same
view types:
TxID = sha256.Sum256(tx.Bytes())
tx.Bytes() returns the ZAP buffer. No re-canonicalization. No
re-encoding. The hash domain consumes whatever the wire layer
presents.
Same shape — block bytes are the ZAP buffer for the block envelope;
block ID is sha256.Sum256(block.Bytes()). Block envelopes inherit
LP-023's StandardBlock layout (parent + height + time + merkle root
+ tx list).
Wires owned by other repositories:
luxfi/crypto/secp256k1 — secp256k1 signature wire (embedded incredentials as opaque 65-byte blobs)
luxfi/zap — ZAP itself (LP-022)Schema extensions deferred to follow-up LPs:
OwnerAddress per output. Multi-address outputs travel through a
future schema version when the BAddressList primitive ships
(matches P-chain).
Schnorr, BLS12381). The v3 schema treats every signature blob as
fixed-65-byte (secp256k1). PQ credentials get a new schema version
when their wire shape is decided.
secp256k1fx.MintOutput. Property-fx and NFT-fx travel through
future schema extensions.
External RPC surface:
luxd JSON-RPC API (/ext/bc/X, /ext/info, etc.) — JSON at theprocess boundary, not an X-chain wire surface.
One wire. Decomplection: the wire layer does exactly one job
(present typed accessors over a byte buffer). Multiple wires would
braid two unrelated concerns (which wire to read) with one job
(read the bytes).
ZAP because the struct IS the wire. Every other option — protobuf,
RLP, linearcodec, msgpack, hand-rolled binary — is a codec: serialize
on write, deserialize on read. ZAP eliminates the step. The buffer is
the value. tx.Bytes() returns it. sha256(tx.Bytes()) requires no
re-encoding. That is the property no other option offers.
Same primitives as P-chain. The X-chain TransferableOutput,
TransferableInput, Credential, SigIndicesArray, SignatureArray are
byte-identical to the P-chain. This is intentional: one canonical
spending-envelope shape across both VMs, one cross-chain primitive
library, one set of schemas to audit.
Activation at the genesis of the new final Lux network. The new
generation came online 2026-05-31 (devnet) and 2026-06-01 (testnet,
mainnet). Activation timestamp predates every block on the new
generation. Every X-chain tx on the new final Lux network is a ZAP
tx by construction.
None.
The new final Lux network has one X-chain wire, present in the binary
from the release that ships this LP. A node presenting non-ZAP bytes
for an X-chain tx is not a member of the network by definition.
The pre-Quasar Edition Lux network (2020–2025) used linearcodec for
X-chain. That network is separate and not migrated.
None. Activation predates every block on the network. There are no
pre-activation blocks to replay under a different wire.
None. The binary speaks ZAP. Byte input that fails ZAP parse is
rejected. No LUXD_ENABLE_LEGACY_CODEC knob, no dispatch by magic
byte, no runtime selection.
X-chain interop with non-Go consensus stubs follows the same rule as
LP-023 and LP-182: every reference implementation reads the schema
definitions in ~/work/lux/node/vms/xvm/wire_*.go as the single
source of truth, and CI gates byte-equality before any release.
Reference implementation lives in ~/work/lux/node/vms/xvm/wire_*.go.
The legacy ~/work/lux/codec/linearcodec package is removed from the
X-chain hot path — codec.Codec{Marshal, Unmarshal} is not called
from the new final Lux generation's X-chain code.
TestBaseTx_RoundTrip build → Bytes() → Parse → fields equal
TestBaseTx_RoundTrip_Empty all-empty (Outs=Ins=Creds=Memo=nil)
TestCreateAssetTx_RoundTrip Name + Symbol + Denomination + InitialState
TestImportTx_RoundTrip SourceChain + shared SigIndicesArr
TestExportTx_RoundTrip DestinationChain + ExportedOuts
TestOperationTx_RoundTrip Operation with shared OpUTXOIDs + OpOuts
TestBytes_IsUnderlyingBuffer Bytes() returns msg buffer, no copy
TestWrap_RefusesNonZAP random bytes reject at Parse
TestWrap_RefusesCrossTypeConfusion BaseTx buffer ≠ CreateAssetTx
TestPeekTxKind dispatcher helper reads kind for every type
TestParse_RefusesV1Header legacy v1-header buffers rejected
Lives in ~/work/lux/node/vms/xvm/wire_test.go.
~/work/lux/node/vms/xvm/wire_*.go — lands in the luxd release
carrying this LP.
Every future Lux network-upgrade LP that changes the X-chain wire on
the new final Lux network MUST use the same activation marker as
this LP:
activates: 2025-12-25T16:20:00-08:00
activates-unix: 1766708400
Set once. Does not move.
Measured impact, Apple M1 Max, Go 1.24, native ZAP-native X-chain
tx types (Parse only; Build is one-time per-proposer and dwarfed
by the L1 finality budget):
Geometric mean Parse: ~32 ns/op, 1 alloc, 24 B. Parse is the
per-validator-per-block hot path.
Reproduce:
cd ~/work/lux/node/vms/xvm && GOWORK=off go test -bench=BenchmarkParse_ -benchmem -benchtime=1s
CC0.