Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

storagecost.gno

7.79 Kb · 198 lines
  1// Package storagecost answers one question: is it worth paying gas to delete
  2// on-chain state?
  3//
  4// On gno.land every byte of realm state locks GNOT, and the lock is refunded
  5// to whoever signs the transaction that frees the byte. Deleting state is
  6// therefore paid work, and whether a given deletion pays depends on two prices
  7// that move independently: the storage price, a chain parameter, and the gas
  8// price of the day. A contract cannot know the second one, so it cannot decide
  9// on its own behalf whether to compact, reindex or reap. It can only publish
 10// the size of the prize and let a caller do the arithmetic.
 11//
 12// This package is that arithmetic. It is pure integer maths with no chain
 13// imports, so a realm can call it inside a Render and an off-chain bot can
 14// reuse the identical formula.
 15//
 16// The single fact worth remembering is the ratio. One byte freed refunds 100
 17// ugnot, and at the lowest gas price mainnet has actually accepted one ugnot
 18// buys 1000 gas. So a byte is worth 100,000 gas, and any deletion costing less
 19// than that per byte pays for itself.
 20//
 21// Demo realms: gno.land/r/nym-moultest123/x/reaper for reaping expired state, and
 22// gno.land/r/nym-moultest123/x/compact for the compaction half.
 23package storagecost
 24
 25import (
 26	"gno.land/p/nym-moultest123/kit/num/v0"
 27	"gno.land/p/nt/ufmt/v0"
 28)
 29
 30const (
 31	// DefaultStoragePrice is the ugnot locked per byte of realm state, the
 32	// default of the chain's vm:p:storage_price parameter. It is governance
 33	// settable, so read it from the chain rather than trusting this constant
 34	// when real money depends on the answer.
 35	DefaultStoragePrice int64 = 100
 36
 37	// GasPerUgnotFloor is how much gas one ugnot buys at the lowest gas price
 38	// mainnet has been observed to accept, 0.001 ugnot per gas. It is a floor,
 39	// not a promise: the fee a node requires tracks gas_wanted, so asking for
 40	// more headroom raises the fee proportionally.
 41	GasPerUgnotFloor int64 = 1000
 42
 43	// payloadOverheadNum/payloadOverheadDen approximate what a payload really
 44	// costs once the realm has wrapped it in an object. Measured at 1.85x: ten
 45	// 1,024-byte strings in a realm slice cost 18,984 bytes of state, 1,898
 46	// each. It is an estimate and nothing more. The authoritative number is
 47	// the chain's own, from the vm/qstorage query or a StorageDepositEvent.
 48	payloadOverheadNum int64 = 185
 49	payloadOverheadDen int64 = 100
 50
 51	// BytesPerTreeNode is the realm state one balanced-tree container node
 52	// occupies, for the tree containers in this namespace (p/moul/ulist and
 53	// what is built on it).
 54	//
 55	// Measured twice, agreeing exactly. Compacting 8 dead nodes on mainnet
 56	// freed 6,848 bytes (2026-09-23), and 31 nodes in the local integration
 57	// harness freed 26,536. Both give 856.
 58	//
 59	// It is a property of the node, not of the element: the node costs this
 60	// whether the value it carried was 8 bytes or 1,024. That is what makes
 61	// EstimateNodes accurate where EstimateBytes is only indicative.
 62	BytesPerTreeNode int64 = 856
 63
 64	ugnotPerGNOT int64 = 1_000_000
 65)
 66
 67// Refund is the deposit returned for freeing bytes at the given price per
 68// byte. Returns 0 for non-positive inputs rather than panicking, so a Render
 69// on a realm with no state still works.
 70func Refund(bytes, pricePerByte int64) int64 {
 71	if bytes <= 0 || pricePerByte <= 0 {
 72		return 0
 73	}
 74	return bytes * pricePerByte
 75}
 76
 77// BreakEvenBytes is the fewest bytes whose refund covers a fee: the point
 78// where a cleanup stops costing money and starts making it. Below this many
 79// bytes the transaction is charity.
 80func BreakEvenBytes(feeUgnot, pricePerByte int64) int64 {
 81	if pricePerByte <= 0 {
 82		return 0
 83	}
 84	if feeUgnot <= 0 {
 85		return 0
 86	}
 87	// Round up: freeing exactly feeUgnot/pricePerByte bytes must cover the fee.
 88	return (feeUgnot + pricePerByte - 1) / pricePerByte
 89}
 90
 91// GasFee is the fee a transaction asking for gasWanted must pay at a gas price
 92// of num/den ugnot per gas, rounded up.
 93//
 94// The fee tracks gas_wanted rather than gas_used, so unused headroom is paid
 95// for. Pass the ceiling you will actually put in the transaction, not what you
 96// expect to burn.
 97func GasFee(gasWanted, num, den int64) int64 {
 98	if gasWanted <= 0 || num <= 0 || den <= 0 {
 99		return 0
100	}
101	return (gasWanted*num + den - 1) / den
102}
103
104// FloorGasFee is GasFee at the lowest gas price mainnet has accepted.
105func FloorGasFee(gasWanted int64) int64 {
106	return GasFee(gasWanted, 1, GasPerUgnotFloor)
107}
108
109// EstimateBytes guesses the realm state a payload of this many bytes will
110// occupy, applying the measured object overhead.
111//
112// It is for sizing a bounty in a Render, where being within a factor of two
113// beats reporting the raw payload length. Never settle an accounting question
114// with it.
115func EstimateBytes(payloadBytes int64) int64 {
116	if payloadBytes <= 0 {
117		return 0
118	}
119	return payloadBytes * payloadOverheadNum / payloadOverheadDen
120}
121
122// EstimateNodes is the realm state a count of dead container nodes occupies,
123// and so what compacting them frees.
124//
125// Prefer it to EstimateBytes wherever the caller can count, which for a
126// compaction it always can. EstimateBytes scales a payload length by a ratio
127// measured at one size and is wrong at the others: a per-entry floor dominates
128// at the small end, and a realm advertising a bounty that way under-reported
129// by 25x against what the cleanup actually returned on chain (2026-09-23).
130// Counting nodes has no such failure mode. The container reports the exact
131// number, and every node costs the same.
132//
133// Still an estimate. BytesPerTreeNode is measured rather than derived, and the
134// chain's own StorageDepositEvent remains the only settlement.
135func EstimateNodes(nodes int64) int64 {
136	if nodes <= 0 {
137		return 0
138	}
139	return nodes * BytesPerTreeNode
140}
141
142// Quote is a complete answer for one candidate cleanup.
143type Quote struct {
144	Bytes     int64 // bytes the cleanup would free
145	Refund    int64 // ugnot returned for them
146	Fee       int64 // ugnot the transaction will cost
147	Net       int64 // Refund - Fee; negative means it costs more than it pays
148	BreakEven int64 // bytes needed to cover Fee
149}
150
151// Worth reports whether the cleanup pays for itself.
152func (q Quote) Worth() bool { return q.Net > 0 }
153
154// String renders the quote as one line of markdown-safe text, for a Render.
155func (q Quote) String() string {
156	verdict := "not worth it yet"
157	if q.Worth() {
158		verdict = "worth " + FormatGNOT(q.Net)
159	}
160	return ufmt.Sprintf(
161		"%d bytes, refunds %s against %s of gas, break-even %d bytes: %s",
162		q.Bytes, FormatGNOT(q.Refund), FormatGNOT(q.Fee), q.BreakEven, verdict,
163	)
164}
165
166// Evaluate prices one cleanup: freeing bytes in a transaction asking for
167// gasWanted, at a storage price of pricePerByte and a gas price of num/den
168// ugnot per gas.
169func Evaluate(bytes, pricePerByte, gasWanted, num, den int64) Quote {
170	fee := GasFee(gasWanted, num, den)
171	refund := Refund(bytes, pricePerByte)
172	return Quote{
173		Bytes:     bytes,
174		Refund:    refund,
175		Fee:       fee,
176		Net:       refund - fee,
177		BreakEven: BreakEvenBytes(fee, pricePerByte),
178	}
179}
180
181// EvaluateAtFloor is Evaluate at the default storage price and the floor gas
182// price: the best case, and the one to quote when advertising a bounty.
183func EvaluateAtFloor(bytes, gasWanted int64) Quote {
184	return Evaluate(bytes, DefaultStoragePrice, gasWanted, 1, GasPerUgnotFloor)
185}
186
187// FormatGNOT renders ugnot as GNOT with trailing zeros trimmed, because a
188// bounty shown in ugnot is unreadable and gno has no floats.
189//
190// It delegates to [p/moul/kit/num], which owns amount formatting. v0 did it by
191// hand with `amount = -amount`, and since that leaves math.MinInt64 negative
192// both the whole and the fractional part then carried their own sign:
193// FormatGNOT(math.MinInt64) returned "--9223372036854.-775808 GNOT". No caller
194// in this repo can reach it, but the function is exported and a formatter that
195// can return a non-number is not one.
196func FormatGNOT(amount int64) string {
197	return num.GNOTf(amount)
198}