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 realm

Package reaper is a noticeboard whose garbage is a standing bounty.

Readme View source

gno.land/r/moul/x/reaper/v0

A noticeboard whose garbage is a standing bounty.

Anyone can post a note with an expiry. Posting locks a storage deposit, paid by the poster. Once a note expires, anyone at all can delete it, and the chain refunds that deposit to whoever signed the deleting transaction. The poster pays to occupy space; a stranger is paid to reclaim it.

There is no token here, no reward pool and no emission schedule. The incentive is the chain's own storage accounting, which already works this way for every realm on gno.land. This realm only makes it legible.

Why it is safe to let strangers delete things

Reap and Compact are permissionless because the expiry predicate is checked on chain. A reaper cannot delete a note that has not expired, so the worst a malicious caller can do is waste their own gas. That is the general pattern worth taking away: where the predicate for "this is garbage" is cheap to verify on chain, deletion needs no authorization at all, and the protocol is the bounty.

The inverse is the warning. The chain pays for destruction, so in any realm whose delete path is not predicate-guarded, authorization is the only thing standing between it and profitable vandalism.

The interface

Post(body, ttl) adds a note reapable ttl blocks from now, and locks its deposit against you. ttl 0 is allowed and is the cheapest demonstration
Reap(limit) deletes up to limit expired notes. Permissionless. The refund goes to you. Unexpired notes are skipped, not refused, so a reaper never has to guess which indices are ripe
Compact() frees the dead tree nodes reaping left behind. Permissionless, paid the same way
Bounty() prices what is currently on the table, as a storagecost.Quote
Reapable() · Compactable() · Live() free reads: the three numbers a bot needs

The ordering that turned out to matter

Reap walks from the highest index down, and that is economic rather than cosmetic.

In the backing list the oldest indices are the ancestors of the newest, and a node can only be freed once everything below it is dead. So a reap that took the oldest notes first, which is the obvious way to drain an expiry queue, would never create a dead tail: Compactable stays at zero and the tree structure stays locked. That structure is not a rounding error. Measured on chain with 32 entries of 512 bytes, deleting the notes refunded 8,896 bytes and the subsequent compaction refunded a further 27,679, because a list node costs more than the note it carries.

Every candidate is expired either way, so the direction changes nothing about what is legal to delete. It only changes how much the reaper gets paid, by about 4x. The measurement is in p/moul/ulist.

Reap and Compact stay separate calls because they are separate decisions, and they are worth batching in that order: compaction returns nothing while a live note still sits below the dead ones.

What it is built from

The realm is thin on purpose. Two packages own the parts it does not:

  • p/moul/ulist stores the notes and owns compaction. Its Delete is a soft delete that leaves a dead node behind, and its Compact frees those nodes without moving a live index.
  • p/moul/x/storagecost owns the arithmetic: what a byte refunds, and how many bytes a transaction must free to pay for itself.

The figures on the page are estimates

No stdlib call exposes a realm's own locked storage, so every byte count in Render is derived from payload length. Treat the bounty as an advertisement, not a settlement. The authoritative numbers are the chain's, in the StorageDepositEvent and StorageUnlockEvent each transaction emits.


Part of moul/gno-contracts — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.

Dependency graph:

gno.land/r/moul/x/reaper/v0 dependency graph

🧪 Highly experimental — potentially vibe-coded. Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: DISCLAIMER.

Overview

Package reaper is a noticeboard whose garbage is a standing bounty.

Anyone can post a note with an expiry. Posting locks a storage deposit, paid by the poster. Once a note expires, anyone at all can delete it, and the chain refunds that deposit to whoever signed the deleting transaction. So the poster pays to occupy space and a stranger is paid to reclaim it.

Nothing here is a token, a reward pool or an emission schedule. The incentive is the chain's own storage accounting, which already works this way for every realm on gno.land; this realm only makes it legible. Reap and Compact are permissionless because the expiry predicate is checked on chain, so the worst a malicious reaper can do is waste their own gas.

Two collaborators own the parts this realm does not:

  • gno.land/p/moul/ulist/v1 stores the notes and owns compaction. Its Delete is a soft delete that leaves a dead tree node behind, and its Compact frees those nodes without moving any live index.
  • gno.land/p/moul/x/storagecost/v0 owns the arithmetic: what a byte refunds, and how many bytes a transaction must free to pay for itself.

The realm deliberately cannot see its own byte count. No stdlib call exposes a realm's locked storage, so every figure Render shows is an estimate from payload length. The authoritative numbers are the chain's, in the StorageDepositEvent and StorageUnlockEvent each transaction emits.

Functions 8

func Bounty

Action
1func Bounty() storagecost.Quote
source

Bounty prices what is currently on the table for a reaper, at the default storage price and the floor gas price.

The byte figure is an estimate from payload length, never the chain's own accounting. Treat it as an advertisement, not a settlement.

func Compact

crossing Action
1func Compact(cur realm) int
source

Compact frees the tree nodes that reaping left behind and returns how many.

Also permissionless, and also paid the same way. It is a separate call because it is a separate economic decision, and a surprisingly large one: on chain, compacting a drained board returned about three times what deleting the notes themselves did, because a list node costs more than the note it carries. But it returns nothing at all while any live note sits below the dead ones, so the two calls are worth batching in that order: reap, then compact. Compactable says how much is actually there, for free.

func Post

crossing Action
1func Post(cur realm, body string, ttl int64) int
source

Post adds a note that becomes reapable ttl blocks from now, and locks the storage deposit for it against the caller.

A ttl of zero is allowed and makes the note reapable immediately, which is the cheapest way to demonstrate the mechanism.

func Reap

crossing Action
1func Reap(cur realm, limit int) int
source

Reap deletes up to limit expired notes and returns how many it deleted.

Permissionless by design. The storage deposit freed goes to whoever signed this transaction, so a stranger keeping the board tidy is paid for it out of the deposits the posters locked. A note that has not expired is skipped, not refused, so a reaper never has to guess which indices are ripe.

It walks from the highest index down, which is not cosmetic. In the backing list the oldest indices are the ancestors of the newest, so a node can only be freed once everything below it is dead. Reaping oldest-first with a binding limit therefore never creates a dead tail and leaves Compactable at zero, stranding the tree structure, which measures at roughly two thirds of what an entry costs. Reaping newest-first makes each batch immediately compactable. Every candidate is expired either way, so the order changes only who gets paid how much, and it is measured: see the ulist package doc.

func Reapable

Action
1func Reapable() int
source

Reapable counts the notes that have expired and not yet been reaped.

Types 1

type Note

struct
1type Note struct {
2	Body    string
3	Author  address
4	Posted  int64 // block height
5	Expires int64 // block height; reapable once ChainHeight passes it
6}
source

Note is one posting. Body is what costs storage; the rest is bookkeeping that makes the incentive visible in Render.

Imports 7

Source Files 5