Lux Proposals
← All proposals
LP-0184Draft

Status

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.

The struct IS the wire


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.

Specification

Wire schemas

~/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

TxKind discriminator

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.

| Kind value | Tx type |
|------------|-------------------|
| 0 | Reserved |
| 1 | BaseTx |
| 2 | CreateAssetTx |
| 3 | OperationTx |
| 4 | ImportTx |
| 5 | ExportTx |

Primitives

Mirror of P-chain wire (LP-023). Same offsets, same strides, same

view types:

| Primitive | Stride | Notes |
|----------------------|--------|---------------------------------------------|
| TransferableOutput | 96 | AssetID + Amount + Threshold + Locktime + OwnerAddress (single-address v3 stub) |
| TransferableInput | 88 | TxID + OutputIndex + AssetID + Amount + SigIndicesStart/Count |
| Credential | 16 | SigsStart/Count into shared SignatureArray |
| SigIndex | 4 | one uint32 entry in shared SigIndicesArray |
| Signature blob | 65 | secp256k1 sig + recovery id (fixed stride) |
| UTXOID | 36 | TxID + OutputIndex (OperationTx primitive) |
| InitialState | 16 | FxIndex + OutputsStart/Count (CreateAssetTx) |
| Operation | 88 | AssetID + slices into shared arrays |

Per-type schema layout

BaseTx (TxKind = 1, 85-byte fixed section)

| Offset | Size | Field |
|--------|------|-------------------|
| 0 | 1 | TxKind |
| 1 | 4 | NetworkID |
| 5 | 32 | BlockchainID |
| 37 | 8 | OutsList |
| 45 | 8 | InsList |
| 53 | 8 | CredsList |
| 61 | 8 | SigIndicesArr |
| 69 | 8 | SigArr |
| 77 | 8 | Memo |

CreateAssetTx (TxKind = 2, 122-byte fixed section)

| Offset | Size | Field |
|---------|------|-------------------|
| 0..84 | | BaseTx fields |
| 85 | 1 | Denomination |
| 86 | 8 | Name |
| 94 | 8 | Symbol |
| 102 | 8 | InitialStatesList |
| 110 | 8 | StateOutputsList |

OperationTx (TxKind = 3, 109-byte fixed section)

| Offset | Size | Field |
|---------|------|-------------------|
| 0..84 | | BaseTx fields |
| 85 | 8 | OpsList |
| 93 | 8 | OpUTXOIDsArr |
| 101 | 8 | OpOutsArr |

ImportTx (TxKind = 4, 125-byte fixed section)

| Offset | Size | Field |
|---------|------|-------------------|
| 0..84 | | BaseTx fields |
| 85 | 32 | SourceChain |
| 117 | 8 | ImportedInsList |

ExportTx (TxKind = 5, 125-byte fixed section)

| Offset | Size | Field |
|---------|------|-------------------|
| 0..84 | | BaseTx fields |
| 85 | 32 | DestinationChain |
| 117 | 8 | ExportedOutsList |

Tx ID


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.

Block ID

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).

Out of scope

Wires owned by other repositories:

Schema extensions deferred to follow-up LPs:

External RPC surface:

Rationale

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.

No backwards compatibility

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.

No replay rules

None. Activation predates every block on the network. There are no

pre-activation blocks to replay under a different wire.

No flags

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.

Cross-implementation byte equality

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.

Implementation

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.

Test cases


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.

Reference implementation

~/work/lux/node/vms/xvm/wire_*.go — lands in the luxd release

carrying this LP.

Cross-references

Activation marker

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.

Appendix A: Benchmark results (informative)

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):

| Tx Type | ns/op | allocs/op | bytes/op |
|----------------|--------|-----------|----------|
| BaseTx | 25.17 | 1 | 24 |
| CreateAssetTx | 29.71 | 1 | 24 |
| ImportTx | 24.99 | 1 | 24 |
| ExportTx | 26.13 | 1 | 24 |
| OperationTx | 95.14 | 1 | 24 |

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

Copyright

CC0.