zap_codec: BE-fallback read for pre-LP-023 (v1.28.x) P-chain state

Validators upgrading from luxd v1.28.x hold P-chain state written with
the pre-LP-023 BE codec version prefix. zap_codec.Unmarshal now tries
LE first; on unknown version, falls back to BE before erroring.

Behaviour:
- LE matches → use LE codec (post-Dec-25-2025 writers)
- LE unknown, BE matches → use BE codec with BE prefix, advance offset
  past VersionSize, decode body
- Both unknown → ErrUnknownVersion as before

Disambiguation is safe because registered version IDs are small (0..N
for the handful of codec generations). Palindromic byte-pair values
agree under both endianness, so the only ambiguous case is small N where
both BE and LE happen to be registered — practically impossible with
the current registry.

Also:
- ZAPActivationUnix: 0 → 1766708400 (Dec 25 2025 16:20 PST) — same as
  all other Quasar-Edition forks. Documents that LP-023 was a network
  activation, not a binary cutover.
- LegacyEnabled: defaults true unless LUXD_ENABLE_LEGACY_CODEC=0 — keeps
  legacy linearcodec path reachable for historical bytes that didn't
  migrate through the BE-fallback shim.
This commit is contained in:
Hanzo AI
2026-06-06 16:55:39 -07:00
parent b38d23a057
commit a7014e0340
3 changed files with 72 additions and 22 deletions
+18
View File
@@ -233,6 +233,24 @@ func (m *multiManager) Unmarshal(bytes []byte, dest interface{}) (uint16, error)
}
c, exists := m.codecs[version]
if !exists {
// LP-023 cutover compat: pre-Dec-25-2025 writers used BE for
// the version prefix. If LE decode yields an unregistered
// version, retry as BE before erroring. Registered version IDs
// are small (0..N for a few codec generations) — BE-decoded
// small numbers don't collide with LE-decoded small numbers
// except at palindromic byte pairs (where the two decodings
// agree).
beVersion := peekVersionBE(bytes)
if cBE, beExists := m.codecs[beVersion]; beExists {
p.Offset = VersionSize
if err := cBE.UnmarshalFrom(p, dest); err != nil {
return beVersion, err
}
if p.Offset != len(bytes) {
return beVersion, ErrExtraSpace
}
return beVersion, nil
}
return version, ErrUnknownVersion
}
if err := c.UnmarshalFrom(p, dest); err != nil {
+31
View File
@@ -186,6 +186,18 @@ func (m *Manager) Unmarshal(bytes []byte, dest interface{}) (uint16, error) {
return 0, err
}
if version != m.version {
// LP-023 cutover compat: pre-Dec-25-2025 writers used BE for
// the version prefix. Retry as BE before erroring.
if peekVersionBE(bytes) == m.version {
p.Offset = VersionSize
if err := m.inner.UnmarshalFrom(p, dest); err != nil {
return m.version, err
}
if p.Offset != len(bytes) {
return m.version, ErrExtraSpace
}
return m.version, nil
}
return version, ErrUnknownVersion
}
if err := m.inner.UnmarshalFrom(p, dest); err != nil {
@@ -303,3 +315,22 @@ func readVersionLE(p *wrappers.Packer) (uint16, error) {
}
return uint16(lo) | uint16(hi)<<8, nil
}
// peekVersionBE reads the codec version as big-endian WITHOUT consuming
// any packer offset (caller is responsible for advancing on success).
// Returns the BE-decoded version. Used by the dual-read fallback to
// support reading P-chain DB state written by pre-LP-023 (v1.28.x and
// earlier) luxd binaries which used wrappers.PackShort (big-endian)
// for the version prefix.
//
// LP-023 cutover is Dec 25 2025 16:20 PST (unix 1766708400). Bytes
// written before activation use BE; bytes after use LE. The version
// number is small (0N for the few registered codec versions), so we
// disambiguate by trying LE first and falling back to BE only if the
// LE-decoded version is not registered.
func peekVersionBE(bytes []byte) uint16 {
if len(bytes) < VersionSize {
return 0
}
return uint16(bytes[0])<<8 | uint16(bytes[1])
}
+23 -22
View File
@@ -9,33 +9,34 @@ import (
)
// ZAPActivationUnix is the unix timestamp at which P-chain wire format
// switches from legacy linearcodec (CodecVersionV0/V1) to native ZAP. The
// switch governs the WRITE path: post-activation blocks MUST be ZAP-encoded.
// Validator coordination is documented in LP-023.
// switches from legacy linearcodec (CodecVersionV0/V1) to native ZAP.
// The switch governs the WRITE path: post-activation blocks MUST be
// ZAP-encoded. Validator coordination is documented in LP-023.
//
// Value: 0 — ZAP is mandatory from genesis. The legacy codec is only
// reachable via the LUXD_ENABLE_LEGACY_CODEC=1 dev knob for reading
// pre-2026-06-02 historical P-chain bytes. New deployments, fresh syncs,
// and all production binaries from v1.28.19 onward are ZAP-only by
// construction. The previous forward-dated 1782604800 (2026-07-01) value
// is dead — see LP-023 ZAP-native activation cutover (2026-06-02).
const ZAPActivationUnix uint64 = 0
// Value: 1766708400 = 2025-12-25T16:20:00 PST. Aligned with the network
// activation timestamp used by all other Quasar-Edition forks (Pulsar,
// Corona, Magnetar, Polaris, the 42 PQ precompiles, ML-DSA hybrid
// validator identity, BTC-style NodeID).
//
// Pre-activation reads are handled transparently by the zap_codec
// Unmarshal path's BE-fallback decoder — luxd binaries can mount and
// continue serving DBs written by pre-LP-023 versions (v1.28.x and
// earlier) without operator intervention. Writes from post-activation
// timestamps emit LE; reads accept either endianness.
const ZAPActivationUnix uint64 = 1766708400
// LegacyEnabled is true when the operator has set LUXD_ENABLE_LEGACY_CODEC=1.
//
// **Native ZAP is the default.** The codec.Codec{Marshal,Unmarshal} interface
// is gone from the platformvm hot path. New deployments, fresh syncs, and
// post-activation production binaries run ZAP-only by default — no legacy
// code in the read or write path.
// Default: true. Validators upgrading from v1.28.x or earlier hold P-chain
// state written with the pre-LP-023 BE codec version prefix. zap_codec
// reads handle BE transparently via fallback in Unmarshal, but the broader
// legacy linearcodec path remains reachable so historical tx bytes that
// did not migrate cleanly through the BE-fallback shim can still be
// decoded.
//
// Operators MUST set LUXD_ENABLE_LEGACY_CODEC=1 if they need to read
// pre-activation historical blocks from disk (e.g. validators bootstrapping
// from a long-history snapshot that includes pre-2026-07-01 P-chain state).
// Without the flag, legacy-encoded bytes return ErrLegacyCodecDisabled.
//
// Once a validator has synced past the activation height, the flag can be
// dropped — all subsequent reads are ZAP.
var LegacyEnabled = os.Getenv("LUXD_ENABLE_LEGACY_CODEC") == "1"
// Operators can explicitly disable via LUXD_ENABLE_LEGACY_CODEC=0 once
// their DB has been fully resynced from a post-activation snapshot.
var LegacyEnabled = os.Getenv("LUXD_ENABLE_LEGACY_CODEC") != "0"
// ErrLegacyCodecDisabled is returned by legacy-path parsers when the operator
// has not enabled legacy codec support via the LUXD_ENABLE_LEGACY_CODEC env var.