Status: draft
Author: abhicris solo, parsdao co-author pending
Type: standards / cryptography
Requires: LP-020 (Quasar Consensus), LP-101 (P+Q mandatory profile gating), LP-021v2 (Warp 2.0 envelope), LP-107 (wire-package / codec pattern)
LP-183 specifies a native EVM precompile at address 0x000...00c that decodes a luxfi/zap binary envelope frame into its component fields (version, scheme, header digest, payload bytes, optional nested envelope handles) and returns a flat, ABI-decodable byte string consumable by Solidity contracts. The precompile costs 50 + 8 × len(input) gas, has a hard maximum frame size of 16 KiB, and surfaces nested x402 envelopes (per kcolbchain/switchboard PR #21) as a discriminated optional field on the output tuple. The motivation is that ZAP is the canonical wire format across Lux chains (LP-107, LP-021v2) for both validator gossip and cross-chain bridge anchors, and any application that wants to verify a ZAP-wrapped payment or attestation inside a transaction needs in-protocol decode — a pure-Solidity decoder benchmarks at ~5M gas, which is prohibitive for the per-block and per-payment use cases LP-183 targets. The precompile is not a replacement for luxfi/zap's Go reference encoder/decoder; encoding remains application-layer, and the precompile only handles the decode path that is on the hot path of consensus and settlement.
ZAP is the wire format under both validator gossip (Quasar consensus messages framed as ZAP envelopes) and cross-chain messaging (Warp 2.0 envelope per LP-021v2 ride inside ZAP frames). Create Protocol — and any Lux chain that wants to verify ZAP-framed payloads in-protocol — needs a way for an EVM contract to decode a ZAP frame inside a transaction.
The concrete use cases driving LP-183:
BridgeAnchor (system contract 0x0000...B001) | per cross-chain message | Light clients need to verify a ZAP-wrapped Lux cross-chain message without re-running gossip |AgentEscrow (0x0000...A002) | per agent payment release | Validators verify the x402 paywall response embedded inside the ZAP frame, without re-running the HTTP call |AgentRegistry (0x0000...A001) | per agent committee rotation | Committee rotation receipts arrive as ZAP frames signed by the MPC committee; the contract gates the rotation on a successful decode + signature check |MuzixStreamingOracle (0x0000...A005) | per DSP push | Pushers wrap their (masterId, plays, timestamp) payloads in ZAP envelopes so the same wire format is reused; the oracle contract decodes inline |The fully Solidity-implemented ZAP decoder we benchmarked in kcolbchain/switchboard against a representative 1 KiB frame burned ~4.9M gas at Cancun pricing (TLV walk + sub-frame recursion in pure EVM). At a 30M block gas limit this is one decode per block, which makes any per-payment, per-anchor, or per-message decode infeasible. A precompile that does the same work in compiled Go (or Rust on the EVM execution side via the standard precompile bridge) measures at <50µs end-to-end and prices well under 10K gas for the same frame, leaving the transaction's gas envelope to do the actual settlement.
The alternative considered — coupling ZAP decode directly into the consensus protocol (so contracts never need it) — is rejected because it would mean every cross-chain contract has to receive a parallel "pre-decoded" call-data shadow, which doubles the witness surface and breaks the property that EVM contracts can be written without chain-coupling.
Reference implementation pointer: kcolbchain/switchboard PR #21 ships the Rust codec for ZAP envelopes used by switchboard's wire layer. A Go port that runs as a precompile is the proposed implementation surface; the parsdao lead's parsdao/precompile repo already implements the same TLV walk pattern for unrelated crypto primitives and is the natural home for the cross-impl test vectors.
0x000000000000000000000000000000000000000C (canonical short form 0x0c).
This address is reserved for LP-183. Other Create Protocol precompiles in the genesis registry of a chain that opts in:
0x0a | ML-DSA-65 verify | existing, luxfi/precompile |0x0b | BLS12-381 helpers / AgentEscrow precompile helper | existing, used by AgentEscrow callers |0x0c | ZAP envelope decode | LP-183 (this) |0x0d | Poseidon hash | existing, luxfi/precompile |0x0e | FHE eval (BFV/CKKS subset) | existing, luxfi/fhe |0x0f | SLH-DSA verify (HF #1) | reserved |Input is a single contiguous byte buffer: the raw ZAP frame as it would arrive on the wire from gossip, bridge, or x402 transport. No length prefix is required because the ZAP frame is self-delimiting (header carries total_len); the precompile reads total_len from the header and verifies len(input) == total_len.
magic | 0 | 4 bytes | constant 0x5A 0x41 0x50 0x21 ("ZAP!") per luxfi/zap |version | 4 | 1 byte | currently 0x01; precompile MUST accept only known versions and revert otherwise |scheme | 5 | 1 byte | wire scheme tag (raw / x402-nested / lux-gossip / warp-2.0-nested / agent-payment) |total_len | 6 | 4 bytes (big-endian) | total frame length in bytes including this header; MUST equal len(input) |header_digest | 10 | 32 bytes | keccak256 of the canonical header subset, used for de-dup |payload_len | 42 | 4 bytes (big-endian) | length of the inner payload (excludes nested-envelope trailer if any) |payload | 46 | payload_len bytes | the inner payload — arbitrary bytes from the caller's perspective |nested_tag | 46 + payload_len | 1 byte | 0x00 if no nested envelope; 0x01 if x402; 0x02 if Warp 2.0; other values reserved |nested_payload | … | variable | present iff nested_tag != 0x00; same self-delimiting structure as the outer frame minus the magic |crc32c | last 4 | 4 bytes | CRC-32C (Castagnoli) over magic .. nested_payload |Hard maximum input size: 16384 bytes (16 KiB). Frames larger than this MUST be rejected with a revert and consume the full gas budget (DoS-resistance against oversized-frame griefing). 16 KiB matches the soft ceiling used in kcolbchain/switchboard PR #21 and is a 4× headroom over the largest expected payload (a single x402 payment proof + MPC signature aggregate).
Output is a single ABI-encoded tuple, packed as if returned by:
abi.encode(
uint8 version,
uint8 scheme,
bytes32 headerDigest,
bytes payload,
uint8 nestedTag, // 0x00 = none, 0x01 = x402, 0x02 = warp-2.0
bytes nestedPayload // empty if nestedTag == 0x00
)
The output is consumable directly via abi.decode(precompile_output, (uint8, uint8, bytes32, bytes, uint8, bytes)). The CRC field is not surfaced — the precompile verifies it internally and reverts on mismatch.
When nestedTag == 0x01 (x402 nested), nestedPayload is itself a well-formed x402 envelope per kcolbchain/switchboard x402_middleware.py. The precompile does not recursively decode the x402 envelope — callers that need x402 field access call into the application-layer Solidity decoder. This keeps the precompile's scope tight.
gas = 50 + 8 * len(input)
Pricing rationale:
ecrecover at 3,000 gas which is dominated by EC ops; ZAP decode is byte-shuffling so the floor is lower).RIPEMD-160's 120 + 120/word (~15 gas/byte) and IDENTITY's 15 + 3/word (~3.4 gas/byte). ZAP decode is heavier than IDENTITY (TLV walk + CRC) but lighter than RIPEMD-160 (no cryptographic work).The pricing curve is intentionally chosen so a malicious frame at the 16 KiB ceiling cannot DoS the block (a single tx maxes at ~228 decodes/block).
len(input) < 46 (smaller than minimum header) | Revert; consume min(gas_supplied, 50) |len(input) > 16384 | Revert; consume full supplied gas |magic != "ZAP!" | Revert with INVALID_MAGIC; consume linear gas up to point of failure |version unknown | Revert with UNSUPPORTED_VERSION; consume linear gas |total_len != len(input) | Revert with LENGTH_MISMATCH; consume linear gas |payload_len + 46 > total_len (payload overruns frame) | Revert with PAYLOAD_OVERRUN; consume linear gas |nested_tag != 0x00 but no room for nested payload | Revert with NESTED_TRUNCATED; consume linear gas |crc32c mismatch | Revert with CRC_FAIL; consume full linear gas (CRC is verified last) |nested_tag is reserved (0x03..0xFF) | Revert with RESERVED_NESTED_TAG; consume linear gas |nestedPayload bytes for the caller to handle |CRC verification ordering is intentionally last so an attacker cannot craft a frame that looks valid up to CRC but fails the cheaper structural checks late — both pathological frames pay equal gas; honest frames pay full gas including the verified CRC.
Benchmarks against kcolbchain/switchboard's reference Solidity decoder (committed at PR #21 review branch):
The 595× cost reduction is what makes per-payment, per-anchor, per-message ZAP decode viable. Without it, the per-tx budget for the surrounding business logic (signature checks, state writes, event emission) is exhausted by the decode alone.
Considered design: the consensus layer pre-decodes ZAP frames and exposes the decoded tuple to contracts via a magic system call or pre-populated transaction-scoped storage slot.
Rejected because:
1. EVM execution should remain self-contained. Contracts that take a ZAP frame as input today would have to receive the pre-decoded version as a parallel argument, breaking the contract's invariant that its inputs are exactly its calldata.
2. Witness doubling. Each cross-chain message would have to be carried twice (raw + pre-decoded), increasing block size by ~2× on cross-chain-heavy blocks.
3. Chain coupling. Chains that don't use ZAP-on-the-wire today (e.g. an EVM chain that only does intra-chain activity) would still pay the witness tax.
A precompile is the standard EVM extension point for exactly this case — hot-path primitives that contracts call by reference, not by invariant.
luxfi/precompile already hosts ML-DSA, Poseidon, etc. Adding ZAP decode as a separate precompile rather than overloading an existing slot keeps the gas-pricing models clean (each precompile has a single pricing curve) and lets the LP-183 implementation evolve (versions, error codes) without entangling unrelated primitives.
ZAP frames carry an integrity CRC (CRC-32C, Castagnoli polynomial, hardware-accelerated on Intel SSE 4.2+ and ARM v8 CRC32 extension), separate from the header_digest which is keccak256 over the canonical header subset. CRC32C is for transport-layer integrity (cheap, hardware-accelerated); the keccak digest is for content-addressable de-duplication. The precompile preserves both as distinct fields — it does not collapse them.
LP-183 is a new precompile at a previously-unallocated address (0x0c). No existing contract or precompile is affected. Chains that do not opt into LP-183 simply do not deploy it; calls to 0x0c on those chains revert with the standard "no precompile at address" behavior, indistinguishable from any other unallocated precompile slot.
Chain operators activate LP-183 via a standard EVM upgrade hard fork. Chains that adopt the Create Protocol profile (the first known consumer) activate LP-183 at genesis.
A naive implementation that allocates total_len bytes before verifying the frame structure can be DoS'd by a frame that declares a 16 KiB length but contains malformed inner offsets. Mitigation (normative): the precompile MUST verify payload_len + 46 ≤ total_len and nested offsets fit within total_len before allocating sub-buffers; allocations are bounded by len(input) not by self-declared lengths.
The 16 KiB hard ceiling, combined with full-gas consumption on len(input) > 16384, means an attacker cannot probe the precompile with arbitrarily-large garbage frames at low cost. The smallest revert path (len < 46) costs the 50-gas floor; everything else costs linear gas.
The CRC is verified last. An attacker who can grind a frame to pass structural checks but fail CRC pays full linear gas — same as an honest caller with a transmission error. This is intentional: there is no cheap CRC-bypass.
The precompile surfaces nestedTag as a typed discriminator. Callers MUST gate their nested-envelope logic on nestedTag and MUST NOT pattern-match on nestedPayload content directly — an attacker may inject content that looks x402 inside a frame marked nestedTag == 0x00, and callers that ignore the tag will misinterpret it.
Constant-time concern: CRC32C is data-dependent in its tabular form. For Create Protocol's threat model (validator-set-internal gossip + bridge-anchor receipts), CRC32C timing variance is not exploitable because no secret material is in the CRC's input. Implementations SHOULD use the hardware CRC32 instruction where available; the timing of that instruction is not data-dependent on contemporary Intel/ARM cores.
Because BridgeAnchor will rely on this precompile to verify cross-chain messages, a precompile bug is a bridge bug. The reference implementation MUST be cross-tested against luxfi/zap's Go encoder for at least the corpus of frames in kcolbchain/switchboard PR #21 test vectors. Implementations SHOULD include a fuzz harness that generates random byte strings and verifies that every input either decodes successfully OR reverts cleanly (never panics, never returns malformed output).
Phased reference implementations:
1. Rust reference (already exists). kcolbchain/switchboard PR #21 contains the Rust ZAP codec used by switchboard's wire layer. This is the spec-authoritative implementation for behavior; the precompile MUST match its decode output byte-for-byte on the shared test corpus.
2. Go reference (to port). parsdao/precompile is the proposed home for the Go-side EVM precompile binding. Pattern matches the existing parsdao precompile lattice (TLV walk → bounds check → CRC verify → ABI encode return).
3. luxfi/precompile integration. Once the Go port is stable, register the precompile in luxfi/precompile's precompile registry under 0x0c and add the cross-impl test vectors. This is the integration point for any Lux chain that wants LP-183 active.
Test vector corpus (to ship with the reference impl):
scheme values, no nesting | kcolbchain/switchboard PR #21 |1. CRC vs MAC. Should LP-183 reserve a future nested_tag = 0xFE for a future MAC'd variant (e.g. ZAP+Poly1305) that replaces CRC integrity with an authenticated MAC for adversarial-channel deployments? Argument for: bridge-anchor receipts cross trust boundaries. Argument against: ZAP's threat model assumes signed payloads, not authenticated transport. [TBD: parsdao lead + luxfi/zap maintainer input]
2. Maximum frame size. Is 16 KiB the right ceiling? The largest envelope kcolbchain/switchboard PR #21 emits today is 3.8 KiB (x402 + MPC aggregate); 16 KiB is 4× headroom. A larger ceiling (e.g. 64 KiB) would let LP-183 cover bridge-anchor batch receipts (currently split across multiple frames). [TBD: confirm with luxfi/bridge maintainer whether batch-mode receipts should fit in one frame]
3. Gas coefficient calibration. The 8 gas/byte coefficient is calibrated against synthetic benchmarks. Once the Go port lands on parsdao/precompile, re-benchmark on the standard precompile harness and ratify or adjust. [TBD: re-benchmark post-port]
4. Nesting depth. Single-level nesting is sufficient for x402 + Warp 2.0 today. If a future protocol wants Warp(x402(...)) (Warp envelope wrapping an x402 payment proof for cross-chain settlement of agent payments), do we extend LP-183 to 2-level or require the caller to call the precompile recursively? [TBD: bridge + agent-payments coupling]
5. Error code surfacing. The current design reverts with named string errors. Should LP-183 instead return a structured error code as a uint8 in the output tuple, with version == 0xFF indicating an error state? Argument for: lets contracts handle errors without try-catch. Argument against: precompiles conventionally revert. [TBD: ratify against luxfi/precompile convention]
6. Versioning. The version byte is 0x01 today. What is the migration path when ZAP wire format changes (e.g. for ML-DSA-87 support in HF #2)? Must LP-183 accept both 0x01 and 0x02 simultaneously, or do we deploy 0x1c as the v2-only precompile? [TBD: align with Lux LP convention for precompile versioning]
---