1// Package storagecost answers one question: is it worth paying gas to delete2// on-chain state?3//4// On gno.land every byte of realm state locks GNOT, and the lock is refunded5// to whoever signs the transaction that frees the byte. Deleting state is6// therefore paid work, and whether a given deletion pays depends on two prices7// that move independently: the storage price, a chain parameter, and the gas8// price of the day. A contract cannot know the second one, so it cannot decide9// on its own behalf whether to compact, reindex or reap. It can only publish10// 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 chain13// imports, so a realm can call it inside a Render and an off-chain bot can14// reuse the identical formula.15//16// The single fact worth remembering is the ratio. One byte freed refunds 10017// ugnot, and at the lowest gas price mainnet has actually accepted one ugnot18// buys 1000 gas. So a byte is worth 100,000 gas, and any deletion costing less19// than that per byte pays for itself.20//21// Demo realm: gno.land/r/moul/x/reaper/v0.22package storagecost2324import "gno.land/p/nt/ufmt/v0"2526const (27 // DefaultStoragePrice is the ugnot locked per byte of realm state, the28 // default of the chain's vm:p:storage_price parameter. It is governance29 // settable, so read it from the chain rather than trusting this constant30 // when real money depends on the answer.31 DefaultStoragePrice int64 = 1003233 // GasPerUgnotFloor is how much gas one ugnot buys at the lowest gas price34 // 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 for36 // more headroom raises the fee proportionally.37 GasPerUgnotFloor int64 = 10003839 // payloadOverheadNum/payloadOverheadDen approximate what a payload really40 // costs once the realm has wrapped it in an object. Measured at 1.85x: ten41 // 1,024-byte strings in a realm slice cost 18,984 bytes of state, 1,89842 // each. It is an estimate and nothing more. The authoritative number is43 // the chain's own, from the vm/qstorage query or a StorageDepositEvent.44 payloadOverheadNum int64 = 18545 payloadOverheadDen int64 = 1004647 ugnotPerGNOT int64 = 1_000_00048)4950// Refund is the deposit returned for freeing bytes at the given price per51// byte. Returns 0 for non-positive inputs rather than panicking, so a Render52// on a realm with no state still works.53func Refund(bytes, pricePerByte int64) int64 {54 if bytes <= 0 || pricePerByte <= 0 {55 return 056 }57 return bytes * pricePerByte58}5960// BreakEvenBytes is the fewest bytes whose refund covers a fee: the point61// where a cleanup stops costing money and starts making it. Below this many62// bytes the transaction is charity.63func BreakEvenBytes(feeUgnot, pricePerByte int64) int64 {64 if pricePerByte <= 0 {65 return 066 }67 if feeUgnot <= 0 {68 return 069 }70 // Round up: freeing exactly feeUgnot/pricePerByte bytes must cover the fee.71 return (feeUgnot + pricePerByte - 1) / pricePerByte72}7374// GasFee is the fee a transaction asking for gasWanted must pay at a gas price75// of num/den ugnot per gas, rounded up.76//77// The fee tracks gas_wanted rather than gas_used, so unused headroom is paid78// for. Pass the ceiling you will actually put in the transaction, not what you79// expect to burn.80func GasFee(gasWanted, num, den int64) int64 {81 if gasWanted <= 0 || num <= 0 || den <= 0 {82 return 083 }84 return (gasWanted*num + den - 1) / den85}8687// FloorGasFee is GasFee at the lowest gas price mainnet has accepted.88func FloorGasFee(gasWanted int64) int64 {89 return GasFee(gasWanted, 1, GasPerUgnotFloor)90}9192// EstimateBytes guesses the realm state a payload of this many bytes will93// occupy, applying the measured object overhead.94//95// It is for sizing a bounty in a Render, where being within a factor of two96// beats reporting the raw payload length. Never settle an accounting question97// with it.98func EstimateBytes(payloadBytes int64) int64 {99 if payloadBytes <= 0 {100 return 0101 }102 return payloadBytes * payloadOverheadNum / payloadOverheadDen103}104105// Quote is a complete answer for one candidate cleanup.106type Quote struct {107 Bytes int64 // bytes the cleanup would free108 Refund int64 // ugnot returned for them109 Fee int64 // ugnot the transaction will cost110 Net int64 // Refund - Fee; negative means it costs more than it pays111 BreakEven int64 // bytes needed to cover Fee112}113114// Worth reports whether the cleanup pays for itself.115func (q Quote) Worth() bool { return q.Net > 0 }116117// 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}128129// Evaluate prices one cleanup: freeing bytes in a transaction asking for130// gasWanted, at a storage price of pricePerByte and a gas price of num/den131// 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}143144// EvaluateAtFloor is Evaluate at the default storage price and the floor gas145// 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}149150// FormatGNOT renders ugnot as GNOT with six decimal places and trailing zeros151// trimmed, because a bounty shown in ugnot is unreadable and gno has no152// floats.153func FormatGNOT(amount int64) string {154 neg := amount < 0155 if neg {156 amount = -amount157 }158 whole := amount / ugnotPerGNOT159 frac := amount % ugnotPerGNOT160161 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" + digits167 }168 for len(digits) > 1 && digits[len(digits)-1] == '0' {169 digits = digits[:len(digits)-1]170 }171 out += "." + digits172 }173 if neg {174 out = "-" + out175 }176 return out + " GNOT"177}178Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.