Pure package on pearl-1
duebook
gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/duebook
pure packageprimitivescheduling
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 path | gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/duebook |
|---|---|
| Kind | pure package (/p/) |
| Chain | pearl-1 |
| Namespace | g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3 |
| Realm address | none — 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 height | 609,571 |
|---|---|
| Deploy transaction | 5cd6de9e86a19cbf5633763dbf2536223a559212bdf51239304326e71f1b50bb look it up on the RPC |
| Deployer | g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3 |
| Gas used | 28,679,403 |
| Storage | 21,842 bytes, deposit 2184200ugnot |
| Files on chain | duebook.gno gnomod.toml |
| Deployed bytes | duebook.gno — 16,570 bytes |
| sha256 | 5e82d0351dd7edf173b47d9aa3480f7cdf0cc476e8df81047de6c16ba226f05c |
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 256Expected: 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:
- IDs are allocated monotonically from an internal counter and are NEVER reused, for the lifetime of the Book;
- a successful Claim removes the deferral before returning;
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)
- 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.
- 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.
- 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.
- 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.
- 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
errorsgno.land/p/nt/avl/v0strconv
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.
- Do not Schedule, Claim, Cancel or Expire from inside fn: mutating the tree while iterating it is undefined. Collect IDs first, then act after IterateOpen returns.
- fn runs under the CALLING REALM'S STORAGE AUTHORITY. This method's receiver is stamped with the importing realm's PkgID, so the borrow rules leave the realm context set to that realm for the whole callback, and a top-level fn has no receiver and no declaring realm to anchor it elsewhere. A callback that re-enters the calling realm's own mutators therefore does so with that realm's authority. Never pass a caller-supplied function here from inside a permission-gated path; pass only a closure this package's consumer wrote itself.
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| Imports | errors, gno.land/p/nt/avl/v0, strconv |
|---|---|
| First-party dependencies | none |
| Used by | duebook_demo |
Known limitations
Recorded by the people who built and deployed it. This list is deliberately not empty where honesty costs something.
curated- One consumer on chain (duebook_demo); its reusability is a design argument, not a demonstrated fact.
Source and records
| Source file | pearl/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 bytes | yes — byte-identical |
| Registered in | gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/service_registry as duebook (type library) |
| Records | catalog/primitives.md#duebookpearl/DEPLOYMENT.md |