Lux Proposals
← All proposals
LP-0185Draft

Status

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

In scope / Out of scope

In scope

| File (pre-LP) | Surface | Disposition |
|---|---|---|
| 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.

Out of scope — preserved byte-for-byte

| Surface | Format | Where it lives |
|---|---|---|
| Ethereum block header | RLP | luxfi/geth/core/types.Header |
| Ethereum block body | RLP | luxfi/geth/core/types.Body |
| Signed tx Ethereum format (legacy, EIP-1559, EIP-4844, …) | RLP / typed-tx prefixes | luxfi/geth/core/types.Transaction.MarshalBinary |
| Receipts | RLP | luxfi/geth/core/types.Receipt |
| Logs | RLP | luxfi/geth/core/types.Log |
| State trie nodes | RLP | luxfi/geth/trie |
| Predicate access-list packing (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.

The struct IS the wire

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.

Specification

Wire schemas

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

Schema table

| ID | Name | File | Root size | Description |
|---|---|---|---|---|
| 1 | BlockResults | 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.

BlockResults wire layout


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.

Buffer-size cap

MaxResultsSize = 1 MiB. Parse rejects buffers above this with

ErrResultsExceedsMaxSize. Mirrors the legacy cap.

LP-176 gas-state header

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.

No backwards compatibility

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.

No replay rules

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.

No flags

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.

External Ethereum RLP preservation

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:

These files are not edited by this LP. The grep audit (below) confirms.

Audit gates

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)

Cross-implementation byte equality

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.

Implementation

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.

Test cases


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.

Reference implementation

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

Cross-references

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 the EVM internal wire on the new final Lux network MUST use

this same activation marker.

Copyright

CC0.