/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/x/daily/varint/v0
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
-1directly would sign-extend it to ten bytes, which is worse than not encoding it at all. Zigzag maps0, -1, 1, -2, 2onto0, 1, 2, 3, 4, so magnitude decides the cost and the sign does not.ZigZagandUnZigZagare exported for callers packing their own frames. Uvarintreads one value and reports how many bytes it took. It never looks past the value it decoded, so a stream is read by reslicing.Uvarintsis the all-at-once form, and it refuses a trailing fragment withErrTrailingrather 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) []byteis Go'sencoding/binaryshape, 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.
ErrTruncatedmeans more input would have helped,ErrOverflowmeans the value cannot fit in 64 bits,ErrNotCanonicalmeans 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.