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:
- Integers in shortest form (no over-wide length encoding).
- Definite-length containers only; indefinite-length input is rejected.
- 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.
- No floating-point types at all. NaN, infinities, and negative zero are never encoded and are rejected on decode.
- 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
- Variables
- func ReadEnvelope(r io.Reader, v *Version, body cbg.CBORUnmarshaler) error
- func ReadEnvelopeStrict(r io.Reader, v *Version, body cbg.CBORUnmarshaler) error
- func ReadFrame(r io.Reader, max uint64, v *Version, body cbg.CBORUnmarshaler) error
- func UnmarshalExact(data []byte, body cbg.CBORUnmarshaler) error
- func WriteEnvelope(w io.Writer, v Version, body cbg.CBORMarshaler) error
- func WriteFrame(w io.Writer, v Version, body cbg.CBORMarshaler) error
- type Addr20
- type BigInt
- type Decimal
- type Hash32
- type MaybeBigInt
- type Time
- type Version
Constants ¶
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.
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 ¶
var ErrEmptyEnvelope = errors.New("cborx: empty envelope")
ErrEmptyEnvelope reports that the version byte could not be read.
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.
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.
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.
var ErrReservedVersion = errors.New("cborx: reserved version byte 0x00")
ErrReservedVersion reports a 0x00 version byte on decode.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
MarshalCBOR writes the 20-byte byte string.
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 ¶
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 ¶
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.
type 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 ¶
MarshalCBOR writes the tag-4 two-element array.
type Hash32 ¶
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 ¶
MarshalCBOR writes the 32-byte byte string with the canonical shortest-form length (a single byte in this case).
type MaybeBigInt ¶
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 ¶
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 ¶
MarshalCBOR writes the Unix-nanosecond integer.
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.