Transaction

4708875FE0BD77…2BEDA707A27A

Block 227,788 · index 0 · indexed

Summary

Hash
4708875FE0BD7797F6C3C0D7DA6001EA4EDAFFDAC098AFF7F71E2BEDA707A27A
Block
227,788
Size
14720 bytes
Gas used
19,982,758 / 25,806,600
Fee
258066ugnot
Status
success

Messages

#1AddPackagegno.land/p/moul/x/storagecost/v011 arguments
Attached funds
6000000ugnot

Arguments · 11

  1. #1storagecost
  2. #2README.md
  3. #3# `gno.land/p/moul/x/storagecost/v0` 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.** ```go q := storagecost.EvaluateAtFloor(17835, 5_000_000) q.Refund // 1783500 ugnot q.Fee // 5000 ugnot q.Net // 1778500 ugnot q.Worth() // true q.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`](https://github.com/moul/gno-contracts/tree/main/r/moul/x/reaper). <!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) --> --- Part of **[moul/gno-contracts](https://github.com/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/v0 dependency graph](https://raw.githubusercontent.com/moul/gno-contracts/main/_assets/gno.land/p/moul/x/storagecost/v0/deps.png) > 🧪 **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](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md). <!-- END GNOCONTRACTS FOOTER -->
  4. #4gnomod.toml
  5. #5module = "gno.land/p/moul/x/storagecost/v0" gno = "0.9"
  6. #6storagecost.gno
  7. #7// 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 realm: gno.land/r/moul/x/reaper/v0. package storagecost import "gno.land/p/nt/ufmt/v0" const ( // DefaultStoragePrice is the ugnot locked per byte of realm state, the // default of the chain's vm:p:storage_price parameter. It is governance // settable, so read it from the chain rather than trusting this constant // when real money depends on the answer. DefaultStoragePrice int64 = 100 // GasPerUgnotFloor is how much gas one ugnot buys at the lowest gas price // mainnet has been observed to accept, 0.001 ugnot per gas. It is a floor, // not a promise: the fee a node requires tracks gas_wanted, so asking for // more headroom raises the fee proportionally. GasPerUgnotFloor int64 = 1000 // payloadOverheadNum/payloadOverheadDen approximate what a payload really // costs once the realm has wrapped it in an object. Measured at 1.85x: ten // 1,024-byte strings in a realm slice cost 18,984 bytes of state, 1,898 // each. It is an estimate and nothing more. The authoritative number is // the chain's own, from the vm/qstorage query or a StorageDepositEvent. payloadOverheadNum int64 = 185 payloadOverheadDen int64 = 100 ugnotPerGNOT int64 = 1_000_000 ) // 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 Refund(bytes, pricePerByte int64) int64 { if bytes <= 0 || pricePerByte <= 0 { return 0 } return bytes * pricePerByte } // 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 BreakEvenBytes(feeUgnot, pricePerByte int64) int64 { if pricePerByte <= 0 { return 0 } if feeUgnot <= 0 { return 0 } // Round up: freeing exactly feeUgnot/pricePerByte bytes must cover the fee. return (feeUgnot + pricePerByte - 1) / pricePerByte } // 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 GasFee(gasWanted, num, den int64) int64 { if gasWanted <= 0 || num <= 0 || den <= 0 { return 0 } return (gasWanted*num + den - 1) / den } // FloorGasFee is GasFee at the lowest gas price mainnet has accepted. func FloorGasFee(gasWanted int64) int64 { return GasFee(gasWanted, 1, GasPerUgnotFloor) } // 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 EstimateBytes(payloadBytes int64) int64 { if payloadBytes <= 0 { return 0 } return payloadBytes * payloadOverheadNum / payloadOverheadDen } // Quote is a complete answer for one candidate cleanup. type Quote struct { Bytes int64 // bytes the cleanup would free Refund int64 // ugnot returned for them Fee int64 // ugnot the transaction will cost Net int64 // Refund - Fee; negative means it costs more than it pays BreakEven int64 // bytes needed to cover Fee } // Worth reports whether the cleanup pays for itself. func (q Quote) Worth() bool { return q.Net > 0 } // String renders the quote as one line of markdown-safe text, for a Render. func (q Quote) String() string { verdict := "not worth it yet" if q.Worth() { verdict = "worth " + FormatGNOT(q.Net) } return ufmt.Sprintf( "%d bytes, refunds %s against %s of gas, break-even %d bytes: %s", q.Bytes, FormatGNOT(q.Refund), FormatGNOT(q.Fee), q.BreakEven, verdict, ) } // 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 Evaluate(bytes, pricePerByte, gasWanted, num, den int64) Quote { fee := GasFee(gasWanted, num, den) refund := Refund(bytes, pricePerByte) return Quote{ Bytes: bytes, Refund: refund, Fee: fee, Net: refund - fee, BreakEven: BreakEvenBytes(fee, pricePerByte), } } // 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. func EvaluateAtFloor(bytes, gasWanted int64) Quote { return Evaluate(bytes, DefaultStoragePrice, gasWanted, 1, GasPerUgnotFloor) } // FormatGNOT renders ugnot as GNOT with six decimal places and trailing zeros // trimmed, because a bounty shown in ugnot is unreadable and gno has no // floats. func FormatGNOT(amount int64) string { neg := amount < 0 if neg { amount = -amount } whole := amount / ugnotPerGNOT frac := amount % ugnotPerGNOT out := ufmt.Sprintf("%d", whole) if frac != 0 { // Left-pad the fraction to six digits, then trim trailing zeros. digits := ufmt.Sprintf("%d", frac) for len(digits) < 6 { digits = "0" + digits } for len(digits) > 1 && digits[len(digits)-1] == '0' { digits = digits[:len(digits)-1] } out += "." + digits } if neg { out = "-" + out } return out + " GNOT" }
  8. #8storagecost_test.gno
  9. #9package storagecost import ( "testing" "gno.land/p/nt/uassert/v0" ) func TestRefund(t *testing.T) { tests := []struct { name string bytes int64 price int64 want int64 }{ {"one byte at the default price", 1, DefaultStoragePrice, 100}, {"the measured reap", 17835, DefaultStoragePrice, 1783500}, {"the measured compact", 830, DefaultStoragePrice, 83000}, {"zero bytes", 0, DefaultStoragePrice, 0}, {"negative bytes are not a credit", -100, DefaultStoragePrice, 0}, {"a free chain refunds nothing", 1000, 0, 0}, {"a ten-fold price rise", 1000, 1000, 1000000}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { uassert.Equal(t, tt.want, Refund(tt.bytes, tt.price)) }) } } func TestBreakEvenBytes(t *testing.T) { tests := []struct { name string fee int64 want int64 }{ // A 2.6M gas call at the floor costs 2,600 ugnot, so 26 bytes. {"the canonical 2.6M gas call", 2600, 26}, {"rounds up, never down", 101, 2}, {"exactly one byte", 100, 1}, {"a fee below one byte still needs a byte", 1, 1}, {"no fee, no threshold", 0, 0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := BreakEvenBytes(tt.fee, DefaultStoragePrice) uassert.Equal(t, tt.want, got) // The definition: this many bytes covers the fee, one fewer does not. if tt.want > 0 { uassert.True(t, Refund(got, DefaultStoragePrice) >= tt.fee) uassert.False(t, Refund(got-1, DefaultStoragePrice) >= tt.fee) } }) } uassert.Equal(t, int64(0), BreakEvenBytes(1000, 0)) } func TestGasFee(t *testing.T) { // The floor mainnet has actually accepted: 1 ugnot per 1000 gas. uassert.Equal(t, int64(5000), FloorGasFee(5_000_000)) uassert.Equal(t, int64(2500), FloorGasFee(2_500_000)) // Rounds up, so a fee is never short. uassert.Equal(t, int64(1), FloorGasFee(1)) uassert.Equal(t, int64(0), FloorGasFee(0)) // The harness price used in the measurements, 1.2 ugnot per gas. uassert.Equal(t, int64(6_000_000), GasFee(5_000_000, 12, 10)) uassert.Equal(t, int64(0), GasFee(5_000_000, 0, 10)) uassert.Equal(t, int64(0), GasFee(5_000_000, 1, 0)) } func TestEvaluateReproducesTheMeasuredReap(t *testing.T) { // The measured run: 17,835 bytes freed by a call that used 4,120,876 gas, // so gas_wanted 5,000,000 at the floor. Net +1,778,500 ugnot. q := EvaluateAtFloor(17835, 5_000_000) uassert.Equal(t, int64(1783500), q.Refund) uassert.Equal(t, int64(5000), q.Fee) uassert.Equal(t, int64(1778500), q.Net) uassert.Equal(t, int64(50), q.BreakEven) uassert.True(t, q.Worth()) } func TestEvaluateReproducesTheMeasuredCompact(t *testing.T) { q := EvaluateAtFloor(830, 2_500_000) uassert.Equal(t, int64(83000), q.Refund) uassert.Equal(t, int64(2500), q.Fee) uassert.Equal(t, int64(80500), q.Net) uassert.True(t, q.Worth()) } func TestEvaluateAtTheHarnessPriceIsALoss(t *testing.T) { // The same reap priced at 1.2 ugnot/gas, 1200x the floor, loses money. // This is why the gas price cannot be baked into a contract. q := Evaluate(17835, DefaultStoragePrice, 5_000_000, 12, 10) uassert.Equal(t, int64(6_000_000), q.Fee) uassert.False(t, q.Worth()) uassert.True(t, q.Net < 0) } func TestQuoteString(t *testing.T) { worth := EvaluateAtFloor(17835, 5_000_000) uassert.Equal(t, "17835 bytes, refunds 1.7835 GNOT against 0.005 GNOT of gas, break-even 50 bytes: worth 1.7785 GNOT", worth.String()) loss := Evaluate(10, DefaultStoragePrice, 5_000_000, 12, 10) uassert.Equal(t, "10 bytes, refunds 0.001 GNOT against 6 GNOT of gas, break-even 60000 bytes: not worth it yet", loss.String()) } func TestFormatGNOT(t *testing.T) { tests := []struct { amount int64 want string }{ {0, "0 GNOT"}, {1, "0.000001 GNOT"}, {100, "0.0001 GNOT"}, {1_000_000, "1 GNOT"}, {1_500_000, "1.5 GNOT"}, {1_783_500, "1.7835 GNOT"}, {978_743_800, "978.7438 GNOT"}, {-2_100, "-0.0021 GNOT"}, {1_000_001, "1.000001 GNOT"}, } for _, tt := range tests { uassert.Equal(t, tt.want, FormatGNOT(tt.amount)) } } func TestEstimateBytes(t *testing.T) { // Measured: a 1,024-byte payload occupied 1,898 bytes of realm state. uassert.Equal(t, int64(1894), EstimateBytes(1024)) uassert.Equal(t, int64(0), EstimateBytes(0)) uassert.Equal(t, int64(0), EstimateBytes(-5)) // Monotonic, which is all a bounty estimate really needs. uassert.True(t, EstimateBytes(2048) > EstimateBytes(1024)) }
  10. #10/gno.MemPackageType
  11. #11 MPUserAll

Result log

msg:0,success:true,log:,events:[]

← Back to block 227,788