Lux Proposals
← All proposals
LP-0186Draft

Status

All chain-VM internal wire surfaces in ~/work/lux/chains/ are ZAP. The

Go struct IS the wire — there is no codec, no marshal step, no encode

or decode. A block, tx, vertex, or any other on-chain envelope is a

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

This LP is the umbrella for chain-side VMs. The primary-network wires

are covered by sibling LPs:

The struct IS the wire


type BlockWire struct {
    msg *zap.Message       // the wire buffer itself
    obj zap.Object         // typed window into msg
}

func (b BlockWire) BlockHeight() uint64 { return b.obj.Uint64(OffsetBlock_BlockHeight) }
func (b BlockWire) Bytes() []byte       { return b.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.

This is the Cap'n-Proto / FlatBuffers invariant: **wire format is the

in-memory representation**.

Specification

Per-VM wire packages

Each VM owns its wire/ subdir under ~/work/lux/chains/<vm>/wire/.

Each file in that directory defines one wire-envelope schema with:

Schema table

| Schema ID | VM | Kind name | File |
|---|---|---|---|
| 0xC0 | aivm | KindVertex / KindQueryTx / KindInferenceTx / KindTaskResult | aivm/wire/{vertex,query_tx,inference_tx}.go |
| 0xC1 | bridgevm | KindBlock / KindBridgeRequest / KindGenesis | bridgevm/wire/{block,bridge_request,genesis}.go |
| 0xC2 | dexvm | KindBlock / KindOrderTx / KindCancelTx / KindSwapTx / KindLiquidityTx / KindMatchTx | dexvm/wire/{block,order_tx,swap_tx}.go |
| 0xC3 | evm (chains-side) | KindPluginEnvelope / KindChainGenesis | evm/wire/{plugin_envelope,chain_genesis}.go |
| 0xC4 | graphvm | KindBlock / KindSchemaUpdateTx / KindIndexUpdateTx / KindGraphMutationTx / KindQueryResultTx | graphvm/wire/{block,schema_update_tx,graph_mutation_tx}.go |
| 0xC5 | identityvm | KindBlock / KindIdentityTx / KindCredentialTx / KindRevocationTx | identityvm/wire/{block,identity_tx,credential_tx,revocation_tx}.go |
| 0xC6 | keyvm | KindBlock / KindKeyTx / KindKeyRotationTx | keyvm/wire/{block,key_tx,key_rotation_tx}.go |
| 0xC7 | oraclevm | KindBlock / KindOracleUpdateTx / KindOracleQueryTx / KindAggregationTx | oraclevm/wire/{oracle_update_tx,aggregation_tx}.go |
| 0xC8 | quantumvm | KindBlock / KindQuantumTx / KindPrecompileTx / KindQuasarWitness | quantumvm/wire/{block,quantum_tx,precompile_tx}.go |
| 0xC9 | relayvm | KindOpenChannelTx / KindCloseChannelTx / KindSendPacketTx / KindReceivePacketTx / KindRelayReceiptTx | relayvm/wire/{channel_tx,packet_tx,receipt_tx}.go |
| 0xCA | thresholdvm | KindBlock / KindOperation / KindManagedKey / KindCrossChainMPCRequest | thresholdvm/wire/{block,operation,managed_key,cross_chain_request}.go |
| 0xCB | zkvm | KindBlock / KindTransaction / KindUTXO / KindPrivateAddress / KindVertex | zkvm/wire/{block,transaction,utxo,private_address,vertex}.go |

Schema-ID values are the VM namespace bucket. The 1-byte Kind

discriminator inside each VM is local — i.e. KindBlock=1 inside

bridgevm/wire is distinct from KindBlock=1 inside graphvm/wire,

because they live in different packages with different schema tables.

External cross-VM tooling that needs to disambiguate uses the schema

ID as the namespace prefix.

Kind discriminator

Every chain-side wire envelope's fixed section starts with a 1-byte

Kind at offset 0. Wrap<Schema> verifies the discriminator before

returning a typed accessor — rejects cross-kind confusion (e.g. a

Block buffer Wrap'd as a Transaction returning garbage-but-

deterministic field reads).

Field ordering

Field bytes inside the fixed section appear at fixed offsets named

Offset<Schema>_<Field>. The variable-length tail (packed lists,

sub-buffers, opaque payloads) lives after the fixed section. The

constructor takes a struct-of-go-values and writes them to the buffer

at compile-time-known offsets; the accessor reads from the same

offsets. No serialization step exists between buffer and value.

Variable-length fields

Lists ride as packed []byte slots inside the fixed section:

These helpers are local to each wire/ package (no cross-VM coupling).

Sub-buffer composition

When one wire envelope nests another (e.g. a Block carries an

ordered list of Transaction buffers), the inner buffers are stored

in their ZAP-encoded form inside a packBytesList slot. The outer

accessor returns [][]byte; the caller round-trips each entry

through the inner schema's Wrap<Inner> to get a typed accessor.

Out-of-scope wires

| Surface | Format | Where it lives |
|---|---|---|
| External JSON-RPC service handlers | JSON | <vm>/service.go, <vm>/api/*.go |
| GraphQL/Graphvm query surface | JSON-over-GraphQL | graphvm/graphql.go |
| Boot-time config blobs (ZConfig, Genesis, ThresholdConfig) | JSON | <vm>/vm.go::Initialize |
| Upstream-EVM block/tx/receipt | RLP | luxfi/geth/core/types/* |
| relayvm upstream luxfi/relay/vm re-export | upstream-owned | luxfi/relay/vm/* |
| oraclevm upstream luxfi/oracle/vm re-export | upstream-owned | luxfi/oracle/vm/* |

The chain-VM modules' RPC handlers, GraphQL schemas, and boot-time

config blobs are operator-facing and never travel on the consensus

wire. JSON is the canonical encoding for those surfaces. ZAP is for

the on-chain consensus wire only.

No backwards compatibility

None.

The new final Lux network has one chain-VM wire per VM, present in

the binary from the release that ships this LP. A node presenting

non-ZAP bytes for an in-scope chain-VM surface is not a member of the

network by definition.

The pre-Quasar Edition Lux network (2020–2025) used linearcodec for

several of these surfaces (bridgevm/codec.go, thresholdvm/codec.go,

zkvm/codec.go). That network is separate and not migrated. The

codec files are deleted; an audit confirms zero luxfi/codec /

linearcodec source-file references in ~/work/lux/chains/:


grep -rn "luxfi/codec\b\|linearcodec\|codec\.Manager" \
  ~/work/lux/chains --include="*.go" \
  | grep -v _test.go
# expected: empty

No flags

None. The binary speaks ZAP for in-scope chain-VM surfaces and JSON

for the out-of-scope RPC/config surfaces. Byte input that fails ZAP

parse on an in-scope surface is rejected. No LUXD_ENABLE_LEGACY_CODEC

knob, no dispatch by magic byte, no runtime selection. No per-VM

override flag.

No replay rules

None. Activation predates every block on the new final Lux network.

There are no pre-activation chain-VM blocks on this network to replay

under a different wire.

Audit gates

CI gates each release with:


# 1. zero luxfi/codec source-file hits in chains/
grep -rn "luxfi/codec\b\|linearcodec\|codec\.Manager\|Codec\.Marshal\|Codec\.Unmarshal" \
  ~/work/lux/chains --include="*.go" \
  | grep -v _test.go \
  || true
# expected: empty

# 2. every <vm>/wire/ subdir builds and tests pass
cd ~/work/lux/chains && go build \
  ./aivm/wire/... ./bridgevm/wire/... ./dexvm/wire/... \
  ./evm/wire/... ./graphvm/wire/... ./identityvm/wire/... \
  ./keyvm/wire/... ./oraclevm/wire/... ./quantumvm/wire/... \
  ./relayvm/wire/... ./thresholdvm/wire/... ./zkvm/wire/...

cd ~/work/lux/chains && go test \
  ./aivm/wire/... ./bridgevm/wire/... ./dexvm/wire/... \
  ./evm/wire/... ./graphvm/wire/... ./identityvm/wire/... \
  ./keyvm/wire/... ./oraclevm/wire/... ./quantumvm/wire/... \
  ./relayvm/wire/... ./thresholdvm/wire/... ./zkvm/wire/... \
  -count=1 -timeout=10m

Test cases


TestBuildParseRoundTrip            New<Schema>(in).Bytes() → Wrap<Schema> → field equality
TestParseRejectsNonZAP             non-ZAP bytes reject at Wrap<Schema> (nil, empty, short, bad magic)
TestKindMismatch                   Wrap<Schema-A> on a Schema-B buffer returns ErrWrongKind
TestPeekKind                       PeekKind on a fresh buffer returns the constructor's Kind
TestBytesIsUnderlyingBuffer        Bytes() returns the underlying ZAP buffer, no copy

Lives in ~/work/lux/chains/<vm>/wire/wire_test.go.

Cross-implementation byte equality

Chain-VM internal-wire interop with non-Go consensus stubs follows

the same rule as LP-023 and LP-184: every reference implementation

reads the schema definitions in ~/work/lux/chains/<vm>/wire/*.go

as the single source of truth, and CI gates byte-equality before any

release.

Reference implementation

~/work/lux/chains/<vm>/wire/ — lands in the luxd release carrying

this LP.

Per-VM follow-ups that flesh out sub-type schemas (e.g. ZKProof inner

fields, FHEData inner fields, ShieldedOutput inner fields in zkvm)

are tracked under this LP as a single follow-up; the umbrella does

not block on those because the outer envelopes already carry the

inner data as opaque sub-buffers.

Activation marker


activates:      2025-12-25T16:20:00-08:00
activates-unix: 1766708400

Set once. Does not move. Every future Lux network-upgrade LP that

changes a chain-VM wire on the new final Lux network MUST use this

same activation marker.

Copyright

CC0.