Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/x/daily/varint/v0

Directory · 5 Files
README.md Open

gno.land/p/moul/x/daily/varint/v0

LEB128 / protobuf-style variable-length integers over strings, canonical form only: PutUvarint, Uvarint, PutVarint, Varint, ZigZag, Len.

1import "gno.land/p/moul/x/daily/varint/v0"
2
3varint.PutUvarint(300)       // → "\xac\x02", two bytes instead of eight
4varint.PutVarint(-1)         // → "\x01", one byte instead of ten
5varint.Len(16384)            // → 3, without encoding anything
6varint.Uvarint("\xac\x02")   // → 300, 2, nil

A varint stores an integer in base 128, least significant group first, seven payload bits per byte and a continuation bit in the high position. Small numbers get small, which is the whole point: a counter under 128 costs one byte where a fixed uint64 costs eight, and on chain every byte of a persisted key or packed record is paid for once at write and again at every read.

Only the canonical encoding decodes, deliberately. Nothing in the base-128 scheme stops you padding a value with extra continuation groups: 80 00 and 00 both say zero. Protobuf accepts both. A realm that stores or hashes an encoded value cannot, because two spellings of one number are two different keys, two different hashes, and a cheap way to make the same state look like two states to anything comparing bytes. Uvarint returns ErrNotCanonical rather than quietly normalizing, so the caller finds out at the boundary instead of at the comparison.

Three behaviours worth knowing before you use it:

  • Signed values go through zigzag, not two's complement. Encoding -1 directly would sign-extend it to ten bytes, which is worse than not encoding it at all. Zigzag maps 0, -1, 1, -2, 2 onto 0, 1, 2, 3, 4, so magnitude decides the cost and the sign does not. ZigZag and UnZigZag are exported for callers packing their own frames.
  • Uvarint reads one value and reports how many bytes it took. It never looks past the value it decoded, so a stream is read by reslicing. Uvarints is the all-at-once form, and it refuses a trailing fragment with ErrTrailing rather than returning a silently short list: a partial value at the end is corruption, not data.
  • Encoding appends to a byte slice, decoding reads a string. AppendUvarint(dst []byte, x uint64) []byte is Go's encoding/binary shape, and it is a slice rather than a string because a string accumulator copies the whole prefix on every append, which makes packing a sequence quadratic in the encoded size. Decoding takes a string because that is what a realm has stored.
  • The errors are distinct for a reason. ErrTruncated means more input would have helped, ErrOverflow means the value cannot fit in 64 bits, ErrNotCanonical means the bytes decode fine but should never have been written. A caller validating untrusted input wants to tell the third from the first two.

Encoding is O(bytes out), at most ten iterations, and Len answers the size question without allocating. There is no streaming writer: the unit is a value, and a realm packing many of them calls AppendUvarint into one buffer in a loop, which is what PutUvarints does.

Live demo: r/moul/x/daily/varintdemo.


Part of moul/gno-contracts — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.

🧪 Highly experimental — potentially vibe-coded. Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: DISCLAIMER.