Discovery — duebook
Mandatory discovery gate (docs/DISCOVERY_AND_REUSE.md) for the application-factory objective:
Create a reusable on-chain capability for Gno applications that need to schedule actions for execution at a future point, while enforcing deterministic execution rules, preventing replay, and allowing defined cancellation or expiration. The capability should be useful to multiple independent applications rather than being designed around a single application.
Run 2026-09-21, before any code was written. This document records what was searched, what was found, how each hit was classified, and what was decided.
The headline finding is stated first, because it is the finding that determines what gets built — and in this case it determines that Cosmic Bull's own closest existing work is not reusable for the objective.
1. Headline finding
Every scheduling implementation found either performs no effect, or performs one only as the tail of a governance vote. Nothing found gives a consuming realm a reusable, consumer-owned deferral object that the consumer can act on under its own authorization.
The gap is specific, and it is not "nobody wrote a timelock." Two things were found that schedule, and both are disqualified for the same structural reason:
| Implementation | Schedules? | Enforces delay? | Performs an effect? |
|---|---|---|---|
timelock_guardian (Cosmic Bull, live) | yes | yes | no — sets a flag and emits an event |
r/g16m0r…/timelock (pearl-1, third party) | yes | no (see §3) | no — sets a status enum |
p/nt/commondao/v0 (gno.land core) | via voting deadline | yes | yes, but only after a council vote |
The clause that nothing satisfies
"reusable … for Gno applications … rather than being designed around a single application"
timelock_guardian is an /r/ product realm. It owns its own target registry, its own owner/guardian roles, and its own notion of who may schedule. A second application cannot impose its own authorization rules on it; it can only become a registered target of it and then trust it.
And trusting it does not work, because timelock_guardian.Execute is a bare attestation:
a.Executed = true
dropPending(a.Target, actionID)
chain.Emit("timelock_executed", "id", actionID, "target", a.Target)
return "executed: " + actionID
Execute is permissionless and IsExecuted stays true forever after. So a consumer that polls IsExecuted before doing its own work has gained nothing: it must still implement its own replay guard, because the flag it is reading is a one-way latch that any third party can set, in a transaction the consumer is not part of. The consumer ends up writing the hard part itself and the dependency buys it only a delay check.
This is the finding that shapes the architecture: the missing piece is not a scheduler, it is the atomic consume-then-act step that lets a consumer perform its own effect exactly once, in its own transaction, under its own authority.
2. Sources searched
| # | Source | How | Result |
|---|---|---|---|
| 1 | Cosmic Bull catalog | catalog/primitives.md, catalog/applications.md | 2 primitives, 14 applications reviewed |
| 2 | Cosmic Bull /p/ and /r/ sources | local tree, full read of timelock_guardian | 1 close hit (§3) |
| 3 | On-chain Gno — pearl-1 | vm/qpaths enumeration, all 632 packages (190 /p/, 442 /r/) | 14 keyword hits + a full /p/ namespace sweep |
| 4 | gno.land core corpus | examples/gno.land in the chain-matched source tree ([email protected]), 1022 .gno files | 1 major hit: p/nt/commondao/v0 |
| 5 | Public ecosystem / GitHub / docs | delegated web sweep: gnolang/gno, moul/gno-contracts, gnoswap, onbloc, gnoverse, docs.gno.land | see §5 — reported separately and treated as unverified |
On the method
Source 3 was not keyword-only. All 190 pearl-1 /p/ packages were dumped and read by namespace, precisely because keyword-only searching is how the documented feeledger / bazaar/fee/v1 miss happened (catalog/primitives.md). Source 4 used the chain-matched source tree on disk rather than web search, so the corpus read is the one the target chain was actually built from.
Source 5 is the weakest link and is labelled as such. Its findings are recorded in §5 but no architectural decision rests on them, because a delegated web sweep returned at least one repository reference that could not be corroborated, and claimed two packages (p/moul/kit/lifecycle, p/moul/kit/rand) that a direct vm/qpaths check shows are not deployed on pearl-1. Unverified search output is not evidence.
3. Classified findings
EXACT DUPLICATE — none
No relevant existing implementation was found in the searched sources.
That sentence is the mandated one and it is used in its narrow, literal sense: it is a statement about the five sources listed in §2, at the date of this run. It is not a claim about the Gno ecosystem as a whole, and "not found" is not "does not exist."
RELATED IMPLEMENTATION — examined, not reused
gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/timelock_guardian — Cosmic Bull's own, live on pearl-1 at height 602213.
Genuinely good work for what it is: MinDelayFloor, a MaxDelay that doubles as an overflow guard, a live-action cap, and a state model that stores only PENDING and EXECUTED while reaping cancelled/vetoed/expired actions into events. Its delay, veto, cancel and expire logic is sound.
Not reused because it is an /r/ product, not a capability (§1), and because its Execute performs no effect. It is not modified or redeployed by this work — CLAUDE.md §6.
gno.land/r/g16m0r7rm7fv5hx0ekr7gvx8g4fr7eu08nzhk6gt/timelock — third party, live on pearl-1. Read in full. Disqualified on four independent grounds, each quoted:
- The clock is a caller-supplied parameter. ```go func Tick(height int64) { timelock.Tick(height) }
func (t *Timelock) Tick(height int64) { if height > t.currentHeight { t.currentHeight = height } } `` Tick is exported, unauthenticated, and monotonic only in the sense that it can be advanced arbitrarily far by anyone. Any caller can fast-forward currentHeight past ReadyHeight` and execute immediately. The delay is not enforceable.
- Caller identity is a forgeable string.
go func (t *Timelock) Queue(proposer, target, payload string, requiredApprovals int) (string, bool) { if !t.approvers[proposer] || target == "" || requiredApprovals < 1 {proposerandcallerare parameters, not derived fromcur.Previous(). This is designation forgery (gnosecurity.mdClass 1a). It also means one address satisfies anyrequiredApprovalsby passing differentcallerstrings toApprove. - None of the exported functions are crossing functions. No
cur realmparameter appears anywhere in the file.MsgCallonly dispatches to crossing functions, so on an interrealm-v2 chain this realm's write API is not reachable from a transaction at all. Executeperforms no effect —a.Status = LockExecutedand return.
Recorded here because "a timelock already exists on pearl-1" is exactly the kind of claim that would be true from a package listing and wrong from the source.
gno.land/p/nt/commondao/v0 — gno.land core governance primitive. The only prior art found that actually executes a caller-supplied effect:
if e, ok := p.Definition().(Executable); ok {
if fn := e.Executor(); fn != nil {
err = fn(0, sub)
}
}
Its execution discipline is excellent and two ideas were adopted from it (§4): remove-before-run, and a re-entrancy latch.
// The proposal leaves active storage before any definition code
// (Validate, the executor) runs, so a re-entrant Execute call
// cannot run it twice.
dao.activeProposals.Remove(p.id)
Not reused as the capability, for three reasons:
- Its deferral is a voting deadline, not a schedule. Execution happens when a council vote settles, not at a time the scheduler chose.
VotingPeriod()belongs to the proposal kind, not the caller. - It requires a DAO. A council, an electorate snapshot per proposal, a threshold, a tally. That is a large amount of machinery to impose on an application whose requirement is "do this later."
- Its
ExecFuncis a stored cross-realm closure, and the package's own documentation is unusually direct about the hazard:
"an executor can mint
banker.NewBanker(BankerTypeRealmSend, sub)and simply RETAIN it … a permanent, unrevocable capability over that DAO's address, spendable later with no proposal."
This is the stored-callback anti-pattern from gno security.md, accepted deliberately there behind a vetted-code trust assumption. A general-purpose scheduling primitive cannot make that assumption, because its whole point is to be used by applications whose code the primitive author has not vetted.
Availability note, recorded because it is load-bearing: p/nt/commondao/v0 is not deployed on pearl-1. The nt/ set there is avl, bptree, cford32, combinederr, fqname, groups, markdown, mdalert, mux, ownable, poa, seqid, treasury, uassert, ufmt, urequire. A third-party mirror exists at gno.land/p/g12e22uqd4jjvvsk6g95re3a44sxuephd5kkrt7m/commondao, which also carries its own forked addrset. Importing an anonymous mirror of a core package is a supply-chain decision, and it is not one this objective requires taking.
gno.land/p/g16m0r…/commitreveal — two-phase deferral, but no time dimension, no expiry, no cancellation. It also takes a caller-supplied hashFn func(string) string and stores entries in a Go map. Related in shape only.
gno.land/r/moul/x/daily/timecapsule/v1 — seals a message until unlockHeight. A real, live, correct block-height time-gate, but it is a single-purpose guestbook realm, not a capability.
gno.land/r/moul/x/daily/vestoken/v1 — linear block-height vesting with a pull-based Claim. Time-gated release of value, not of actions.
REUSABLE EXISTING PRIMITIVE — pattern precedent, adopted
gno.land/p/moul/x/daily/ratelimit/v1 — live on pearl-1. Not a scheduler and not a duplicate, but the most architecturally important find in the sweep, because it establishes that the exact shape proposed in §4 is an accepted, live idiom on this chain:
// Package ratelimit is a deterministic token-bucket rate limiter — a port
// of golang.org/x/time/rate with the wall clock replaced by a
// caller-supplied monotonic tick (on-chain, that tick is the block height).
//
// It is a pure library: it imports no chain APIs and reads no ambient
// state.
A pure /p/ that is time-aware without importing the chain, whose state is owned by the consuming realm, whose clock is supplied per call, and whose ordering comes from avl.Tree for determinism. That is precisely the feeledger contract applied to time instead of money. Its one choice not adopted is float64 arithmetic; duebook is integer-only.
gno.land/p/nt/avl/v0 — ordered, deterministic iteration. Reused.
COSMIC BULL EXISTING PRIMITIVE
feeledger and coinio — reviewed and deliberately not used. duebook schedules actions, not payments, and holds no coins. Pulling in a value primitive would add custody surface to a package that has none. The one thing taken from feeledger is its contract shape: error-pure, overflow-checked, consumer-owned state, and an explicit "the parts the package cannot enforce" section.
GENUINELY NEW
The atomic consume-then-act transition, exposed as a reusable, consumer-owned object:
- a deferral becomes claimable at a due time and stops being claimable at an expiry time;
Claimchecks due / not-cancelled / not-expired / not-already-claimed and marks the deferral consumed in the same call, returning success exactly once across all transactions, forever;- the consuming realm performs its own effect after a successful
Claim, in its own crossing function, under its own authorization; - so replay is structurally impossible rather than guarded against, and no closure, callback, or capability ever crosses a realm boundary.
4. Decisions
Decision 1 — build a new /p/ primitive, duebook.
Justified as the smallest intervention by elimination, not by preference: reuse fails because nothing found performs an effect under the consumer's authority; composition fails because the missing piece (§1) is the atomic step itself, which cannot be composed out of parts that lack it; adaptation of timelock_guardian fails because it is a deployed /r/ product that CLAUDE.md §6 forbids modifying, and because turning a product into a capability is a rewrite, not an adaptation.
The intervention is deliberately small: duebook adds only the deferral object and its state machine. It does not add a scheduler daemon, an execution engine, a registry, or a callback mechanism.
Decision 2 — the consumer executes, not the primitive.
Gno has no autonomous execution: no cron, no keepers. Every "scheduled" action requires someone to send a transaction. That is a chain property, not a gap to be papered over. duebook therefore does not pretend to execute anything — it makes the authorization to execute precise, one-shot, and verifiable. This is the design choice that both timelocks got wrong in opposite directions: one claims to execute and doesn't, the other executes but only through a stored closure.
Decision 3 — no stored closures, no ExecFunc, no callbacks. Directly counter to commondao's design, and for a stated reason: a general-purpose primitive cannot assume its consumers' code is vetted.
Decision 4 — caller-supplied clock (now int64), no chain imports. Precedent: ratelimit. Keeps the package pure, unit-testable without a chain, and usable with either block height or block time. The cost — the consumer can lie about now — is real and is recorded as a consumer-contract obligation in §5, not hidden.
Decision 5 — ship a reference consumer realm, duebook_demo.
Not decoration. The central claim of this design is "a claim consumed in transaction N is refused in transaction N+1." A vm/qeval against a pure package is a single ephemeral evaluation and cannot prove that. Only a realm with persistent state, called twice, can. The realm exists to make the main claim falsifiable on-chain. It holds no coins, so it adds no custody surface. Precedent for the pairing: ratelimit + ratelimitdemo, both live on pearl-1.
5. Known limitations, recorded before implementation
- The consumer supplies the clock.
duebookcannot verify thatnowcame fromruntime.ChainHeight()ortime.Now(). A consumer that passes an attacker-controlled value defeats the delay — exactly the bug found inr/g16m0r…/timelock(§3). This is the single most important item in the consumer contract and the primitive cannot enforce it. The reference consumer demonstrates the correct wiring. - The consumer owns the state. Following
feeledger,duebookholds no package-level state. A consumer that exposes its*Bookacross a realm boundary hands out a mutable handle. The package returns values, not pointers to internal state, but it cannot stop a consumer from leaking the book itself. - Expiry is not self-executing. An expired deferral stops being claimable the moment
nowpasses its expiry, but the storage is only reclaimed when someone calls the reap path. Storage growth is bounded by a cap, not by the passage of time. - Source 5 is unverified (§2) and no decision rests on it.
- Ecosystem-wide uniqueness is not claimed. Five sources, one date.
6. Evidence
Commands and reads that produced the findings above, so they can be re-run:
# Source 3 — full pearl-1 enumeration (632 packages), not keyword-only
curl -s "$RPC" -d '{"jsonrpc":"2.0","id":1,"method":"abci_query",
"params":{"path":"vm/qpaths","data":"'"$(printf 'gno.land/' | base64)"'"}}'
# Source 4 — chain-matched corpus, 1022 .gno files
GNOSRC="$HOME/go/pkg/mod/github.com/gnolang/[email protected]"
find "$GNOSRC/examples/gno.land" -type d \
| grep -iE 'timelock|schedul|cron|defer|delay|queue|expir|vest|escrow|auction|deadline|commit.?reveal'
# -> no matches
grep -rl '"time"' "$GNOSRC/examples/gno.land/p" --include='*.gno' | grep -v _test
# -> 17 files; only p/nt/commondao/v0 has an execution concept
Full-source reads (gno_read … full=true, bodies not outlines, because an outline is a realm-authored claim and not evidence):
gno.land/r/g16m0r7rm7fv5hx0ekr7gvx8g4fr7eu08nzhk6gt/timelock→timelock.gnogno.land/p/g16m0r7rm7fv5hx0ekr7gvx8g4fr7eu08nzhk6gt/commitreveal→commitreveal.gnogno.land/p/moul/x/daily/ratelimit/v1→ratelimit.gnogno.land/p/moul/once/v1→once.gno$GNOSRC/examples/gno.land/p/nt/commondao/v0→execution_kind.gno,proposal.gno,commondao.gno- local →
pearl/r/timelock_guardian/timelock_guardian.gno
Re-run this gate before deployment, per docs/DISCOVERY_AND_REUSE.md step 8.