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

6.26 Kb · 177 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 realm: gno.land/r/nym-moultest123/x/reaper/v0.
 22package storagecost
 23
 24import "gno.land/p/nt/ufmt/v0"
 25
 26const (
 27	// DefaultStoragePrice is the ugnot locked per byte of realm state, the
 28	// default of the chain's vm:p:storage_price parameter. It is governance
 29	// settable, so read it from the chain rather than trusting this constant
 30	// when real money depends on the answer.
 31	DefaultStoragePrice int64 = 100
 32
 33	// GasPerUgnotFloor is how much gas one ugnot buys at the lowest gas price
 34	// mainnet has been observed to accept, 0.001 ugnot per gas. It is a floor,
 35	// not a promise: the fee a node requires tracks gas_wanted, so asking for
 36	// more headroom raises the fee proportionally.
 37	GasPerUgnotFloor int64 = 1000
 38
 39	// payloadOverheadNum/payloadOverheadDen approximate what a payload really
 40	// costs once the realm has wrapped it in an object. Measured at 1.85x: ten
 41	// 1,024-byte strings in a realm slice cost 18,984 bytes of state, 1,898
 42	// each. It is an estimate and nothing more. The authoritative number is
 43	// the chain's own, from the vm/qstorage query or a StorageDepositEvent.
 44	payloadOverheadNum int64 = 185
 45	payloadOverheadDen int64 = 100
 46
 47	ugnotPerGNOT int64 = 1_000_000
 48)
 49
 50// Refund is the deposit returned for freeing bytes at the given price per
 51// byte. Returns 0 for non-positive inputs rather than panicking, so a Render
 52// on a realm with no state still works.
 53func Refund(bytes, pricePerByte int64) int64 {
 54	if bytes <= 0 || pricePerByte <= 0 {
 55		return 0
 56	}
 57	return bytes * pricePerByte
 58}
 59
 60// BreakEvenBytes is the fewest bytes whose refund covers a fee: the point
 61// where a cleanup stops costing money and starts making it. Below this many
 62// bytes the transaction is charity.
 63func BreakEvenBytes(feeUgnot, pricePerByte int64) int64 {
 64	if pricePerByte <= 0 {
 65		return 0
 66	}
 67	if feeUgnot <= 0 {
 68		return 0
 69	}
 70	// Round up: freeing exactly feeUgnot/pricePerByte bytes must cover the fee.
 71	return (feeUgnot + pricePerByte - 1) / pricePerByte
 72}
 73
 74// GasFee is the fee a transaction asking for gasWanted must pay at a gas price
 75// of num/den ugnot per gas, rounded up.
 76//
 77// The fee tracks gas_wanted rather than gas_used, so unused headroom is paid
 78// for. Pass the ceiling you will actually put in the transaction, not what you
 79// expect to burn.
 80func GasFee(gasWanted, num, den int64) int64 {
 81	if gasWanted <= 0 || num <= 0 || den <= 0 {
 82		return 0
 83	}
 84	return (gasWanted*num + den - 1) / den
 85}
 86
 87// FloorGasFee is GasFee at the lowest gas price mainnet has accepted.
 88func FloorGasFee(gasWanted int64) int64 {
 89	return GasFee(gasWanted, 1, GasPerUgnotFloor)
 90}
 91
 92// EstimateBytes guesses the realm state a payload of this many bytes will
 93// occupy, applying the measured object overhead.
 94//
 95// It is for sizing a bounty in a Render, where being within a factor of two
 96// beats reporting the raw payload length. Never settle an accounting question
 97// with it.
 98func EstimateBytes(payloadBytes int64) int64 {
 99	if payloadBytes <= 0 {
100		return 0
101	}
102	return payloadBytes * payloadOverheadNum / payloadOverheadDen
103}
104
105// Quote is a complete answer for one candidate cleanup.
106type Quote struct {
107	Bytes     int64 // bytes the cleanup would free
108	Refund    int64 // ugnot returned for them
109	Fee       int64 // ugnot the transaction will cost
110	Net       int64 // Refund - Fee; negative means it costs more than it pays
111	BreakEven int64 // bytes needed to cover Fee
112}
113
114// Worth reports whether the cleanup pays for itself.
115func (q Quote) Worth() bool { return q.Net > 0 }
116
117// String renders the quote as one line of markdown-safe text, for a Render.
118func (q Quote) String() string {
119	verdict := "not worth it yet"
120	if q.Worth() {
121		verdict = "worth " + FormatGNOT(q.Net)
122	}
123	return ufmt.Sprintf(
124		"%d bytes, refunds %s against %s of gas, break-even %d bytes: %s",
125		q.Bytes, FormatGNOT(q.Refund), FormatGNOT(q.Fee), q.BreakEven, verdict,
126	)
127}
128
129// Evaluate prices one cleanup: freeing bytes in a transaction asking for
130// gasWanted, at a storage price of pricePerByte and a gas price of num/den
131// ugnot per gas.
132func Evaluate(bytes, pricePerByte, gasWanted, num, den int64) Quote {
133	fee := GasFee(gasWanted, num, den)
134	refund := Refund(bytes, pricePerByte)
135	return Quote{
136		Bytes:     bytes,
137		Refund:    refund,
138		Fee:       fee,
139		Net:       refund - fee,
140		BreakEven: BreakEvenBytes(fee, pricePerByte),
141	}
142}
143
144// EvaluateAtFloor is Evaluate at the default storage price and the floor gas
145// price: the best case, and the one to quote when advertising a bounty.
146func EvaluateAtFloor(bytes, gasWanted int64) Quote {
147	return Evaluate(bytes, DefaultStoragePrice, gasWanted, 1, GasPerUgnotFloor)
148}
149
150// FormatGNOT renders ugnot as GNOT with six decimal places and trailing zeros
151// trimmed, because a bounty shown in ugnot is unreadable and gno has no
152// floats.
153func FormatGNOT(amount int64) string {
154	neg := amount < 0
155	if neg {
156		amount = -amount
157	}
158	whole := amount / ugnotPerGNOT
159	frac := amount % ugnotPerGNOT
160
161	out := ufmt.Sprintf("%d", whole)
162	if frac != 0 {
163		// Left-pad the fraction to six digits, then trim trailing zeros.
164		digits := ufmt.Sprintf("%d", frac)
165		for len(digits) < 6 {
166			digits = "0" + digits
167		}
168		for len(digits) > 1 && digits[len(digits)-1] == '0' {
169			digits = digits[:len(digits)-1]
170		}
171		out += "." + digits
172	}
173	if neg {
174		out = "-" + out
175	}
176	return out + " GNOT"
177}