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

storagecost.gno

6.25 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/moul/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}