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

v0 source pure

Package checkpoint remembers what a number used to be.

Overview

Package checkpoint remembers what a number used to be.

A Series is one value's history. It answers what that value held during a past epoch, which is the question governance asks: voting weight has to be read as of the moment a question was put, or it can be borrowed for the length of one transaction and voted twice.

It was written for a token's balances and its total supply, and there is nothing about it that knows what a token is. Anything a realm wants to look back at — a fee, a quorum, a member count — is a Series.

What it costs

One fact about gno shapes the whole thing: storage deposit is charged per OBJECT, on the DELTA of its encoded size (gnovm/pkg/gnolang/store.go, LastObjectSize, at 100ugnot/byte). A field added to a record that was going to be written anyway is paid for once, at creation, and costs nothing on every write after. On the EVM every extra word touched is a fresh SSTORE forever, which is the entire reason OpenZeppelin's ERC20Votes appends to an array per block — and the reason that shape does not have to be inherited.

So: two points inline, the rest paged into an archive. The overwhelmingly common query is answered without touching the archive at all, and a value that changes several times within one epoch coalesces into a single update of a record already being written.

Epochs, not blocks

The caller supplies the epoch and this package never asks what one is. That is deliberate — the clock is policy. Quantising to something coarser than a block is what makes the cost work, because a checkpoint per block maximises the one thing gno charges dearly for, a brand new key.

It also turns the anti-flash-loan property from a convention into an invariant, if the caller refuses to answer for the current epoch: a transaction cannot outlive its block and a block cannot outlive its epoch, so a borrowed balance cannot be voted and returned. This package cannot enforce that, because it does not know what time it is. It is the caller's half of the bargain and worth writing down where the caller will read it.

Constants 3

const MaxKey

1const MaxKey = 128
source

MaxKey bounds a key.

A key is not stored once. It is the prefix of every page key this series ever writes, so its length multiplies by the number of pages a long-lived value accumulates — and the pages are what the design buys with the deposit it saves elsewhere.

Exported so a caller can check before calling rather than discover by panicking. A hundred and twenty-eight is room for a bech32 address twice over with a separator between, which is the largest sensible key the realm this came out of could form.

const PageEpochs

1const PageEpochs = uint32(32)
source

PageEpochs is how many epochs one archive page covers.

It bounds the archive in both directions, and both matter.

Downwards: one new key buys this many checkpoints instead of one, which is what keeps the marginal cost of history near zero.

Upwards: a page cannot grow past one bucket. Keeping every checkpoint for a key in a single page would be CHEAPER in keys — one per key, forever — and that is exactly its problem. Appending stays cheap, since the deposit is charged on the size delta, but the whole object is deserialised to answer anything about it. A key with years behind it would pay to load all of them to answer about one epoch.

const Sep

1const Sep = "\x00"
source

Sep separates a caller's key from this package's own suffix.

Exported because a caller sharing one Archive between several keyspaces usually wants the same byte for its own compound keys, and two constants with the same value in different files eventually disagree.

A key may not contain it. That is checked rather than documented, because the failure is silent: one key's range scan would reach into another's and return a plausible number belonging to somebody else.

Functions 1

func NewArchive

1func NewArchive() *Archive
source

NewArchive returns an empty archive.

Types 2

type Archive

struct
1type Archive struct {
2	t *bptree.BPTree
3}
source

Archive is the shared store the older points live in.

One Archive can hold any number of Series, told apart by key. Bounding each scan to its own key's prefix is what keeps them apart, and it is the reason a key may not contain Sep.

Methods on Archive

func Size

method on Archive
1func (a *Archive) Size() int
source

Size is the number of pages stored, across every key.

Exported for the caller that wants to assert what its own history costs — the count is the number of KEYS bought, which is the expensive quantity.

func Trim

method on Archive
1func (a *Archive) Trim(key string, keepFrom uint32, maxPages int) int
source

Trim deletes archive pages of key that lie entirely below keepFrom — except the newest such page — oldest-first, at most maxPages per call. It returns how many pages were removed.

The exception is the floor-preservation invariant: a value set below the horizon and unchanged since is still the correct answer for epochs INSIDE the kept window, and its floor entry lives in the newest all-below page. Sparing that one page keeps ValueAt exact at and above keepFrom through every intermediate state of a bounded, multi-call trim. A page straddling the horizon is never a candidate (it is not entirely below), so up to PageEpochs-1 epochs of over-retention are kept deliberately.

Deletion frees the page object's bytes; on chains with storage deposits the locked amount is released by the same mechanism that charged it. Trim never touches the two inline points — recent reads stay exact by construction.

The walk is collect-then-remove because the tree forbids mutation from an iteration callback, and it is bounded to maxPages+1 keys of collection, so a call's cost is a constant of the caller's choosing.

func WalkDesc

method on Archive
1func (a *Archive) WalkDesc(key string, fn func(e uint32, v int64) bool)
source

WalkDesc visits key's ARCHIVED checkpoints newest-first, stopping when fn returns true. The two inline points are the Series' own fields and are not visited — a caller assembling a history appends them itself (they are always newer than anything archived, because points only ever roll OUT of the inline slots). Bounded by the key's page prefix like every other walk here.

type Series

struct
1type Series struct {
2	cur  int64
3	prev int64
4	e0   uint32
5	e1   uint32
6}
source

Series is one value's history: the two most recent points inline, everything older in the archive.

Example
1at >= e0        -> cur
2e1 <= at < e0   -> prev
3at < e1         -> the archive

e1 == 0 means the second slot was never used, and prev is then zero, which is the correct answer for every epoch below e0 — so a key that has held a steady value since it was created never touches the archive at all. That case is the overwhelming majority of queries.

That sentinel is why epochs are 1-BASED and SetAt refuses zero. Zero has to mean "before this series existed" and cannot also be an epoch somebody wrote in, or the two readings collide: a value written at epoch 0 leaves e1 == 0 after the next change, the roll after that reads it as an empty slot, and the point is dropped instead of archived. Silently — the archive stays empty and every query below the newest two points answers zero.

The realm this came out of never could have hit it, because its clock is 1-based for exactly this reason. The assumption did not travel with the code, which is the whole hazard of moving code into a package.

Hold it as a VALUE field in whatever record the caller already persists. Mutating it then dirties only that record. A pointer would make it a second object, which is a second key, which is the cost this design exists to avoid.

Methods on Series

func Prev

method on Series
1func (s *Series) Prev() (uint32, int64)
source

Prev is the older of the two inline points: its epoch and value. A zero epoch means that slot was never used (the series has at most one point). Callers assembling a full history read the archive, then Prev, then Since/Value — ascending by construction, since points only roll outward.

func SetAt

method on Series
1func (s *Series) SetAt(a *Archive, key string, e uint32, v int64)
source

SetAt records v from epoch e onward, rolling whatever falls out of the two inline slots into the archive.

e must be at least 1, and must not be below the newest epoch already recorded. Callers pass a chain height quantised to an epoch, which only ever increases.

func Since

method on Series
1func (s *Series) Since() uint32
source

Since is the epoch the current value has held from.

Useful for asserting that something did NOT write — a caller that expects an operation to be free can check this did not move.

func Value

method on Series
1func (s *Series) Value() int64
source

Value is what the series holds now, without consulting anything.

func ValueAt

method on Series
1func (s *Series) ValueAt(a *Archive, key string, at uint32) int64
source

ValueAt answers what the series held during epoch at.

Zero for an epoch before the series began, which is exact rather than a miss: the key held nothing then.

Imports 2

Source Files 2