Cosmic Bull

Pure package on pearl-1

duebook

gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/duebook

pure packageprimitivescheduling

RenderedSourceCall builder

curated

Deferred-action scheduling. Claim is consume-then-act over never-reused IDs, so at most one Claim per ID can succeed across all transactions. No closure or capability crosses a realm boundary.

Identity

Import pathgno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/duebook
Kindpure package (/p/)
Chainpearl-1
Namespaceg1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3
Realm addressnone — A /p/ package is not a realm: it holds no state, custodies no coins and is never a transaction sender. The pkgPath derivation would still produce a value; recording one would name nothing.

Provenance

chain-attested
Deployed at height609,571
Deploy transaction5cd6de9e86a19cbf5633763dbf2536223a559212bdf51239304326e71f1b50bb look it up on the RPC
Deployerg1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3
Gas used28,679,403
Storage21,842 bytes, deposit 2184200ugnot
Files on chainduebook.gno gnomod.toml
Deployed bytesduebook.gno — 16,570 bytes
sha2565e82d0351dd7edf173b47d9aa3480f7cdf0cc476e8df81047de6c16ba226f05c

Do not take the hash above on trust. $download returns the bytes pearl-1 is actually running; this command fetches them and prints their digest, which should equal the one in the table:

curl -sS 'https://pearl.testnets.gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/duebook$download&file=duebook.gno' | shasum -a 256

Expected: 5e82d0351dd7edf173b47d9aa3480f7cdf0cc476e8df81047de6c16ba226f05c — 16,570 bytes. This was checked for all 21 packages while building this site's architecture record; every one matched. Use curl: pearl's edge answers Python's default user-agent with HTTP 403.

API

chain-derived 2 exported functions, 2 types, 20 methods.

This is a /p/ package: you import it, you do not call it in a transaction. gnoweb's $help shows only exported top-level functions, so the types and methods below do not appear there at all — which is why this reference exists.

Overview

Package duebook is a pure scheduling primitive for realms that need to authorize an action now and perform it later. It is to *time* what feeledger is to *money*: it holds no coins, performs no effects, imports no chain APIs, and owns no package-level state. The importing realm owns a *Book, supplies the clock, and performs its own effects.

The one idea

A realm does not ask duebook to execute anything — Gno has no autonomous execution, so "scheduled" always means "someone sends a transaction later". What a realm actually needs is for that later transaction to be authorized exactly once. Claim is that step:

d, err := book.Claim(id, now) if err != nil { panic(err) // not due, expired, cancelled, or already claimed } // ... the realm performs its own effect here, under its own authority

Claim checks due / not-expired / still-open and CONSUMES the deferral in the same call, before returning. The realm then acts. Replay is not guarded against, it is structurally impossible:

so at most one Claim per ID can ever succeed, across all transactions, forever. A second Claim of the same ID returns ErrNotFound whether the first one happened in this transaction or a year ago.

Because the realm performs the effect itself, no closure, callback, or capability ever crosses a realm boundary. duebook cannot be handed code to run, so it cannot be tricked into running the wrong code.

Lifecycle

A deferral is open from Schedule until exactly one of Claim, Cancel, or Expire consumes it. There is no other transition and no way back.

Schedule ──> open ──┬── Claim (now >= DueAt, before ExpiresAt) ├── Cancel (owner only, any time while open) └── Expire (anyone, once now >= ExpiresAt)

Claimability is the half-open interval [DueAt, ExpiresAt): due at DueAt, no longer claimable at ExpiresAt. A deferral scheduled with ttl == 0 never expires and has ExpiresAt == 0.

State growth

Consumed deferrals are removed, not archived — the ID counter, not a tombstone, is what prevents replay, so there is nothing to keep. Open deferrals are capped per Book at construction. Storage is therefore bounded by maxOpen regardless of how many deferrals have ever existed. Audit history belongs in the consuming realm's events.

Consumer contract (the parts the package cannot enforce)

  1. SUPPLY A REAL CLOCK. duebook cannot verify that `now` came from runtime.ChainHeight() or time.Now().Unix(). A realm that lets a caller choose `now` has no delay at all. Pass the chain's clock, never a transaction parameter. This is the single most important obligation and the one most commonly got wrong.
  2. USE ONE CLOCK CONSISTENTLY. Heights and seconds must not be mixed within a Book; delay, ttl and now are all in the caller's chosen unit.
  3. DO NOT EXPORT THE BOOK. A *Book is a mutable handle. Returning one across a realm boundary hands out the right to schedule, cancel and claim. Expose your own crossing functions instead; this package returns Deferral values, never pointers into its state.
  4. AUTHORIZE THE ACTOR. duebook authenticates nothing but ownership on Cancel. Who may Schedule, and who may Claim, are the realm's policy — derive the caller from cur.Previous().Address(), not from an argument.
  5. ACT AFTER A SUCCESSFUL CLAIM, IN THE SAME TRANSACTION. Claim's return value is the authorization. Storing it to act on later reintroduces the replay window this package exists to close.

All failures are returned as errors and leave the Book COMPLETELY UNCHANGED. Must* wrappers are the only functions here that panic.

The Book is address-agnostic: owners are non-empty strings. Realms normally use address.String().

Imports

Constants and variables

MaxOpenLimit is the largest maxOpen a Book may be constructed with. It bounds the worst-case cost of Due and IterateOpen, which scan the open set.

const MaxOpenLimit = 10000

MaxPayloadLen bounds a single deferral's payload. The payload is opaque to duebook — it exists so a realm can recover what it scheduled without keeping a parallel table.

const MaxPayloadLen = 4096

Errors returned by Book operations.

var (
	ErrEmptyOwner    = errors.New("duebook: empty owner key")
	ErrPayloadTooBig = errors.New("duebook: payload exceeds MaxPayloadLen")
	ErrInvalidNow    = errors.New("duebook: now must be non-negative")
	ErrInvalidDelay  = errors.New("duebook: delay outside [minDelay, maxDelay]")
	ErrInvalidTTL    = errors.New("duebook: ttl must be non-negative")
	ErrInvalidConfig = errors.New("duebook: invalid book configuration")
	ErrBookFull      = errors.New("duebook: open deferral cap reached")
	ErrNotFound      = errors.New("duebook: no such open deferral")
	ErrNotDue        = errors.New("duebook: not due yet")
	ErrExpired       = errors.New("duebook: deferral has expired")
	ErrNotExpired    = errors.New("duebook: deferral has not expired")
	ErrNotOwner      = errors.New("duebook: caller does not own this deferral")
	ErrOverflow      = errors.New("duebook: int64 overflow")
	ErrIDExhausted   = errors.New("duebook: identifier space exhausted")
)

Types

type Book

type Book struct {
	minDelay int64
	maxDelay int64
	maxOpen  int
	nextID   uint64    // never decreases; IDs are never reused
	open     *avl.Tree // padded id -> Deferral
}

Book holds the open deferrals of one consuming realm. The zero value is not usable; construct with New.

Book.Cancel

func (b *Book) Cancel(id uint64, owner string) (Deferral, error)

Cancel consumes an open deferral without performing it. Only its owner may cancel, and cancellation is permitted at any time while the deferral is open — including after it became due but before anyone claimed it.

Fails with ErrEmptyOwner, ErrNotFound, or ErrNotOwner. On error nothing is modified.

Book.Claim

func (b *Book) Claim(id uint64, now int64) (Deferral, error)

Claim consumes the deferral and returns it, authorizing the caller to perform the deferred action NOW, in this transaction.

It succeeds only while the deferral is open and now is in [DueAt, ExpiresAt). The deferral is removed BEFORE Claim returns, so a re-entrant or later Claim of the same ID finds nothing; combined with non-reused IDs, at most one Claim per ID ever succeeds.

Fails with ErrInvalidNow, ErrNotFound (never existed, or already consumed by Claim/Cancel/Expire), ErrNotDue, or ErrExpired. On error nothing is modified.

duebook does NOT check who is claiming: whether a deferral is permissionlessly claimable or restricted to its owner is the consuming realm's policy, applied before calling Claim.

Book.Due

func (b *Book) Due(now int64, limit int) []Deferral

Due returns up to limit open deferrals that are claimable at now, in ascending ID order (oldest first). A limit <= 0 returns nothing.

It scans the open set, so its cost is bounded by MaxOpen.

Book.Expirable

func (b *Book) Expirable(now int64, limit int) []Deferral

Expirable returns up to limit open deferrals that Expire would accept at now, in ascending ID order. A limit <= 0 returns nothing.

Book.Expire

func (b *Book) Expire(id uint64, now int64) (Deferral, error)

Expire consumes a deferral that is past its expiry, reclaiming its storage. It is deliberately permissionless: an expired deferral can never be claimed again, so letting anyone clear it keeps a Book from silting up with dead entries that block Schedule against maxOpen.

Fails with ErrInvalidNow, ErrNotFound, or ErrNotExpired (including for deferrals with no expiry, which never expire). On error nothing is modified.

Book.Get

func (b *Book) Get(id uint64) (Deferral, bool)

Get returns an open deferral by ID. The second result is false if the deferral never existed or has already been consumed — Get cannot tell those apart, by design: consumed deferrals leave no tombstone.

Get is a read-only preview and never authorizes anything. Only Claim's return value authorizes an action.

Book.IterateOpen

func (b *Book) IterateOpen(fn func(Deferral) bool)

IterateOpen calls fn for every open deferral in ascending ID order. Iteration stops early when fn returns true.

fn receives a COPY: Deferral is passed by value and holds only scalars and strings, so fn gets no pointer into the Book and cannot reach past it — Book's fields are all unexported.

Two rules for fn, and the second is the one that is easy to miss.

Book.MaxDelay

func (b *Book) MaxDelay() int64

MaxDelay returns the Book's scheduling ceiling.

Book.MaxOpen

func (b *Book) MaxOpen() int

MaxOpen returns the Book's cap on simultaneously open deferrals.

Book.MinDelay

func (b *Book) MinDelay() int64

MinDelay returns the Book's scheduling floor.

Book.MustCancel

func (b *Book) MustCancel(id uint64, owner string) Deferral

MustCancel is Cancel but panics on error.

Book.MustClaim

func (b *Book) MustClaim(id uint64, now int64) Deferral

MustClaim is Claim but panics on error.

Book.MustExpire

func (b *Book) MustExpire(id uint64, now int64) Deferral

MustExpire is Expire but panics on error.

Book.MustSchedule

func (b *Book) MustSchedule(owner, payload string, now, delay, ttl int64) uint64

MustSchedule is Schedule but panics on error.

Book.NextID

func (b *Book) NextID() uint64

NextID returns the identifier the next Schedule will allocate. It only ever increases, which is what makes a consumed ID unreusable.

Book.OpenCount

func (b *Book) OpenCount() int

OpenCount returns how many deferrals are currently open.

Book.Schedule

func (b *Book) Schedule(owner, payload string, now, delay, ttl int64) (uint64, error)

Schedule opens a deferral owned by owner, due at now+delay, expiring ttl after that (ttl == 0 means it never expires). It returns the new deferral's ID.

Fails with ErrEmptyOwner, ErrPayloadTooBig, ErrInvalidNow (now < 0), ErrInvalidDelay (delay outside [MinDelay, MaxDelay]), ErrInvalidTTL (ttl < 0), ErrBookFull, ErrOverflow, or ErrIDExhausted. On error nothing is modified.

type Deferral

type Deferral struct {
	ID        uint64
	Owner     string
	Payload   string
	CreatedAt int64
	DueAt     int64
	ExpiresAt int64
}

Deferral is a scheduled action. It is returned BY VALUE: holders cannot reach into a Book through it. Payload is opaque to this package.

ExpiresAt == 0 means the deferral never expires. Otherwise the deferral is claimable exactly on [DueAt, ExpiresAt).

Deferral.IsClaimable

func (d Deferral) IsClaimable(now int64) bool

IsClaimable reports whether Claim would succeed at now, assuming the deferral is still open.

Deferral.IsDue

func (d Deferral) IsDue(now int64) bool

IsDue reports whether the deferral has reached its due time at now.

Deferral.IsExpired

func (d Deferral) IsExpired(now int64) bool

IsExpired reports whether the deferral is past its expiry at now. A deferral with no expiry is never expired.

Functions

MustNew

func MustNew(minDelay, maxDelay int64, maxOpen int) *Book

MustNew is New but panics on error.

New

func New(minDelay, maxDelay int64, maxOpen int) (*Book, error)

New returns an empty Book.

minDelay is the floor on how far ahead a deferral may be scheduled; 0 permits same-instant scheduling. maxDelay is the ceiling, and doubles as the overflow guard on now+delay. maxOpen caps simultaneously open deferrals and must be in [1, MaxOpenLimit].

Units are the caller's choice — block heights or seconds — but must be used consistently for the life of the Book.


Doc text is reproduced as vm/qdoc returns it. The node markdown-escapes doc comments, so a bracket or angle bracket may carry a backslash the committed source does not have. The source itself is at source and in this repository.

Dependencies

chain-attested
Importserrors, gno.land/p/nt/avl/v0, strconv
First-party dependenciesnone
Used byduebook_demo

Known limitations

Recorded by the people who built and deployed it. This list is deliberately not empty where honesty costs something.

curated

Source and records

Source filepearl/p/duebook/duebook.gno at commit 6a510c665a53 in the project repository (not public — the digest command above is the check that needs no repository)
Matches the deployed bytesyes — byte-identical
Registered ingno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/service_registry as duebook (type library)
Recordscatalog/primitives.md#duebook
pearl/DEPLOYMENT.md