mirror of
https://github.com/luxfi/vm.git
synced 2026-10-30 15:13:40 +00:00
Wave 2E of codec final rip (#101). The package-local Codec singleton no longer wraps a reflectcodec-backed codec.Manager + linearcodec; it is a hand-rolled two-shape marshaller for the [2]ids.ID sharedID seed and the *dbElement value record stored under each shared-memory key. Public surface is preserved bit-for-bit: - Codec, CodecVersion, Manager interface - Marshal(version, source) ([]byte, error) - Unmarshal(bytes, dest) (uint16, error) Wire format for both supported shapes is byte-identical to the legacy reflectcodec output (verified out-of-band against a side-by-side linearcodec run; new codec_test.go pins fixtures). Existing on-disk shared-memory state and bootstrapped peers therefore remain readable. The codec is now closed by type: any source/dest outside the two registered shapes is rejected with ErrUnsupportedType at marshal / unmarshal time. Adding a new shape is a deliberate change — register a case in marshalValue/unmarshalValue. Max blob size dropped from math.MaxInt to a 64 MiB explicit cap; the two registered shapes are bounded well under that and the cap prevents pathological inputs from allocating up to memory exhaustion at unmarshal time. No consumer changes required: the singleton + interface preserve the exact call surface every internal call site (memory.go::sharedID, state.go::SetValue / RemoveValue / loadValue) already uses.
204 lines
6.3 KiB
Go
204 lines
6.3 KiB
Go
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package atomic
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/utils/wrappers"
|
|
)
|
|
|
|
// Wire format
|
|
//
|
|
// All atomic shared-memory wire blobs are length-prefixed-and-versioned:
|
|
//
|
|
// [u16 version][... type-specific big-endian fields ...]
|
|
//
|
|
// Version is currently 0. Only two payload shapes ever reach this codec
|
|
// — the cross-chain sharedID seed and the per-key dbElement record. The
|
|
// legacy linearcodec wire layout for these shapes is preserved exactly
|
|
// so existing on-disk shared-memory state and bootstrapped peers remain
|
|
// readable.
|
|
//
|
|
// Layout per source kind:
|
|
//
|
|
// [2]ids.ID (memory.go::sharedID)
|
|
// [32 id0][32 id1] // raw bytes, no length prefix
|
|
//
|
|
// *dbElement (state.go: SetValue/RemoveValue/loadValue)
|
|
// [u8 Present] // 0/1
|
|
// [u32 nValueBytes][Value bytes] // bytes slice
|
|
// [u32 nTraits] ([u32 nTraitBytes][trait bytes])* // traits slice-of-slice
|
|
//
|
|
// This is the minimum surface every consumer of vm/chains/atomic
|
|
// touches; no other shapes pass through Codec. Adding a new shape is a
|
|
// deliberate wire-format extension — register a new case in
|
|
// marshalValue/unmarshalValue and bump CodecVersion if the change is
|
|
// not backward compatible.
|
|
|
|
// CodecVersion is the current wire version. Increment on any incompatible
|
|
// schema change. There is no "v0 read fallback" — hard cut, one shape.
|
|
const CodecVersion = uint16(0)
|
|
|
|
// Manager is the surface every wire-touching peer of this package needs.
|
|
// It is the smallest method set that lets a caller serialize or parse
|
|
// an atomic shared-memory record — no reflection registry, no
|
|
// codec.Codec sub-interface, no legacy github.com/luxfi/codec import.
|
|
//
|
|
// The package singleton [Codec] implements [Manager].
|
|
type Manager interface {
|
|
Marshal(version uint16, source interface{}) ([]byte, error)
|
|
Unmarshal(bytes []byte, dest interface{}) (uint16, error)
|
|
}
|
|
|
|
// maxAtomicElementSize is the upper bound on a single Marshal/Unmarshal
|
|
// blob. The legacy singleton used math.MaxInt; the registered shapes
|
|
// (a 64-byte sharedID seed and a dbElement whose Value is bounded by
|
|
// the producing chain) never approach the chunk-size limits of any
|
|
// real DB backend, but we cap explicitly to refuse pathological inputs
|
|
// at unmarshal time instead of letting the underlying packer allocate
|
|
// up to memory exhaustion.
|
|
const maxAtomicElementSize = 64 * 1024 * 1024 // 64 MiB
|
|
|
|
// manager is the package-local marshal/unmarshal entry point.
|
|
type manager struct {
|
|
maxSize int
|
|
}
|
|
|
|
// Codec is the singleton atomic shared-memory codec. Consumers in this
|
|
// package (memory.go, state.go) call Codec.Marshal / Codec.Unmarshal.
|
|
var Codec Manager = &manager{maxSize: maxAtomicElementSize}
|
|
|
|
var (
|
|
ErrUnknownVersion = errors.New("unknown atomic wire version")
|
|
ErrCantUnpackVersion = errors.New("couldn't unpack atomic version")
|
|
ErrCantPackVersion = errors.New("couldn't pack atomic version")
|
|
ErrUnsupportedType = errors.New("unsupported atomic wire type")
|
|
ErrMaxSizeExceeded = errors.New("max atomic message size exceeded")
|
|
ErrExtraSpace = errors.New("trailing buffer space in atomic blob")
|
|
)
|
|
|
|
// Marshal serialises a source value with a u16 [version] prefix.
|
|
func (m *manager) Marshal(version uint16, source interface{}) ([]byte, error) {
|
|
if version != CodecVersion {
|
|
return nil, ErrCantPackVersion
|
|
}
|
|
p := &wrappers.Packer{MaxSize: m.maxSize}
|
|
p.PackShort(version)
|
|
if p.Errored() {
|
|
return nil, ErrCantPackVersion
|
|
}
|
|
if err := marshalValue(source, p); err != nil {
|
|
return nil, err
|
|
}
|
|
if p.Errored() {
|
|
return nil, p.Err
|
|
}
|
|
return p.Bytes, nil
|
|
}
|
|
|
|
// Unmarshal parses bytes into dest and returns the leading version.
|
|
func (m *manager) Unmarshal(bytes []byte, dest interface{}) (uint16, error) {
|
|
if len(bytes) < wrappers.ShortLen {
|
|
return 0, ErrCantUnpackVersion
|
|
}
|
|
if len(bytes) > m.maxSize {
|
|
return 0, ErrMaxSizeExceeded
|
|
}
|
|
p := &wrappers.Packer{Bytes: bytes, MaxSize: m.maxSize}
|
|
version := p.UnpackShort()
|
|
if p.Errored() {
|
|
return 0, ErrCantUnpackVersion
|
|
}
|
|
if version != CodecVersion {
|
|
return version, fmt.Errorf("%w: %d", ErrUnknownVersion, version)
|
|
}
|
|
if err := unmarshalValue(dest, p); err != nil {
|
|
return version, err
|
|
}
|
|
if p.Offset != len(bytes) {
|
|
return version, ErrExtraSpace
|
|
}
|
|
return version, nil
|
|
}
|
|
|
|
// marshalValue dispatches on the concrete shape of [source] and writes
|
|
// its body to [p].
|
|
//
|
|
// The supported shapes mirror the call sites in this package:
|
|
//
|
|
// - [2]ids.ID — sharedID seed emitted by memory.go::sharedID
|
|
// - *dbElement — value record stored under each shared-memory key
|
|
func marshalValue(source interface{}, p *wrappers.Packer) error {
|
|
switch v := source.(type) {
|
|
case [2]ids.ID:
|
|
marshalIDPair(v, p)
|
|
case *dbElement:
|
|
marshalDBElement(v, p)
|
|
default:
|
|
return fmt.Errorf("%w: %T", ErrUnsupportedType, source)
|
|
}
|
|
if p.Errored() {
|
|
return p.Err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func unmarshalValue(dest interface{}, p *wrappers.Packer) error {
|
|
switch v := dest.(type) {
|
|
case *dbElement:
|
|
return unmarshalDBElement(v, p)
|
|
case *[2]ids.ID:
|
|
return unmarshalIDPair(v, p)
|
|
default:
|
|
return fmt.Errorf("%w: %T", ErrUnsupportedType, dest)
|
|
}
|
|
}
|
|
|
|
// --- [2]ids.ID --------------------------------------------------------
|
|
|
|
func marshalIDPair(pair [2]ids.ID, p *wrappers.Packer) {
|
|
p.PackFixedBytes(pair[0][:])
|
|
p.PackFixedBytes(pair[1][:])
|
|
}
|
|
|
|
func unmarshalIDPair(pair *[2]ids.ID, p *wrappers.Packer) error {
|
|
copy(pair[0][:], p.UnpackFixedBytes(ids.IDLen))
|
|
copy(pair[1][:], p.UnpackFixedBytes(ids.IDLen))
|
|
if p.Errored() {
|
|
return p.Err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// --- *dbElement -------------------------------------------------------
|
|
|
|
func marshalDBElement(el *dbElement, p *wrappers.Packer) {
|
|
p.PackBool(el.Present)
|
|
p.PackBytes(el.Value)
|
|
p.PackInt(uint32(len(el.Traits)))
|
|
for _, trait := range el.Traits {
|
|
p.PackBytes(trait)
|
|
}
|
|
}
|
|
|
|
func unmarshalDBElement(el *dbElement, p *wrappers.Packer) error {
|
|
el.Present = p.UnpackBool()
|
|
el.Value = p.UnpackBytes()
|
|
nTraits := p.UnpackInt()
|
|
if p.Errored() {
|
|
return p.Err
|
|
}
|
|
el.Traits = make([][]byte, 0, nTraits)
|
|
for i := uint32(0); i < nTraits; i++ {
|
|
el.Traits = append(el.Traits, p.UnpackBytes())
|
|
}
|
|
if p.Errored() {
|
|
return p.Err
|
|
}
|
|
return nil
|
|
}
|