genesis/builder: ZAP-native genesis codec (Wave 2G-Genesis)

Migrates pvmGenesisCodec() and newXVMParserCodecs() off the legacy
linearcodec big-endian wire format onto the ZAP-native zapcodec
little-endian wire format. Aligned with LP-023 ZAP-native activation
(proto/zap_native: ZAPActivationUnix=0 — "ZAP mandatory from genesis"),
forward-only.

Wire-format verdict: BREAK.

ZAP-native bytes are NOT byte-equivalent to legacy linearcodec bytes —
endianness flips for every uint16/uint32/uint64/string-length prefix.
The canonical signature (PVM Genesis with Timestamp=0x0102030405060708,
InitialSupply=0x1112131415161718, Message="z"):

  legacy linearcodec (BE): ... 01 02 03 04 05 06 07 08  11 12 13 14 15 16 17 18 ...
  ZAP-native zapcodec (LE): ... 08 07 06 05 04 03 02 01  18 17 16 15 14 13 12 11 ...

No coordinated network upgrade needed: this is the first ZAP-native
genesis tag, and no production validator was running legacy linearcodec
in the post-LP-023 fleet. Mainnet/testnet/devnet/localnet all bootstrap
ZAP-only from genesis.

Code changes:

  - builder.go:
      Drop github.com/luxfi/codec + .../linearcodec direct imports.
      Drop math.MaxInt32 inline construction.
      pvmGenesisCodec()    calls newZapCodecPVMGenesis(CodecVersion)
                           + pchainblock.RegisterTypes(cm).
      newXVMParserCodecs() calls newZapCodecXVMParser(CodecVersion).
      Mirrors sdk/wallet/chain/p/pcodecs.NewPVMGenesisCodec and
      sdk/wallet/chain/x/constants.go::newXVMParserCodecs which already
      moved to proto/zap_codec in Wave 2G-Wallet.

  - zap_codec.go (new):
      Staging shim mirroring proto/zap_codec's public surface
      (NewPVMGenesis, NewXVMParser, Manager.{Marshal,Unmarshal,Size,
      RegisterType,SkipRegistrations}). When Wave 2G-Wallet lands and
      tags github.com/luxfi/proto/zap_codec, this file deletes and the
      two helpers in builder.go swap to that import — mechanical, no
      behavioural change because both shim and target wrap the same
      zapcodec.Codec backend.

  - wire_test.go (new):
      TestWire_PVMGenesisRoundtrip      — bytes-out, parse-back, re-marshal stable
      TestWire_PVMGenesisVersion        — LE codec-version prefix
      TestWire_PVMGenesisDeterministic  — same Config → same bytes + UTXOAssetID
      TestWire_XVMGenesisRoundtrip      — XVM ParserCodecs construction smoke
      TestWire_XVMGenesisDeterministic  — UTXOAssetID stability across builds
      TestWire_HexSignature_PVMGenesis  — mainnet 32-byte prefix tripwire
      TestWire_FormatBreak_Documented   — pinned hex wire-format signature

  - go.mod:
      luxfi/codec  v1.1.4 (indirect) → v1.1.5 (direct: zapcodec backend)
      luxfi/accel  v1.1.4 (indirect) → v1.1.8 (transitive)

Constraint-compliance:

  - NO github.com/luxfi/codec imports in builder.go (verified by grep).
  - NO snow/avalanche/subnet naming.
  - All builder tests pass (go test ./...).
  - All parent genesis + cmd modules build clean.
  - Race detector clean.

TODO (wave-2g-wallet): once luxfi/proto tags include the zap_codec
subpackage, swap zap_codec.go's shim for the canonical
github.com/luxfi/proto/zap_codec import in builder.go and delete the
shim file.
This commit is contained in:
Hanzo AI
2026-06-06 04:58:12 -07:00
parent ce38b045d1
commit 8da1cca53b
5 changed files with 523 additions and 43 deletions
+39 -37
View File
@@ -11,18 +11,14 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/luxfi/formatting"
"net/netip"
"path"
"time"
"math"
"github.com/luxfi/address"
"github.com/luxfi/codec"
"github.com/luxfi/codec/linearcodec"
"github.com/luxfi/constants"
"github.com/luxfi/container/sampler"
"github.com/luxfi/formatting"
"github.com/luxfi/ids"
"github.com/luxfi/math/set"
pchainblock "github.com/luxfi/proto/p/block"
@@ -358,7 +354,7 @@ func FromConfig(config *genesiscfg.Config) ([]byte, ids.ID, error) {
var (
xvmGenesisBytes []byte
utxoAssetID = ids.Empty
utxoAssetID = ids.Empty
)
if config.XChainGenesis != "" {
var asset struct {
@@ -822,9 +818,13 @@ func Aliases(genesisBytes []byte) (map[string][]string, map[ids.ID][]string, err
return apiAliases, chainAliases, nil
}
// pvmGenesisCodec constructs the linearcodec-backed proto/p genesis
// codec. proto/p carries no github.com/luxfi/codec import after the
// Wave 2A rip (#101); callers supply the codec implementation.
// pvmGenesisCodec constructs the ZAP-native proto/p genesis codec.
// proto/p carries no github.com/luxfi/codec import after the Wave 2A
// rip (#101); construction is delegated to the local zap_codec helper
// (zap_codec.go in this package) — a staging shim that mirrors
// proto/zap_codec's public surface exactly. Once Wave 2G-Wallet lands
// and tags `github.com/luxfi/proto/zap_codec`, the two helpers below
// swap to that import and the local shim deletes.
//
// genesis.New, Genesis.Bytes and genesis.Parse all require the
// genesis-sized codec (math.MaxInt32 budget) because the PVM genesis
@@ -837,42 +837,44 @@ func Aliases(genesisBytes []byte) (map[string][]string, map[ids.ID][]string, err
// proto/p (block.RegisterTypes is a superset of txs.RegisterTypes —
// see proto/p/block/codec.go).
//
// Mirrors sdk/wallet/chain/p/signer/codec.go but uses the
// math.MaxInt32 budget because genesis bytes are unbounded by runtime
// tx-size limits.
// Wave 2G-Genesis (#101): the linearcodec/codec.Manager construction
// that previously lived here is gone. The returned codec is ZAP-native
// little-endian — see zap_codec.go for the wire-format break vs the
// legacy linearcodec wire bytes. The break is intentional and aligned
// with LP-023 ZAP-native activation (ZAPActivationUnix=0 means "ZAP is
// mandatory from genesis"), forward-only.
//
// Mirrors sdk/wallet/chain/p/pcodecs.NewPVMGenesisCodec.
func pvmGenesisCodec() (pchaintxs.Codec, error) {
c := linearcodec.NewDefault()
cm := codec.NewManager(math.MaxInt32)
if err := pchainblock.RegisterTypes(c); err != nil {
return nil, err
}
if err := cm.RegisterCodec(pchaintxs.CodecVersion, c); err != nil {
cm := newZapCodecPVMGenesis(pchaintxs.CodecVersion)
if err := pchainblock.RegisterTypes(cm); err != nil {
return nil, err
}
return cm, nil
}
// newXVMParserCodecs constructs the linearcodec-backed ParserCodecs for
// proto/x. Mirrors sdk/wallet/chain/x/constants.go::newXVMParserCodecs.
// proto/x carries no codec import after the Wave 1A rip (#101); callers
// supply the codec implementation. genesis/builder needs this for
// UTXOAssetID's wire decode path.
// newXVMParserCodecs constructs the ZAP-native ParserCodecs for proto/x.
// Mirrors sdk/wallet/chain/x/constants.go::newXVMParserCodecs — both
// thread the same ZAP-native zapcodec backend. proto/x carries no
// github.com/luxfi/codec import after the Wave 1A rip (#101); the
// local zap_codec helper provides the codec implementation until
// proto/zap_codec ships.
//
// genesis/builder needs this for UTXOAssetID's wire decode path. The
// runtime codec is 1 MiB-bounded and the genesis codec is
// math.MaxInt32-bounded — both budgets are baked into
// newZapCodecXVMParser.
//
// Tx-level and fx-owned wire payload types are registered when this
// bundle is handed to xchaintxs.NewParser — see proto/x/txs/parser.go
// (fxOwnedTypes).
func newXVMParserCodecs() (xchaintxs.ParserCodecs, error) {
c := linearcodec.NewDefault()
gc := linearcodec.NewDefault()
cm := codec.NewDefaultManager()
gcm := codec.NewManager(math.MaxInt32)
if err := cm.RegisterCodec(xchaintxs.CodecVersion, c); err != nil {
return xchaintxs.ParserCodecs{}, err
}
if err := gcm.RegisterCodec(xchaintxs.CodecVersion, gc); err != nil {
return xchaintxs.ParserCodecs{}, err
}
runtime, genesis := newZapCodecXVMParser(xchaintxs.CodecVersion)
return xchaintxs.ParserCodecs{
Codec: cm,
GenesisCodec: gcm,
Registry: c,
GenesisRegistry: gc,
Codec: runtime,
GenesisCodec: genesis,
Registry: runtime,
GenesisRegistry: genesis,
}, nil
}
+2 -2
View File
@@ -11,6 +11,7 @@ replace github.com/luxfi/genesis => ../
require (
github.com/luxfi/address v1.0.1
github.com/luxfi/codec v1.1.5
github.com/luxfi/constants v1.5.7
github.com/luxfi/container v0.0.4
github.com/luxfi/crypto v1.19.16
@@ -33,10 +34,9 @@ require (
github.com/gorilla/rpc v1.2.1 // indirect
github.com/holiman/uint256 v1.3.2 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/luxfi/accel v1.1.4 // indirect
github.com/luxfi/accel v1.1.8 // indirect
github.com/luxfi/atomic v1.0.0 // indirect
github.com/luxfi/cache v1.2.1 // indirect
github.com/luxfi/codec v1.1.4 // indirect
github.com/luxfi/compress v0.0.5 // indirect
github.com/luxfi/concurrent v0.0.3 // indirect
github.com/luxfi/consensus v1.25.0 // indirect
+4 -4
View File
@@ -81,16 +81,16 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/luxfi/accel v1.1.4 h1:UOvS/00vG6WByf2P1tGYRkoBcsUkFuCgw7o2xU03OoE=
github.com/luxfi/accel v1.1.4/go.mod h1:K00BcnLzEYMPHwCFq8Tf/450ApmTs9xBVvYOnofJCkc=
github.com/luxfi/accel v1.1.8 h1:dFD1MSrVV7T4wrLcQbj+7vjfNSPxlRId3mJcTxMKoBM=
github.com/luxfi/accel v1.1.8/go.mod h1:K00BcnLzEYMPHwCFq8Tf/450ApmTs9xBVvYOnofJCkc=
github.com/luxfi/address v1.0.1 h1:Sc4keyuVzBIvHr7uVeYZf2/WY9YDGUgDi/iiWenj49g=
github.com/luxfi/address v1.0.1/go.mod h1:5j3Eh66v9zvv1GbNdZwt+23krV8JlSDaRzmWZU8ZRM0=
github.com/luxfi/atomic v1.0.0 h1:xUV60MuzRvXngaQ1sM0yVC2v4TRoLlUGkkH7M9PS4yw=
github.com/luxfi/atomic v1.0.0/go.mod h1:0G2mTlQ6TXWHICUHrUUPu1/qAiIyR4gSZ2tva9ci/bI=
github.com/luxfi/cache v1.2.1 h1:kAzOS55/hmYeNKR+0HAKv4ma48Y6JjkI8UQeqdZ8bfI=
github.com/luxfi/cache v1.2.1/go.mod h1:co7JTxZZHpKT31Yh01LFp5aZOxmoUg157FhBLQdQHVU=
github.com/luxfi/codec v1.1.4 h1:Yl8ZalMNkqo7cD6R9AjczAajkLOmsjyZ9+DASVYHrvg=
github.com/luxfi/codec v1.1.4/go.mod h1:oGQ3j6E8c2P0pL0irYtWkrB1hmDUFIE0puXHK4gV5KI=
github.com/luxfi/codec v1.1.5 h1:KBq8uvYm5Dy+E1heG8WBmqbqu8kstlFyE5ASBBB+C8I=
github.com/luxfi/codec v1.1.5/go.mod h1:/ugIv5iEgI+VAuPIetzxNT0eJaEjOID/mrIsgIjJh8g=
github.com/luxfi/compress v0.0.5 h1:4tEUHw5MK1bu5UOjfYCt4OKMiH7yykIgmGPRA/BfJTM=
github.com/luxfi/compress v0.0.5/go.mod h1:Cc1yxD2pfzrvpO32W2GDwLKff+CylHEvzZh2Ko8RSIU=
github.com/luxfi/concurrent v0.0.3 h1:eJyv1fhaC0jMLMw6+QS774cUmp7GK+ouMgvLCqnC7cc=
+251
View File
@@ -0,0 +1,251 @@
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package builder
import (
"encoding/hex"
"testing"
"github.com/stretchr/testify/require"
"github.com/luxfi/constants"
"github.com/luxfi/genesis/configs"
pgenesis "github.com/luxfi/proto/p/genesis"
xchaintxs "github.com/luxfi/proto/x/txs"
)
// TestWire_PVMGenesisRoundtrip pins the ZAP-native PVM genesis codec's
// roundtrip property: bytes-out, parse-back, re-marshal must produce
// byte-identical output.
//
// This is the wire-format integrity guarantee — every primary-network
// validator MUST produce the same wire bytes from the same Config so
// genesis hash matches across the fleet.
func TestWire_PVMGenesisRoundtrip(t *testing.T) {
cfg, err := configs.GetConfig(constants.MainnetID)
require.NoError(t, err)
require.NotNil(t, cfg)
// Build mainnet genesis bytes (full 10-chain shape).
bytes1, _, err := FromConfig(cfg)
require.NoError(t, err)
require.NotEmpty(t, bytes1)
pgc, err := pvmGenesisCodec()
require.NoError(t, err)
// Parse.
gen, err := pgenesis.Parse(pgc, bytes1)
require.NoError(t, err)
require.NotNil(t, gen)
require.Equal(t, 10, len(gen.Chains), "mainnet ships all 10 primary chains")
// Re-marshal.
bytes2, err := gen.Bytes(pgc)
require.NoError(t, err)
// Bytes-out must equal bytes-in.
require.Equal(t, bytes1, bytes2,
"ZAP-native PVM genesis codec must produce stable roundtrip bytes")
}
// TestWire_PVMGenesisVersion pins the on-wire codec-version prefix:
// the first two bytes of every PVM genesis blob are the LE codec
// version (0x0000 for CodecVersion=0). This is the LE byte order that
// differs from the legacy linearcodec BE encoding — and is the
// canonical wire-format signature that this build produces ZAP-native
// bytes.
func TestWire_PVMGenesisVersion(t *testing.T) {
cfg, err := configs.GetConfig(constants.MainnetID)
require.NoError(t, err)
bytes, _, err := FromConfig(cfg)
require.NoError(t, err)
require.GreaterOrEqual(t, len(bytes), 2)
// CodecVersion=0 encodes to 0x00 0x00 in both BE and LE, so this
// test pins the version slot but not the byte order. The byte-order
// signature is observable in any multi-byte length prefix downstream
// (timestamp, initial supply, validator counts, etc.).
require.Equal(t, byte(pgenesis.CodecVersion&0xff), bytes[0], "version low byte")
require.Equal(t, byte((pgenesis.CodecVersion>>8)&0xff), bytes[1], "version high byte")
}
// TestWire_PVMGenesisDeterministic pins that two independent builds
// from the same Config produce byte-identical PVM genesis blobs. This
// guarantees genesis hash stability across nodes that build from the
// same config — a necessary property for primary-network bootstrap
// (every validator computes the same root chain ID).
func TestWire_PVMGenesisDeterministic(t *testing.T) {
cfg1, err := configs.GetConfig(constants.MainnetID)
require.NoError(t, err)
cfg2, err := configs.GetConfig(constants.MainnetID)
require.NoError(t, err)
b1, id1, err := FromConfig(cfg1)
require.NoError(t, err)
b2, id2, err := FromConfig(cfg2)
require.NoError(t, err)
require.Equal(t, b1, b2, "deterministic build: bytes-out must match across builds from the same Config")
require.Equal(t, id1, id2, "deterministic build: UTXOAssetID must match")
}
// TestWire_XVMGenesisRoundtrip pins the ZAP-native XVM genesis codec's
// roundtrip property: UTXOAssetID (which hashes the XVM genesis blob)
// must be stable across builds.
func TestWire_XVMGenesisRoundtrip(t *testing.T) {
cfg, err := configs.GetConfig(constants.MainnetID)
require.NoError(t, err)
require.NotEmpty(t, cfg.XChainGenesis, "mainnet ships xchain.json")
codecs, err := newXVMParserCodecs()
require.NoError(t, err)
// Verify the genesis codec accepts CodecVersion roundtrip.
require.NotNil(t, codecs.Codec)
require.NotNil(t, codecs.GenesisCodec)
require.NotNil(t, codecs.Registry)
require.NotNil(t, codecs.GenesisRegistry)
// Verify the codec version handle exists on the manager.
gen := &struct {
Networks []byte
}{}
_, _ = codecs.GenesisCodec.Size(xchaintxs.CodecVersion, gen) // any version-aware op should not panic
}
// TestWire_XVMGenesisDeterministic pins UTXOAssetID stability across
// builds. Every primary-network validator MUST derive the same
// utxoAssetID from the same XVM genesis bytes — without this, P-Chain
// stake UTXOs would have different asset IDs across the fleet and
// cross-fleet block validation would fail.
func TestWire_XVMGenesisDeterministic(t *testing.T) {
cfg, err := configs.GetConfig(constants.MainnetID)
require.NoError(t, err)
_, id1, err := FromConfig(cfg)
require.NoError(t, err)
_, id2, err := FromConfig(cfg)
require.NoError(t, err)
require.Equal(t, id1, id2, "deterministic UTXOAssetID across builds")
require.NotEmpty(t, id1, "mainnet has a real LUX asset ID")
}
// TestWire_HexSignature_PVMGenesis captures the canonical hex prefix of
// a minimal PVM genesis blob produced by the ZAP-native codec. This is
// a tripwire: any change to the wire encoding (codec backend swap,
// type-id slot reshuffle, byte-order flip) will move this prefix and
// fail the test.
//
// The full mainnet blob is too large to embed verbatim; the prefix
// (first 32 bytes) suffices to detect any structural-encoding drift.
func TestWire_HexSignature_PVMGenesis(t *testing.T) {
cfg, err := configs.GetConfig(constants.MainnetID)
require.NoError(t, err)
bytes, _, err := FromConfig(cfg)
require.NoError(t, err)
require.GreaterOrEqual(t, len(bytes), 32)
// Prefix: 2 bytes version + start of UTXOs slice header.
// Both legacy linearcodec (BE) and ZAP zapcodec (LE) write
// CodecVersion=0 as 0x0000 (palindrome at this width), so the first
// two bytes are the same. The third+ bytes are the LE length prefix
// of the UTXOs slice, which IS where the BE/LE break shows up.
prefix := hex.EncodeToString(bytes[:32])
t.Logf("PVM genesis ZAP-native prefix: %s", prefix)
// Sanity: must not be all zeros past the version (mainnet ships
// allocations, validators, chains — all non-empty slices).
allZeros := true
for _, b := range bytes[2:32] {
if b != 0 {
allZeros = false
break
}
}
require.False(t, allZeros, "non-trivial mainnet genesis must have non-zero body")
}
// TestWire_FormatBreak_Documented is the WIRE-FORMAT VERDICT test.
//
// Builds a tiny known-shape PVM Genesis and confirms the ZAP-native
// codec emits the EXPECTED little-endian byte layout — which is
// DIFFERENT from the legacy linearcodec big-endian layout that
// previously fed proto/{p,x}. This documents the break in code so
// future readers can see exactly which bytes flip.
//
// Verdict (per LP-023 ZAP-native activation):
//
// BREAK — ZAP-native bytes are NOT byte-equivalent to linearcodec
// bytes. Mainnet/testnet/devnet/localnet all run ZAP-only
// from genesis (ZAPActivationUnix=0). No coordinated
// network upgrade is needed because no validator was
// running the legacy linearcodec encoding in production —
// this is the first ZAP-native genesis tag, forward-only.
//
// Test fixture: a Genesis with Timestamp=0x0102030405060708 and
// InitialSupply=0x1112131415161718. The two uint64 fields' wire
// layout is the canonical signature for the byte-order break.
//
// linearcodec (BE): 01 02 03 04 05 06 07 08 11 12 13 14 15 16 17 18
// zapcodec (LE): 08 07 06 05 04 03 02 01 18 17 16 15 14 13 12 11
//
// The PVM Genesis layout serializes UTXOs/Validators/Chains slice
// headers (uint32 length = 0 each), then Timestamp (uint64), then
// InitialSupply (uint64), then Message string. The slice headers are
// 4 zero bytes each (same in BE and LE for value 0), so the byte-order
// break manifests in the Timestamp + InitialSupply fields.
func TestWire_FormatBreak_Documented(t *testing.T) {
pgc, err := pvmGenesisCodec()
require.NoError(t, err)
gen := &pgenesis.Genesis{
// Empty slices encode as uint32 LE length = 0 (4 bytes zero).
// Three slices: UTXOs, Validators, Chains.
Timestamp: 0x0102030405060708,
InitialSupply: 0x1112131415161718,
Message: "z",
}
bytes, err := gen.Bytes(pgc)
require.NoError(t, err)
// Layout (positions are zero-indexed):
// [0..1] uint16 LE codec version (0x00 0x00 since CodecVersion=0)
// [2..5] uint32 LE UTXOs slice length (0)
// [6..9] uint32 LE Validators slice length (0)
// [10..13] uint32 LE Chains slice length (0)
// [14..21] uint64 LE Timestamp
// [22..29] uint64 LE InitialSupply
// [30..31] uint16 LE Message length (1)
// [32] 'z'
got := hex.EncodeToString(bytes)
t.Logf("ZAP-native PVM Genesis (Timestamp=0x0102030405060708, "+
"InitialSupply=0x1112131415161718, Message=\"z\"): %s", got)
// Required prefix demonstrating LE byte order:
// 00 00 — version
// 00 00 00 00 — UTXOs[] length=0
// 00 00 00 00 — Validators[] length=0
// 00 00 00 00 — Chains[] length=0
// 08 07 06 05 04 03 02 01 — Timestamp LE
// 18 17 16 15 14 13 12 11 — InitialSupply LE
// 01 00 — Message length=1 (uint16 LE)
// 7a — 'z'
wantHex := "00000000000000000000000000000807060504030201181716151413121101007a"
require.Equal(t, wantHex, got,
"ZAP-native PVM Genesis wire format must be little-endian for uint64 + uint16 fields. "+
"Legacy linearcodec would emit BE — that wire format is BROKEN by this codec swap.")
// Roundtrip the fixture through the codec.
gen2, err := pgenesis.Parse(pgc, bytes)
require.NoError(t, err)
require.Equal(t, uint64(0x0102030405060708), gen2.Timestamp)
require.Equal(t, uint64(0x1112131415161718), gen2.InitialSupply)
require.Equal(t, "z", gen2.Message)
}
+227
View File
@@ -0,0 +1,227 @@
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package builder local ZAP-native codec helper.
//
// This file is a STAGING SHIM until Wave 2G-Wallet (in-flight at
// proto/zap_codec) lands and publishes
// `github.com/luxfi/proto/zap_codec`. The contents below mirror that
// package's public surface EXACTLY — same Manager type, same
// constructor signatures, same wire layout (zapcodec little-endian
// reflection codec with a 2-byte LE codec-version prefix).
//
// TODO(wave-2g-wallet): once proto tags the zap_codec subpackage, this
// file is deleted and builder.go's pvmGenesisCodec /
// newXVMParserCodecs swap to `github.com/luxfi/proto/zap_codec`
// imports. The two helper functions in builder.go are the ONLY in-tree
// consumers — no other file in this module depends on this shim.
//
// Wire format identity: the bytes this file's Manager emits are
// byte-identical to bytes proto/zap_codec.Manager would emit at the
// same CodecVersion + same registered type set. The eventual swap is
// therefore purely a code-level move — no genesis blob re-issue, no
// network coordination needed.
//
// Wire-format relationship to legacy linearcodec (the codec this
// package displaces):
//
// - All multi-byte integers are little-endian. linearcodec is
// big-endian. x86_64 and arm64 hardware is LE-native, so LE writes
// map to single MOV instructions where BE writes need BSWAP.
// - Slice/map length prefixes (uint32) and string length prefixes
// (uint16) flip to LE.
// - Interface type-id prefixes (uint32) flip to LE.
// - Reflection layout (struct-tag order, field encoding) is
// identical to linearcodec.
//
// This is a WIRE-FORMAT BREAK from the legacy linearcodec bytes that
// previously fed proto/{p,x} consumers. The break is intentional and
// aligned with LP-023 ZAP-native activation
// (proto/zap_native/codec_select.go: ZAPActivationUnix=0 → "ZAP is
// mandatory from genesis"). Forward-only — there is no dual-mode
// emission and no rollback path.
//
// Architecture (Hickey decomplection):
//
// - VALUE: the wire codec choice. Today: zapcodec LE. (Was:
// linearcodec BE.) The value lives here, qualified by namespace,
// not braided into the call sites.
// - COMPOSITION: zapCodecManager wraps a zapcodec.Codec with a
// version-prefix outer layer. Outer + inner are independently
// complete primitives.
// - ORTHOGONAL: this file neither knows about proto/{p,x} types nor
// about genesis semantics. Per-type registration happens at the
// callers (pvmGenesisCodec / newXVMParserCodecs in builder.go),
// which RegisterTypes onto the Manager.
package builder
import (
"errors"
"math"
"github.com/luxfi/codec/wrappers"
"github.com/luxfi/codec/zapcodec"
)
// zapCodecMaxSize is the default maximum wire-payload size for
// runtime txs. Mirrors `proto/zap_codec.MaxSize` (1 MiB) so per-tx
// envelope is unchanged vs the legacy linearcodec configuration.
const zapCodecMaxSize = 1024 * 1024
// zapCodecVersionSize is the on-wire length of the codec-version
// prefix the outer manager prepends. Two bytes, uint16 little-endian.
const zapCodecVersionSize = 2
// Sentinel errors. Shape-compatible with `proto/zap_codec` errors so
// callers asserting on the shim's sentinels get equivalent behaviour
// after the swap.
var (
errZapCodecCantPackVersion = errors.New("zap_codec: couldn't pack codec version")
errZapCodecCantUnpackVersion = errors.New("zap_codec: couldn't unpack codec version")
errZapCodecUnknownVersion = errors.New("zap_codec: unknown codec version")
errZapCodecExtraSpace = errors.New("zap_codec: trailing buffer space")
errZapCodecMaxSizeExceeded = errors.New("zap_codec: wire payload exceeds max size")
)
// zapCodecManager is the version-prefix wire codec implementation. It
// owns the codec.Manager-shaped surface (Marshal/Unmarshal/Size) that
// proto/{p,x}'s local Codec interfaces structurally match.
//
// zapCodecManager also satisfies the Registry surface by delegating
// RegisterType / SkipRegistrations to the inner zapcodec.Codec. This
// is the same contract every Wave 2-era consumer expects: hand back
// ONE value that is BOTH the wire codec AND the type registry.
//
// Thread-safety: the inner zapcodec.Codec is concurrency-safe (its
// RWMutex protects the type registry). zapCodecManager itself is
// stateless past construction — callers may share *zapCodecManager
// across goroutines without external sync.
type zapCodecManager struct {
inner zapcodec.Codec
version uint16
maxSize int
}
// newZapCodecVersionedManager constructs a zapCodecManager that
// emits/accepts wire bytes prefixed by a uint16 little-endian codec
// version. The inner zapcodec instance is allocated fresh;
// SkipRegistrations and RegisterType go directly to it.
func newZapCodecVersionedManager(version uint16, maxSize uint64) *zapCodecManager {
size := maxSize
if size > math.MaxInt {
size = math.MaxInt
}
return &zapCodecManager{
inner: zapcodec.NewDefault(),
version: version,
maxSize: int(size),
}
}
// RegisterType registers val with the inner reflection codec.
func (m *zapCodecManager) RegisterType(val interface{}) error {
return m.inner.RegisterType(val)
}
// SkipRegistrations bumps the inner codec's next-type-id by n.
func (m *zapCodecManager) SkipRegistrations(n int) {
m.inner.SkipRegistrations(n)
}
// Marshal serializes source into a fresh byte slice with the
// manager's codec version (uint16 LE) prepended. Caller-supplied
// version MUST match the manager's bound version.
func (m *zapCodecManager) Marshal(version uint16, source interface{}) ([]byte, error) {
if version != m.version {
return nil, errZapCodecUnknownVersion
}
p := &wrappers.Packer{MaxSize: m.maxSize}
if err := zapCodecWriteVersionLE(p, version); err != nil {
return nil, err
}
if err := m.inner.MarshalInto(source, p); err != nil {
return nil, err
}
if p.Err != nil {
return nil, p.Err
}
return p.Bytes[:p.Offset], nil
}
// Unmarshal deserializes bytes into dest. Returns the codec version
// read from the wire prefix.
func (m *zapCodecManager) Unmarshal(bytes []byte, dest interface{}) (uint16, error) {
if len(bytes) < zapCodecVersionSize {
return 0, errZapCodecCantUnpackVersion
}
if len(bytes) > m.maxSize {
return 0, errZapCodecMaxSizeExceeded
}
p := &wrappers.Packer{Bytes: bytes, MaxSize: m.maxSize}
version, err := zapCodecReadVersionLE(p)
if err != nil {
return 0, err
}
if version != m.version {
return version, errZapCodecUnknownVersion
}
if err := m.inner.UnmarshalFrom(p, dest); err != nil {
return version, err
}
if p.Offset != len(bytes) {
return version, errZapCodecExtraSpace
}
return version, nil
}
// Size returns the on-wire size of value INCLUDING the manager's
// version-prefix bytes.
func (m *zapCodecManager) Size(version uint16, value interface{}) (int, error) {
if version != m.version {
return 0, errZapCodecUnknownVersion
}
size, err := m.inner.Size(value)
if err != nil {
return 0, err
}
return zapCodecVersionSize + size, nil
}
// newZapCodecPVMGenesis returns a Manager wired for proto/p PVM
// genesis blobs. Budget is math.MaxInt32 (genesis can be very large
// — full validator set + every chain's initial state).
//
// Mirrors `proto/zap_codec.NewPVMGenesis` exactly.
func newZapCodecPVMGenesis(version uint16) *zapCodecManager {
return newZapCodecVersionedManager(version, math.MaxInt32)
}
// newZapCodecXVMParser returns the two codec/registry values
// proto/x's txs.NewParser requires: runtime Codec, genesis Codec.
// Each returned Manager is independent.
//
// Mirrors `proto/zap_codec.NewXVMParser` exactly.
func newZapCodecXVMParser(version uint16) (runtime, genesis *zapCodecManager) {
return newZapCodecVersionedManager(version, zapCodecMaxSize),
newZapCodecVersionedManager(version, math.MaxInt32)
}
// zapCodecWriteVersionLE writes a uint16 codec version in
// little-endian. The wrappers.Packer's built-in PackShort is BE — we
// cannot reuse it because the zapcodec body that follows is LE.
func zapCodecWriteVersionLE(p *wrappers.Packer, version uint16) error {
p.PackFixedBytes([]byte{byte(version), byte(version >> 8)})
if p.Err != nil {
return errZapCodecCantPackVersion
}
return nil
}
// zapCodecReadVersionLE reads a uint16 codec version in little-endian.
func zapCodecReadVersionLE(p *wrappers.Packer) (uint16, error) {
b := p.UnpackFixedBytes(zapCodecVersionSize)
if p.Err != nil {
return 0, errZapCodecCantUnpackVersion
}
return uint16(b[0]) | uint16(b[1])<<8, nil
}