Storage-rent repairs (version-gated): 64-bit fee arithmetic + EIP-27 re-emission carve-out - #2438
Draft
arkadianet wants to merge 3 commits into
Draft
Storage-rent repairs (version-gated): 64-bit fee arithmetic + EIP-27 re-emission carve-out#2438arkadianet wants to merge 3 commits into
arkadianet wants to merge 3 commits into
Conversation
…e-out (block version 5) Two storage-rent defects strand UTXOs from the mechanism built to recirculate lost coins: 1. EIP-27 deadlock. checkExpiredBox's recreate branch requires the claimed output to preserve the box's tokens, while verifyReemissionSpending forbids any output from carrying the re-emission token. No transaction satisfies both, so any box still holding re-emission tokens (unclaimed post-777217 miner rewards) becomes permanently rent-immune once it ages past the storage period. The first such box (fbf119cb..., 63 ERG, created 777693) crossed the eligibility line at ~1828900; a live mainnet node rejects both possible claim shapes for it today. 2. Int-wrapping storage fee. storageFeeFactor * box.bytes.length wraps in Int: boxes >= ~1718 bytes (at the default factor) get a negative fee, making the recreate floor exceed the box value (rent-immune short of a subsidy); boxes >= ~3436 bytes wrap back positive and are claimable at a fee unrelated to their size. Both fixes are gated on block version 5 (Header.Interpreter70Version / Constants.StorageRentRepairsBlockVersion), because each changes which transactions are valid: below the activation version the legacy semantics (register preservation, Int wrap) are preserved exactly, so historical blocks validate unchanged and the network switches together on soft-fork activation, as with 5.0's reinterpretation of existing scripts. From block version 5, checkExpiredBox: - computes the storage fee in 64-bit arithmetic (the product cannot overflow Long within parameter bounds); - requires the recreated box to DROP the re-emission token (all other registers and tokens preserved as before), and releases 1 nanoErg per token from the recreation floor. verifyReemissionSpending is untouched: it already demands exactly that value be paid to the pay-to-reemission contract, so the rent path now executes the burn obligation instead of deadlocking against it. The freed value flows back through re-emission to future miners - restoring storage rent's purpose for exactly the boxes most likely to be abandoned. The interpreter learns the re-emission token id via a new optional constructor argument, threaded from chain settings at the three node construction sites (candidate generation, block execution, mempool validation); networks without EIP-27 pass None and are unaffected. New ExpirationSpecification properties pin all four verdict flips (first-wrap uncollectable->consumable, second-wrap true fee, token-dropped claim valid, token-kept claim invalid) and the legacy behaviors below the activation version. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PYSS7hBNFeddkwE3RadSfw
Review feedback (fork PR #1): the storage-rent-repairs properties drove an independently-constructed verifier, leaving the production wiring (ErgoStateContext.storageRentReemissionTokenId and the interpreter construction sites) untested. New property runs the full claim through ErgoState.execTransactions - which builds its own interpreter from the state context - under a block-version-5, checkReemissionRules=true context: the recreated box drops the token, the UNCHANGED verifyReemissionSpending accepts the 1-nanoErg-per-token payment to the real pay-to-reemission contract, and the claimer takes the fee. The same transaction through the same wiring under legacy parameters fails, and the context helper is pinned to expose the token id exactly when re-emission rules are active. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PYSS7hBNFeddkwE3RadSfw
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2251. Reference implementation for discussion — draft because the activation vehicle (which release bundles block version 5) is a maintainer decision; merged as-is this changes nothing on any network, since every new branch is gated on the activated block version and unreachable until a v5 vote passes.
Specification: EIP-0048 — ergoplatform/eips#105
Problem 1: storage fee wraps in Int (#2251)
storageFeeFactor * box.bytes.lengthwraps in 32-bit arithmetic. At the default factor: boxes ≥ ~1,718 bytes get a negative fee, making the recreate floor exceed the box's own value — effectively rent-immune; boxes ≥ ~3,436 bytes wrap back to a small wrong positive fee. Both regions are reachable within the 4,096-byte box cap, and the wrap points move with the voted factor.#2375 (credit @a-shannon) has the same arithmetic fix —
.toLong— but ungated. Ungated, the fix flips transaction verdicts between patched and unpatched nodes: the regression-test transaction in #2375 itself is invalid under 6.0.x rules (wrapped fee is negative, so the recreate floor isbox.value + |fee|) and valid under the patch. The first large-box rent claim mined by a patched node would fork it from every unpatched node, and any historical wrapped-fee claim would break resync. This PR contributes the version gate that makes the same arithmetic deployable; #2375 could equally rebase onto the gate if that ordering is preferred.Problem 2: EIP-27 re-emission tokens make boxes permanently rent-immune
checkExpiredBox's recreate branch requires the claimed output to preserve the box's tokens (R2);verifyReemissionSpendingforbids any output from carrying the re-emission token. No transaction satisfies both, and the full-consume escape is unreachable (post-activation only emission-tx outputs can carry the token, and a reward box's value always far exceeds its storage fee). So every unclaimed post-777,217 reward box still holding re-emission tokens becomes permanently exempt from storage rent once aged past the storage period — defeating rent's purpose (recirculating lost coins) for exactly the class most likely to be lost.This is live today, not theoretical. The first such box —
fbf119cbeb73cadc5866f63931599c85e26ea603b9db6d4a97914b88f1f82cfb(63 ERG reward, created 777,693) — became rent-eligible at ~1,828,900, and a mainnet node rejects both possible claim shapes viaPOST /transactions/check:Transaction should conform EIP-27 rulescheckExpiredBox→ false)Measured at height ~1,831,846: 10,917 unspent sub-100K-ERG boxes carry the token (~192K ERG, mostly recent rewards pending routine sweeps); 17 boxes / ~1,026 ERG have sat unswept for 4+ years, and the eligibility window rolls forward every block until ~3,067,199.
The change (all in
checkExpiredBox, gated onblockVersion >= 5)storageFeeFactor.toLong * box.bytes.length; legacy Int-wrap preserved verbatim below the gate (the wrap is consensus-observable — historical blocks must keep validating).verifyReemissionSpendingis unchanged: it already requires that released value be paid to the pay-to-reemission contract, so the rent path now executes the burn instead of deadlocking against it. Worked example for the real box above: recreated 50.83125 ERG (same owner/script), 12 ERG to pay-to-reemission (and thence to future miner emissions), 0.16875 ERG rent fee.ErgoInterpreterlearns the token id via a new optional constructor argument, threaded fromchainSettings.reemissionat the three construction sites (candidate generation, block execution, mempool validation); networks without EIP-27 passNoneand are byte-identical.Related: EIP-0033 (eips#68) tightens the full-consume branch (burn seized tokens); orthogonal and composable — under this PR the re-emission token is dropped, not seized, in both branches, which is EIP-0033-consistent.
Test plan
testOnly org.ergoplatform.modifiers.mempool.ExpirationSpecification— 16/16: all 11 existing properties untouched and green (legacy semantics preserved), plus 5 new: first-wrap box uncollectable→fully-consumable, second-wrap box charged its true fee, token-dropped claim invalid→valid, token-kept claim valid→invalid, and an end-to-end property throughErgoState.execTransactions(production context→interpreter wiring,checkReemissionRules = true) where the repaired claim validates against the real pay-to-reemission contract and the same tx fails under legacy parameters.ergoWallet/test— 57/57;ergoCore/testOnly org.ergoplatform.reemission.ReemissionRulesSpec— green.An independent Rust-node implementation of the current rules' consequence (claim builders must skip re-emission-token boxes, with the live-node rejections captured as fixtures): arkadianet/ergo#225
🤖 Generated with Claude Code
https://claude.ai/code/session_01PYSS7hBNFeddkwE3RadSfw