Numbers in a bytes-only store

Storage holds bytes, but contracts count things. The conversion pattern below is used by every Animica contract, including the token standard.

Storage holds bytes. Contracts count balances, supplies and votes. So every Animica contract contains some version of these two helpers:

def _uget(key: bytes) -> int:
    raw = storage.get(key, b"")
    if raw == b"":
        return 0
    return int.from_bytes(raw, "big")

def _uset(key: bytes, value: int) -> None:
    if value == 0:
        storage.delete(key)
        return
    storage.set(key, value.to_bytes(max(1, (value.bit_length() + 7) // 8), "big"))

Read it once and it stops being mysterious:

  • int.from_bytes(raw, "big") — big-endian, the network byte order used everywhere in Animica.
  • (bit_length() + 7) // 8 — the minimum number of bytes that can hold the value. max(1, ...) because a zero-bit number still needs one byte.
  • Zero deletes the key. Storing b"\x00" and storing nothing would be two different byte strings meaning the same number, and two encodings of one value is how consensus bugs start. Deleting also refunds storage.

_uget returning 0 for a missing key is what lets a token contract say balance_of(anyone) without pre-creating an entry for every address on the chain. It is not an optimisation; it is the design.

Your turn

Implement a counter using the integer pattern. add(n) adds n and returns the new total; total() reads it. Storing zero must delete the key — check with is_empty().

Hints

Stuck? Ask

Answered by Animica's own free inference network. It is donated GPU capacity, so give it 20-30 seconds.