cborx

package
v0.5.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 17, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package cborx provides shared primitive-adapter codecs for the canonical deterministic CBOR (RFC 8949 §4.2) encoding used across the Yellow Network protocol.

This is the foundation package for the canonical CBOR rules in docs/specs/cbor.md and ADR-009 (docs/decisions/009-cbor-encoding.md §3 — the adapter table, §5 — the version byte envelope).

Scope

cborx does NOT generate per-type struct codecs. It only wraps whyrusleeping/cbor-gen primitives with adapters so that generated codecs (owned by later waves under each package's gen/ subfolder) can delegate the encoding of complex primitive types — big integers, fixed-point decimals, fixed-length hashes and addresses, and timestamps — to one canonical implementation that satisfies RFC 8949 §4.2 deterministic-encoding rules.

Adapter set (ADR-009 §3)

  • BigInt — *big.Int as RFC 8949 tag 2 (unsigned bignum) or tag 3 (negative bignum); zero is tag 2 with empty byte string; nil is invalid inside a plain BigInt.
  • MaybeBigInt — nilable *big.Int; nil encodes as CBOR null.
  • Decimal — internal/decimal.Decimal as RFC 8949 tag 4 (decimal fraction) = [exponent int32, mantissa bigint].
  • Hash32 — [32]byte as major type 2, definite length 32.
  • Addr20 — common.Address / [20]byte as major type 2, definite length 20.
  • Time — time.Time as major type 0/1, Unix nanoseconds int64.

Envelope (ADR-009 §5)

Every CBOR frame on the wire or in a BLOB is prefixed with one byte (the global schema-family version). WriteEnvelope / ReadEnvelope handle the prefix around a cbor-gen CBORMarshaler / CBORUnmarshaler body. V1 (0x01) is the only version emitted by this migration; ReadEnvelope rejects 0x00 (reserved) and anything above V1 with a typed ErrUnsupportedVersion so callers can distinguish framing errors from payload errors.

Determinism (RFC 8949 §4.2) RFC 8949 §4.2)" aria-label="Go to Determinism (RFC 8949 §4.2)">¶

Adapters enforce — and tests prove — the deterministic-encoding rules in docs/specs/cbor.md §2:

  1. Integers in shortest form (no over-wide length encoding).
  2. Definite-length containers only; indefinite-length input is rejected.
  3. Map keys sorted lexicographically by their CBOR-encoded bytes (RFC 8949 §4.2.1). Adapters themselves hold no maps; the envelope helpers preserve sort order produced by cbor-gen.
  4. No floating-point types at all. NaN, infinities, and negative zero are never encoded and are rejected on decode.
  5. Tagged integers only where declared (tag 2/3 for bignums, tag 4 for decimals).

cbor-gen mode

ADR-009 §2 mandates **struct-as-array (positional, no keys on wire)** for every generated type in the network. Wave 2's per-package gen/main.go must invoke cbor-gen with the tuple-encoding entry point (typegen.WriteTupleEncodersToFile) rather than the map entry point. Fields in generated structs may only be appended at the end; insert, reorder, rename, or remove requires a version-byte bump and a re-seed of every CBOR-encoded BLOB (ADR-009 §5).

Imports

This package imports whyrusleeping/cbor-gen and fxamacker/cbor/v2. internal/ is stdlib-only under the repo's architecture rules; the relevant import lines carry the `layer-guard: allow` escape hatch because ADR-009 and the migration plan explicitly sanction cborx as the single package that wraps these libraries.

Index

Constants

View Source
const (
	// MaxBulkFrame bounds a single length-prefixed frame for
	// block / log backfill streams (1 MB).
	MaxBulkFrame uint64 = 1 << 20

	// MaxControlFrame bounds a single length-prefixed frame for every
	// non-bulk libp2p stream, the clearnet TCP listener, and every
	// gossipsub message (64 KB).
	MaxControlFrame uint64 = 64 << 10
)

Frame size caps per docs/specs/cbor.md §5.2 / ADR-009 §8. Callers choose which cap applies to their stream: backfill (blocks/logs) is bulk, every other libp2p stream, the clearnet TCP listener, and every gossipsub message is control.

View Source
const (
	Hash32Len = 32
	Addr20Len = 20
)

Hash32Len / Addr20Len fix the declared definite lengths for the hash / address adapters. The length is part of the encoding contract, not a bound — a decoder that reads a different length rejects.

Variables

View Source
var ErrEmptyEnvelope = errors.New("cborx: empty envelope")

ErrEmptyEnvelope reports that the version byte could not be read.

View Source
var ErrEnvelopeTrailingBytes = errors.New("cborx: trailing bytes after envelope body")

ErrEnvelopeTrailingBytes reports an envelope decoded from a bounded reader (byte slice or io.LimitReader) that contains bytes after the canonical body. Canonical CBOR (ADR-009 §1, RFC 8949 §4.2) requires exactly one logical value per encoded byte string; trailing bytes would admit multiple wire encodings for the same logical value.

View Source
var ErrFrameTooLarge = errors.New("cborx: frame exceeds size cap")

ErrFrameTooLarge reports a declared frame length above the caller's cap. Callers should close the stream when this is returned — the remote is either mis-framed or attempting a DoS.

View Source
var ErrFrameTrailingBytes = errors.New("cborx: trailing bytes in frame")

ErrFrameTrailingBytes reports a frame whose declared length contains bytes after the envelope body has decoded. ADR-009 requires strict decode at the frame boundary rather than silently accepting schema drift or concatenation.

View Source
var ErrReservedVersion = errors.New("cborx: reserved version byte 0x00")

ErrReservedVersion reports a 0x00 version byte on decode.

View Source
var ErrUnsupportedVersion = errors.New("cborx: unsupported schema version")

ErrUnsupportedVersion reports a version byte above V1 (or otherwise unknown to this binary). Callers can distinguish version-envelope errors from payload errors by unwrapping against this sentinel.

Functions

func ReadEnvelope

func ReadEnvelope(r io.Reader, v *Version, body cbg.CBORUnmarshaler) error

ReadEnvelope reads the one-byte schema-family version, rejects reserved / unsupported values with a typed error, and decodes the remainder of the stream into body via body.UnmarshalCBOR.

On return:

  • *v is set to the observed version byte on success, or the rejected byte when the error is one of ErrReservedVersion or ErrUnsupportedVersion (so callers can log the raw byte).
  • A body-level decode failure is returned unwrapped from either version sentinel, which lets callers distinguish framing errors from payload errors via errors.Is.

func ReadEnvelopeStrict

func ReadEnvelopeStrict(r io.Reader, v *Version, body cbg.CBORUnmarshaler) error

ReadEnvelopeStrict reads an envelope and additionally requires that the underlying reader is exhausted after the body decodes. Use this at every byte-slice and bounded-stream (io.LimitReader) boundary to reject non-canonical inputs that would otherwise round-trip to a shorter canonical encoding.

Mirrors the trailing-byte check in ReadFrame (see frame.go). Stream- based callers that intentionally pipeline multiple envelopes on one connection should keep using ReadEnvelope; ReadEnvelopeStrict is for the canonical-single-value boundary.

func ReadFrame

func ReadFrame(r io.Reader, max uint64, v *Version, body cbg.CBORUnmarshaler) error

ReadFrame reads a length-prefixed envelope produced by WriteFrame. max is the largest body length this call will tolerate before returning ErrFrameTooLarge; callers pass MaxBulkFrame or MaxControlFrame depending on the stream.

On success *v carries the observed version byte and body is filled via UnmarshalCBOR. A clean EOF before any bytes were read is reported as io.EOF (not wrapped) so callers can terminate backfill loops without special-casing ErrEmptyEnvelope.

func UnmarshalExact

func UnmarshalExact(data []byte, body cbg.CBORUnmarshaler) error

UnmarshalExact decodes a single canonical CBOR value from data into body and rejects any trailing bytes. Use for raw-CBOR boundaries that do not carry a version envelope (e.g. BlockEntry.Payload).

func WriteEnvelope

func WriteEnvelope(w io.Writer, v Version, body cbg.CBORMarshaler) error

WriteEnvelope writes the one-byte schema-family version followed by the canonical CBOR body produced by body.MarshalCBOR. It is the sole sanctioned way to stamp a frame before it hits a stream or a BLOB (ADR-009 §5).

v MUST be V1 for this migration; any other value returns a wrapped ErrUnsupportedVersion or ErrReservedVersion so callers cannot accidentally write a frame with a version this build doesn't know how to read back.

func WriteFrame

func WriteFrame(w io.Writer, v Version, body cbg.CBORMarshaler) error

WriteFrame writes a length-prefixed envelope to w:

[uvarint body-length][1 byte version][CBOR body]

where body-length covers the version byte + CBOR body (i.e. the envelope that WriteEnvelope would produce). Used on streams that carry more than one frame per connection: the clearnet TCP listener (docs/specs/cbor.md §5.2), libp2p backfill protocols, and anywhere a caller wants explicit per-frame bounds before allocation.

Single-message libp2p request/response streams use this framing so receivers get an explicit message boundary without waiting for stream EOF.

Types

type Addr20

type Addr20 struct {
	V [Addr20Len]byte
}

Addr20 wraps a 20-byte Ethereum-style address as a bare byte array so internal/cborx can stay free of outer-layer dependencies. Wave 2 generated codecs pass a go-ethereum common.Address in via an explicit [20]byte conversion (Address is a named [20]byte type).

Encoded as CBOR major type 2 with a definite length of 20. Round-trips bit-identically to the ABI encoding of a bare 20-byte address slice so later boundaries (e.g. custody/evm/) can splice the bytes without extra conversion.

func (Addr20) MarshalCBOR

func (a Addr20) MarshalCBOR(w io.Writer) error

MarshalCBOR writes the 20-byte byte string.

func (*Addr20) UnmarshalCBOR

func (a *Addr20) UnmarshalCBOR(r io.Reader) error

UnmarshalCBOR reads exactly 20 bytes into a.V; any other length is a hard reject.

type BigInt

type BigInt struct {
	// V is the wrapped integer. Callers pass and read the pointer; the
	// adapter never mutates it in-place when marshaling.
	V *big.Int
}

BigInt is a CBOR adapter for math/big.Int.

Encoding follows RFC 8949 §3.4.3:

  • Non-negative n is written as CBOR tag 2 wrapping a byte-string of big-endian magnitude bytes, with leading zero bytes trimmed.
  • Negative n is written as CBOR tag 3 wrapping the big-endian magnitude of (-1 - n) with leading zeros trimmed.
  • Zero is the single canonical form "tag 2 + empty byte string" (length = 0). This keeps zero unambiguous: the negative-bignum form of zero (-1 - 0 = -1, magnitude 1, byte 0x01) is structurally a different encoding of -1, not of 0, and is rejected as malformed when a byte-string of length 0 appears under tag 3.
  • A nil *big.Int is illegal inside a BigInt — use MaybeBigInt when absence is a legitimate state.

The adapter rejects nil on marshal; use Value only when callers need a read-side zero default for an absent pointer.

func (BigInt) MarshalCBOR

func (b BigInt) MarshalCBOR(w io.Writer) error

MarshalCBOR writes the RFC 8949 tag-2 / tag-3 bignum encoding.

A nil inner value is rejected; callers must wrap a nilable amount in MaybeBigInt (which encodes nil as CBOR null).

func (*BigInt) UnmarshalCBOR

func (b *BigInt) UnmarshalCBOR(r io.Reader) error

UnmarshalCBOR decodes a tag-2 / tag-3 bignum into b.V, enforcing the canonical-zero and canonical-leading-byte rules of RFC 8949 §4.2.

func (BigInt) Value

func (b BigInt) Value() *big.Int

Value returns a non-nil *big.Int (zero if b.V was nil).

type Decimal

type Decimal struct {
	V decimal.Decimal
}

Decimal is a CBOR adapter for internal/decimal.Decimal (the project's vendored fixed-point decimal; API: Exponent() int32, Coefficient() *big.Int, NewFromBigInt).

Encoding follows RFC 8949 §3.4.4:

  • major type 6 (tag) with value 4,
  • wrapping a definite-length array of exactly two elements,
  • element 0: exponent, written as the shortest-form MajUnsignedInt / MajNegativeInt (docs/specs/cbor.md §2 disallows wider encodings),
  • element 1: mantissa, written as a BigInt (RFC 8949 tag 2/3) so the sign is carried without an extra prefix.

internal/decimal exposes only int32 exponents, so the exponent fits comfortably into CBOR's native integer range and no bignum is ever needed for it.

func (Decimal) MarshalCBOR

func (d Decimal) MarshalCBOR(w io.Writer) error

MarshalCBOR writes the tag-4 two-element array.

func (*Decimal) UnmarshalCBOR

func (d *Decimal) UnmarshalCBOR(r io.Reader) error

UnmarshalCBOR decodes the tag-4 two-element array.

type Hash32

type Hash32 struct {
	V [Hash32Len]byte
}

Hash32 wraps a 32-byte digest (keccak256 output, block hash, entry hash, state root, etc.). Encoded as CBOR major type 2 with a definite length of 32.

func (Hash32) MarshalCBOR

func (h Hash32) MarshalCBOR(w io.Writer) error

MarshalCBOR writes the 32-byte byte string with the canonical shortest-form length (a single byte in this case).

func (*Hash32) UnmarshalCBOR

func (h *Hash32) UnmarshalCBOR(r io.Reader) error

UnmarshalCBOR reads exactly 32 bytes into h.V; any other length is a hard reject.

type MaybeBigInt

type MaybeBigInt struct {
	V *big.Int
}

MaybeBigInt wraps a nilable *big.Int. CBOR null decodes to a MaybeBigInt with V == nil; any other input is delegated to BigInt. Useful for optional-amount fields (e.g. a MinAmountOut that may be absent rather than zero).

func (MaybeBigInt) MarshalCBOR

func (m MaybeBigInt) MarshalCBOR(w io.Writer) error

MarshalCBOR writes CBOR null when V == nil, otherwise the BigInt form.

func (*MaybeBigInt) UnmarshalCBOR

func (m *MaybeBigInt) UnmarshalCBOR(r io.Reader) error

UnmarshalCBOR peeks the first byte to distinguish CBOR null from a tagged bignum. The cbg.CborReader wrapper provides the unread capability we need regardless of the underlying reader type.

type Time

type Time struct {
	V time.Time
}

Time is a CBOR adapter for time.Time. Encoding is Unix nanoseconds as a signed CBOR integer:

  • positive (or zero) → major type 0 with shortest-form width.
  • negative (pre-epoch) → major type 1 with shortest-form width.

This is the same encoding used by cbor-gen's CborTime helper, chosen over RFC 8949 tag 0/1 because:

  • Our timestamps are already Unix-normalized (no sub-second-range tricks, no text form).
  • Positional struct-as-array codegen is friendlier to bare integers than to tagged wrappers.
  • int64 nanoseconds give a range of ±292 years around 1970 (roughly 1678-09-21..2262-04-11), which comfortably covers every network timestamp. Times outside that range fail closed rather than relying on time.Time.UnixNano(), which silently overflows.

func (Time) MarshalCBOR

func (t Time) MarshalCBOR(w io.Writer) error

MarshalCBOR writes the Unix-nanosecond integer.

func (*Time) UnmarshalCBOR

func (t *Time) UnmarshalCBOR(r io.Reader) error

UnmarshalCBOR decodes a shortest-form signed int as Unix nanoseconds. A major-type mismatch (e.g. float, tagged value, byte string) is a hard reject — ADR-009 §3 forbids float-encoded timestamps anywhere in the protocol.

type Version

type Version uint8

Version is the one-byte schema-family prefix carried by every CBOR frame on the wire or in a BLOB. ADR-009 §5.

Allocation:

0x00         reserved — never written; rejected on decode.
0x01         V1 — the initial CBOR migration shipped by this repo.
0x02..0x7F   reserved for future CBOR schema-family versions.
0x80..0xFF   reserved for future non-CBOR codecs (e.g. a later
             migration away from CBOR).

A wire-incompatible change bumps the byte globally; there is no per-codec version and no backwards-compatible reader.

const (
	// V1 is the CBOR schema-family version shipped by ADR-009.
	V1 Version = 0x01

	// VersionReserved is the guard value; readers reject it explicitly
	// so a zero-filled buffer can never be mistaken for a valid frame.
	VersionReserved Version = 0x00
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL