The Lux primary-network EVM has two distinct wire surfaces:
1. External Ethereum RLP — block header, block body, signed-tx
Ethereum format, receipts, logs, state-trie nodes. Consumed by
external Ethereum tooling (geth, cast, eth_getBlock, JSON-RPC
subscribers). PRESERVED, byte-for-byte unchanged. RLP stays RLP.
2. Lux-internal EVM wire — predicate-result blobs, gas-state
metadata, internal consensus artifacts that travel alongside the
Ethereum payload but are NOT part of Ethereum interop. These move
to ZAP under this LP.
This LP touches only surface #2. Surface #1 is owned by luxfi/geth
and luxfi/coreth and is out of scope.
Activated at the genesis of the new final Lux network:
2025-12-25 16:20 Pacific (unix 1766708400).
vms/evm/predicate/results.go | Lux-internal block-results blob (per-block, per-tx, per-precompile bitset map carried alongside the block) | Migrated to vms/evm/wire/results.go (ZAP-native). Legacy file deleted. |vms/evm/predicate/results_test.go | Tests for the legacy codec wire | Deleted; replaced by vms/evm/wire/results_test.go exercising the ZAP wire. |vms/evm/lp176/lp176.go | Lux gas-state header (24-byte big-endian fixed layout) | Wire layout unchanged. luxfi/codec/wrappers.LongLen import removed; constant inlined locally. |vms/evm/lp176/lp176/lp176.go | Stale duplicate of the above | Deleted. |After this LP, grep -r "luxfi/codec\|linearcodec" ~/work/lux/node/vms/evm returns zero source-file hits.
luxfi/geth/core/types.Header |luxfi/geth/core/types.Body |luxfi/geth/core/types.Transaction.MarshalBinary |luxfi/geth/core/types.Receipt |luxfi/geth/core/types.Log |luxfi/geth/trie |predicate.Predicate 32-byte chunks) | Hand-rolled stride | vms/evm/predicate/predicate.go (already not a codec) |External Ethereum tooling (geth, cast, web3.js, etc.) reads RLP
at the chain boundary. The contract of RLP byte stability with
external tooling is public and is preserved.
For every in-scope surface, the Go struct IS the wire — there is no
codec, no marshal step, no encode/decode pass.
type BlockResults struct {
msg *zap.Message // the wire buffer itself
root zap.Object
txList zap.List // ListStride over TxResultEntry items
precompileList zap.List // ListStride over PrecompileEntry items
bitsetBlobs []byte // bytes view of BitsetBlobs
}
func (b BlockResults) Bytes() []byte { return b.msg.Bytes() } // return buffer
func (b BlockResults) Get(h common.Hash, a common.Address) set.Bits { /* offset reads */ }
Parse(b) wraps b in a typed accessor — no copy, no decode. Build(m)
writes fields into a buffer at known offsets — no encode. The same
ZAP / Cap'n-Proto / FlatBuffers invariant LP-023 articulates for the
P-chain.
~/work/lux/node/vms/evm/wire/ is the package; one file per type.
Each accessor struct embeds a *zap.Message; the buffer IS the value.
wire/results.go | 32 B | Per-block, per-tx, per-precompile bitset payload (replaces legacy codec-marshalled predicate.BlockResults). |The 1-byte kind discriminator lives at root offset 0. Currently the
EVM wire namespace carries exactly one schema; future additions get
dense, never-reused IDs in this table.
Root object (32 B fixed):
Kind uint8 @ 0 (= 1, WireKindBlockResults)
_ 7B @ 1 (alignment padding, zero)
TxResults list @ 8 (offset+length to TxResultEntry list)
BitsetBlobs bytes @ 16 (offset+length pair, bitset blob array)
PrecompileList list @ 24 (offset+length to flat PrecompileEntry list)
TxResultEntry (48 B stride):
TxHash 32B @ 0
PrecompilesRel uint32 @ 32 (offset into PrecompileList)
PrecompilesCount uint32 @ 36
_ 8B @ 40 (reserved-zero)
PrecompileEntry (32 B stride):
Address 20B @ 0
BitsetRel uint32 @ 20 (offset into BitsetBlobs)
BitsetLen uint32 @ 24
_ 4B @ 28 (reserved-zero)
Canonical wire order:
This makes the wire byte-stable across builders and reproducible across
runs. Map iteration order in Go is non-deterministic; the builder sorts.
MaxResultsSize = 1 MiB. Parse rejects buffers above this with
ErrResultsExceedsMaxSize. Mirrors the legacy cap.
vms/evm/lp176/lp176.go::State.Bytes() continues to emit a fixed
24-byte big-endian header (Capacity ‖ Excess ‖ TargetExcess). The
layout is unchanged across the activation. The legacy import of
luxfi/codec/wrappers.LongLen (a constant = 8) is replaced by a
local longLen constant. No on-wire byte changes.
None.
The new final Lux network has one EVM internal wire, present in the
binary from the release that ships this LP. A node presenting non-ZAP
bytes for these in-scope surfaces is not a member of the network by
definition.
The pre-Quasar Edition Lux network (2020–2025) used linearcodec for
the predicate-results wire. That network is separate and not migrated.
None. Activation predates every block on the new final Lux network.
There are no pre-activation EVM blocks on this network to replay under
a different wire.
None. The binary speaks ZAP for in-scope surfaces and RLP for
out-of-scope 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.
This LP makes a hard, machine-checkable guarantee: a geth-format
Ethereum block (header, body, signed-tx Ethereum format, receipts,
logs, state-trie nodes) is byte-identical across this LP's activation.
No fields renamed, no encoding tweaks, no migration.
The verification:
~/work/lux/geth/core/types/*.go::*Header.EncodeRLP produces thesame bytes pre- and post-activation.
~/work/lux/geth/core/types/transaction.go::Transaction.MarshalBinarysame.
~/work/lux/coreth/plugin/evm/wrapped_block.go reading via RLP same.These files are not edited by this LP. The grep audit (below) confirms.
CI gates each release with:
# 1. zero luxfi/codec source-file hits in vms/evm
grep -rn "luxfi/codec\b\|linearcodec" ~/work/lux/node/vms/evm --include="*.go" \
| grep -v "_test.go" \
| grep -v "comment only" \
|| true
# expected: empty
# 2. external Ethereum RLP encoders are untouched
git diff <activation> -- ~/work/lux/geth/core/types
# expected: empty (this LP does not edit geth)
EVM internal-wire interop with non-Go consensus stubs follows the same
rule as LP-023: every reference implementation reads the schema
definitions in ~/work/lux/node/vms/evm/wire/results.go as the single
source of truth, and CI gates byte-equality before any release. Only
the in-scope surfaces — external Ethereum RLP byte equality is governed
by the upstream Ethereum spec.
Reference implementation lives in ~/work/lux/node/vms/evm/wire/.
The legacy vms/evm/predicate/results.go codec callsites are deleted
alongside the package's only luxfi/codec dependency. The migrated
vms/evm/lp176/lp176.go retains its fixed-stride big-endian header
(no codec used in the hot path) and just sheds the named-constant
import.
TestBuildParseRoundTrip Build → Bytes() → Parse → field equality (5 input shapes)
TestBuildIsCanonical Build(m) is byte-stable across 16 iterations of identical m
TestParseRejectsNonZAP non-ZAP bytes reject at Parse (nil, empty, short, bad magic)
TestParseRejectsOversize buffers > MaxResultsSize reject with ErrResultsExceedsMaxSize
TestGetAbsent Get on missing keys returns set.NewBits()
TestBytesIsUnderlyingBuffer Bytes() returns the underlying ZAP buffer, no copy
TestParseKindMismatch forged kind byte rejects with ErrWireKindMismatch
TestState_Bytes_PreserveLayout lp176.State.Bytes() byte-stable across activation
Lives in ~/work/lux/node/vms/evm/wire/results_test.go and
vms/evm/lp176/lp176_test.go.
~/work/lux/node/vms/evm/wire/ — lands in the luxd release carrying
this LP.
~/work/lux/node/vms/evm/lp176/lp176.go — already lands, this LP only
removes the codec-constant import.
~/work/lux/node/vms/evm/WIRE_CANONICALIZATION.md — the audit ledger
classifying each in-scope file.
activates: 2025-12-25T16:20:00-08:00
activates-unix: 1766708400
Set once. Does not move. Every future Lux network-upgrade LP that
changes the EVM internal wire on the new final Lux network MUST use
this same activation marker.
CC0.