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

v1 source pure

Package storagecost answers one question: is it worth paying gas to delete on-chain state?

Readme View source

gno.land/p/moul/x/storagecost/v1

Is it worth paying gas to delete on-chain state?

On gno.land every byte of realm state locks GNOT, and the lock is refunded to whoever signs the transaction that frees the byte. Deleting state is therefore paid work. Whether a particular deletion pays depends on two prices that move independently: the storage price, a chain parameter, and the gas price of the day. A contract cannot know the second one, so it cannot decide on its own behalf whether to compact, reindex or reap. It can only publish the size of the prize and let a caller do the arithmetic.

This package is that arithmetic: pure integer maths, no chain imports, so a realm can call it inside a Render and an off-chain bot can reuse the identical formula.

The one number to remember

One byte freed refunds 100 ugnot. At the lowest gas price mainnet has actually accepted, one ugnot buys 1000 gas. So a byte is worth 100,000 gas, and any deletion costing less than that per byte pays for itself.

1q := storagecost.EvaluateAtFloor(17835, 5_000_000)
2q.Refund // 1783500 ugnot
3q.Fee    //    5000 ugnot
4q.Net    // 1778500 ugnot
5q.Worth() // true
6q.String() // "17835 bytes, refunds 1.7835 GNOT against 0.005 GNOT of gas, break-even 50 bytes: worth 1.7785 GNOT"

What is in here

Refund · BreakEvenBytes · Net the core arithmetic. BreakEvenBytes rounds up, so a quoted threshold always covers the fee
GasFee · FloorGasFee a fee from a gas ceiling and a rational gas price. The fee tracks gas_wanted, not gas_used, so unused headroom is paid for
Evaluate · EvaluateAtFloor · Quote a whole verdict for one candidate cleanup, with a String() fit for a Render
EstimateBytes what a payload really costs once a realm has wrapped it in an object, at the measured 1.85x
FormatGNOT ugnot as readable GNOT, because gno has no floats and a bounty quoted in ugnot is unreadable

Two honesty notes

DefaultStoragePrice is a default, not a fact. vm:p:storage_price is governance settable. Read it from the chain when real money depends on the answer; the constant is for sizing and display.

EstimateBytes is an estimate. It exists so a bounty shown on a page is within a factor of two instead of reporting raw payload length. No stdlib call exposes a realm's real locked storage, so the authoritative numbers are the chain's: the vm/qstorage query, or the StorageDepositEvent and StorageUnlockEvent every transaction emits. Never settle an accounting question with a guess.

Demo realm: r/moul/x/reaper.

v1: FormatGNOT delegates to p/moul/kit/num

v0 formatted by hand with amount = -amount, which leaves math.MinInt64 negative, so both the whole and the fractional part then carried their own sign: FormatGNOT(math.MinInt64) returned --9223372036854.-775808 GNOT. No caller in this repo can reach a negative (q.Net is the only one that can be, and it is printed only inside if q.Worth()), but the function is exported and a formatter that can return a non-number is not one. v0 stays resolvable.


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/p/moul/x/storagecost/v1 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 storagecost answers one question: is it worth paying gas to delete on-chain state?

On gno.land every byte of realm state locks GNOT, and the lock is refunded to whoever signs the transaction that frees the byte. Deleting state is therefore paid work, and whether a given deletion pays depends on two prices that move independently: the storage price, a chain parameter, and the gas price of the day. A contract cannot know the second one, so it cannot decide on its own behalf whether to compact, reindex or reap. It can only publish the size of the prize and let a caller do the arithmetic.

This package is that arithmetic. It is pure integer maths with no chain imports, so a realm can call it inside a Render and an off-chain bot can reuse the identical formula.

The single fact worth remembering is the ratio. One byte freed refunds 100 ugnot, and at the lowest gas price mainnet has actually accepted one ugnot buys 1000 gas. So a byte is worth 100,000 gas, and any deletion costing less than that per byte pays for itself.

Demo realms: gno.land/r/moul/x/reaper for reaping expired state, and gno.land/r/moul/x/compact for the compaction half.

Constants 1

const DefaultStoragePrice, GasPerUgnotFloor, payloadOverheadNum, payloadOverheadDen, BytesPerTreeNode, ugnotPerGNOT

 1const (
 2	// DefaultStoragePrice is the ugnot locked per byte of realm state, the
 3	// default of the chain's vm:p:storage_price parameter. It is governance
 4	// settable, so read it from the chain rather than trusting this constant
 5	// when real money depends on the answer.
 6	DefaultStoragePrice int64 = 100
 7
 8	// GasPerUgnotFloor is how much gas one ugnot buys at the lowest gas price
 9	// mainnet has been observed to accept, 0.001 ugnot per gas. It is a floor,
10	// not a promise: the fee a node requires tracks gas_wanted, so asking for
11	// more headroom raises the fee proportionally.
12	GasPerUgnotFloor int64 = 1000
13
14	// payloadOverheadNum/payloadOverheadDen approximate what a payload really
15	// costs once the realm has wrapped it in an object. Measured at 1.85x: ten
16	// 1,024-byte strings in a realm slice cost 18,984 bytes of state, 1,898
17	// each. It is an estimate and nothing more. The authoritative number is
18	// the chain's own, from the vm/qstorage query or a StorageDepositEvent.
19	payloadOverheadNum int64 = 185
20	payloadOverheadDen int64 = 100
21
22	// BytesPerTreeNode is the realm state one balanced-tree container node
23	// occupies, for the tree containers in this namespace (p/moul/ulist and
24	// what is built on it).
25	//
26	// Measured twice, agreeing exactly. Compacting 8 dead nodes on mainnet
27	// freed 6,848 bytes (2026-09-23), and 31 nodes in the local integration
28	// harness freed 26,536. Both give 856.
29	//
30	// It is a property of the node, not of the element: the node costs this
31	// whether the value it carried was 8 bytes or 1,024. That is what makes
32	// EstimateNodes accurate where EstimateBytes is only indicative.
33	BytesPerTreeNode int64 = 856
34
35	ugnotPerGNOT int64 = 1_000_000
36)
source

Functions 9

func BreakEvenBytes

1func BreakEvenBytes(feeUgnot, pricePerByte int64) int64
source

BreakEvenBytes is the fewest bytes whose refund covers a fee: the point where a cleanup stops costing money and starts making it. Below this many bytes the transaction is charity.

func EstimateBytes

1func EstimateBytes(payloadBytes int64) int64
source

EstimateBytes guesses the realm state a payload of this many bytes will occupy, applying the measured object overhead.

It is for sizing a bounty in a Render, where being within a factor of two beats reporting the raw payload length. Never settle an accounting question with it.

func EstimateNodes

1func EstimateNodes(nodes int64) int64
source

EstimateNodes is the realm state a count of dead container nodes occupies, and so what compacting them frees.

Prefer it to EstimateBytes wherever the caller can count, which for a compaction it always can. EstimateBytes scales a payload length by a ratio measured at one size and is wrong at the others: a per-entry floor dominates at the small end, and a realm advertising a bounty that way under-reported by 25x against what the cleanup actually returned on chain (2026-09-23). Counting nodes has no such failure mode. The container reports the exact number, and every node costs the same.

Still an estimate. BytesPerTreeNode is measured rather than derived, and the chain's own StorageDepositEvent remains the only settlement.

func FloorGasFee

1func FloorGasFee(gasWanted int64) int64
source

FloorGasFee is GasFee at the lowest gas price mainnet has accepted.

func FormatGNOT

1func FormatGNOT(amount int64) string
source

FormatGNOT renders ugnot as GNOT with trailing zeros trimmed, because a bounty shown in ugnot is unreadable and gno has no floats.

It delegates to p/moul/kit/num, which owns amount formatting. v0 did it by hand with `amount = -amount`, and since that leaves math.MinInt64 negative both the whole and the fractional part then carried their own sign: FormatGNOT(math.MinInt64) returned "--9223372036854.-775808 GNOT". No caller in this repo can reach it, but the function is exported and a formatter that can return a non-number is not one.

func GasFee

1func GasFee(gasWanted, num, den int64) int64
source

GasFee is the fee a transaction asking for gasWanted must pay at a gas price of num/den ugnot per gas, rounded up.

The fee tracks gas_wanted rather than gas_used, so unused headroom is paid for. Pass the ceiling you will actually put in the transaction, not what you expect to burn.

func Refund

1func Refund(bytes, pricePerByte int64) int64
source

Refund is the deposit returned for freeing bytes at the given price per byte. Returns 0 for non-positive inputs rather than panicking, so a Render on a realm with no state still works.

func Evaluate

1func Evaluate(bytes, pricePerByte, gasWanted, num, den int64) Quote
source

Evaluate prices one cleanup: freeing bytes in a transaction asking for gasWanted, at a storage price of pricePerByte and a gas price of num/den ugnot per gas.

func EvaluateAtFloor

1func EvaluateAtFloor(bytes, gasWanted int64) Quote
source

EvaluateAtFloor is Evaluate at the default storage price and the floor gas price: the best case, and the one to quote when advertising a bounty.

Types 1

type Quote

struct
1type Quote struct {
2	Bytes     int64 // bytes the cleanup would free
3	Refund    int64 // ugnot returned for them
4	Fee       int64 // ugnot the transaction will cost
5	Net       int64 // Refund - Fee; negative means it costs more than it pays
6	BreakEven int64 // bytes needed to cover Fee
7}
source

Quote is a complete answer for one candidate cleanup.

Methods on Quote

func String

method on Quote
1func (q Quote) String() string
source

String renders the quote as one line of markdown-safe text, for a Render.

func Worth

method on Quote
1func (q Quote) Worth() bool
source

Worth reports whether the cleanup pays for itself.

Imports 2

Source Files 4