Repository navigation
Consensus: Batched extended coinbase maturity temporary softfork - #404
FlyTheElephant1 wants to merge 8 commits into
Conversation
…ts#402 Use bitcoinknots#402's MTP binary search (lo+1) so getdeploymentinfo reports activation height when the tip MTP first crosses the start time. Keep window [start, expiry) after RDTS so in-window coinbases stay locked. Functional test uses exact init-error strings, mines the post-expiry coinbase to MiniWallet, and covers short-tranche maturity plus lock-sticks-after-expiry.
| /** Coinbase transaction outputs can only be spent after this number of new blocks (network rule) */ | ||
| static const int COINBASE_MATURITY = 100; | ||
| /** Batched extended coinbase maturity (temporary softfork). Assigned by | ||
| * (coinbase_height - window_start) % 6 so 1/6 of window coinbases mature |
There was a problem hiding this comment.
Is batching needed here? Attaching maturity to the creating height already staggers unlocks, one coinbase per block. The single-date cliff only exists when everything unlocks at the expiry.
There was a problem hiding this comment.
Well if there is a very long time where no coins reach maturity, then they all of a sudden start reaching maturity, it causes a large supply shock. The purpose of bringing some small amount forward of the mean and the bulk of it after the mean is to achieve the desired result without as high of a risk of causing a stall.
Prevents single calendar events whereby people will attempt to try and gamify the market dynamics by creating 3 different calendar events whereby some amount begin maturation. Also forces miners to begin thinking about things in multiple different future timeframes instead of one timeframe.
Single event date causes instability, and could lead to a stall (the stall condition is mostly solved in #403 solution in a different way than here)
There was a problem hiding this comment.
This is an image of the per block maturation cycle. from activation to end of rdts, and from end of rtds until it is back to baseline.
(should be noted that the chart is slightly wrong here, there would be a 100 block period where the maturation rate would be 3.125 baseline after rdts ends before the first post rdts 100 block maturation begins happening simultaneous to the backlog)
There was a problem hiding this comment.
I could be missing something here, but along the same lines as kwsantiago's thoughts, wouldn't the 2016 tranche be gameable? If I had huge hashrate (10-15 ph) in a stratum pool I could mine every 6th block as hard as possible and mine a lot of them. Drop off in the other tranches and the DA could go down because minimal hashrate is mining in those tranches. This hurts those not gaming the system (the ones that the system is trying to protect) because they would find mainly those blocks in the long tranches. Worse, if the attacker wanted to do this for a few epochs and then just kill hashrate all together.
One way to possibly fix the "I know what tranche this block is in" is to use something miners know only after working on a block - hash % 6. hash % 6 would be statistically distributed, not as uniform as height based.
And back to kwsantiago's point, are tranches necessary?
There was a problem hiding this comment.
No I don't think so. This doesn't change the reward of any particular block, the divisor by 6 creates a vesting schedule. whereby 1/6th matures at 2016 blocks, 2/6ths mature at 4032 blocks, and the remaining 3/6ths matures at 8064 blocks after being mined.
The purpose of it is to smooth the network maturation rate and prevent events where the maturation in total spikes (relative to 402 and 403). 402 and 403 matures after a set number of blocks or time and then matures all at once when rdts expires. 404 matures gradually in phases over 3 set numbers of blocks and any block that was subject to the rule is always subject to the rule so there arent a huge number of blocks maturing on the block where rdts expires. This proposal is intended to reduce the number of ways for people to game things.
The tranches are neccessary to remove single events that speculators can try to pump and dump the market, and to smooth out the supply dynamic effects. (again, does not alter the reward quantity for any block)
There was a problem hiding this comment.
I guess one other answer to another part of this question on why batch is this: each individual block could have three maturations, but this change from my understanding would require tripling the size of the coinbase transaction for each block. Batching by modulo6blockheight keeps data allocation low while providing an almost same effect to everyone except for true solo miners.
There was a problem hiding this comment.
"No, I don't think so." Not sure what you mean by that. I wasn't saying that the reward is changed by your approach. I get the tranches/schedule. I'm saying as a miner I know EXACTLY what tranche the block I'm about to work on goes in BEFORE I work on it. If I know this, I think I can game it. The only way to prevent that is if the tranche is unknowable before I mine the block, which is what using hash modulo 6 accomplishes.
There was a problem hiding this comment.
The only way you could game it is by shutting off your hashrate and getting less blocks. You can game it if you want less blocks and equipment sitting dormant.
Hash modulo could work to fix that and if people want this proposal then it could be added. I wanted to try and keep it as simple as possible so that the largest number of people could understand what it does while they figure out what they want to do.
Good idea.
There was a problem hiding this comment.
pull 409 resolves AcesHigh70 concern. Thank you aces.
#409
Create run-ci.yml
|
Concept ACK — and of the current maturity proposals I think this height-based one is the right shape to consolidate behind. What makes it work, from reading the code:
I'd favour this over the block-hash variant in #409: neither tranche assignment is choosable before a block is found, so the hash-based randomness buys no additional resistance while making the schedule harder to reason about at reorg boundaries. Deterministic-by-height is easier to review and audit. One thing worth tightening: Overall I'd support shipping this as a temporary softfork in the next release, activated at a chosen height. It's a proportionate, reversible defence against hit-and-run hashrate without needing a hardfork. |
|
@Kilombino I made a fix for what I think is the same issue on #409 . I can fix it here too if we want #404 instead of #409 #409 is essentially an almost identical proposal except that instead of batching them by height, it batches them by hash. So nobody would be able to know what the maturation length they are currently mining will be on that one. |
|
Tested 061c403 on Ubuntu 24.04, gcc, wallet on since the test needs it. 64bc2e8 adds Attaching the lock to the creating height is the right answer to the tail-of-window point, and the part I checked hardest is the one that makes it work: the window bounds come from the parent's own chain in The schedule is denominated in blocks, so it does not deliver the calendar time it is described in. 2016, 4032 and 8064 are given as "~14 days", "~28 days" and "~56 days" at 10 min. The chain has not run at 10 min since the fork: over blocks 971171-971370 the mean median-time-past step is 168.8 s. A miner whose reward is meant to be held for something on the order of a month gets it back in eleven days. This is the same basis problem I raised on #402, and #403 is the variant that fixes it by measuring the duration in median-time-past seconds instead of blocks. The two corrections are orthogonal: attaching the lock to the creating height, as here, and denominating the duration in time, as in #403, address different halves of the same proposal. A version carrying both would not have either gap, and at that point the batching is the only thing left to argue about. On whether the tranche is gameable, since @AcesHigh70's question is still open. The tranche is knowable before the work starts, so the concern is fair on its face, but a miner cannot choose its height, only whether to mine on the tip it has. Taking only batch 0 therefore means running one sixth of the time: Nothing prices that, so I do not think the height-based tranche is worth gaming at these parameters. The second-order effect is real though: a miner who did concentrate on batch 0 would raise the difficulty everyone else faces while leaving them the long tranches, which is @AcesHigh70's point and it stands even if nobody rational does it. One thing worth a line in the release note rather than a change: because the batch here is @FlyTheElephant1 has since opened #409 with @Kilombino, on preferring this to #409 because "neither tranche assignment is choosable before a block is found": the height one is. A miner knows the height it is working on before it starts, which is exactly the opening @AcesHigh70 raised, and the residue of a hash is only readable off a solution that already meets the target. The two are not equally blind, and the asymmetry runs the other way from the one in your reply: buying the short tranche by height means idling the equipment through five sixths of the blocks, buying it by hash means finding blocks and throwing five of every six away, which forgoes the same revenue and burns the power on top. The reorg-boundary point is also the height variant's, not the hash variant's: None of that is an argument against consolidating. It is an argument that the thing to consolidate on is the residue source, and on this evidence it should be the hash. Scripts and raw output: https://gist.github.com/jasonsopko/afd2f55c8bf248c18f5a1072a178aa51 |
|
Something none of the five maturity branches has put a number on: whose coins these locks actually hold, and how long those coins sit today. I measured it on this chain rather than reasoning about it, and the answer is not the one I expected when I started. All of #402, #403, #404, #409 and #410 lock generation outputs. Over the last 2016 blocks (969726-971741), 6302.6 BTC of generation output, from my own node's REST interface: 62.5 % of the generation value goes to a single output, which looks like an argument that the lock falls on pool treasuries rather than on miners. It is not, and this is the table that matters. Joining the 400 most recent transactions that spend a coinbase back to the coinbases they spend gives the age of each generation output at the moment it moved: Age is in blocks between the output being created and being spent, so 100 is the ordinary maturity. The pool with the single-output coinbases spends it at a median age of 102 blocks into a transaction with 96 outputs. Three of those, picked at the tip: 81, 101 and 87 distinct addresses, smallest output 0.0100 BTC in each, which is a payout threshold. It is not holding a treasury and it is not paying off-chain. It pays its hashers on-chain, one block after consensus lets it, in a second transaction instead of in the coinbase. The rows with many coinbase outputs are the pools that pay in the coinbase itself, and there the spends are the individual miners spending their own payouts, which is why those medians are longer and noisier. So the difference between a 116-output coinbase and a one-output coinbase followed by a 96-output payout is one block of settlement, not a difference in who ends up holding the coins. On the evidence here, none of the pools carrying real volume holds the reward back: the coins reach hashers as soon as the 100 blocks are up, in the coinbase or one block after it. That means an extended maturity does not land on pool treasuries. It lands on the hasher's paycheck, at every pool measured, and the only way a pool can avoid passing it on is to finance the gap out of its own capital -- 6480 blocks at the rate this chain actually runs is 12.7 days of payouts, and 8064 blocks is 15.8. The capacity to do that scales with the size of the operator. A pool large enough to borrow against locked coins keeps paying on time and advertises it; a small one tells its miners to wait two weeks for work already done. If the aim is to make burst hashrate less attractive without making the chain more concentrated, that is worth looking at directly, because as written the rule has the second effect whether or not it achieves the first. One lever in these diffs separates the two. Coverage is per coinbase; if it were per output, with outputs below some threshold exempt, a coinbase that pays a hundred hashers 0.01 BTC each would be untouched and a single-output reward would not. That prices paying hashers in the coinbase, which is a real policy choice with real consequences for pool software, and it should be made on purpose rather than arrived at sideways. Method and scripts are in the gist: https://gist.github.com/jasonsopko/75e2f3e69cefcfa19911378aa4c5a54d |
|
Thanks @jasonsopko — the incidence data is the useful part, and it points at a clean fix that keeps the delay off the hasher's paycheck. Proposal: make coverage per-output with a small-output exemption, and keep this branch's height-batched schedule for the rest. A coinbase output at or below an intermediate threshold keeps ordinary 100-block maturity; larger outputs carry the extended schedule. That locks AlphaPool's single large output while leaving untouched the coinbases that pay many hashers directly (Lazarus, PYBLOCK, …), so the lock lands on burst/treasury rewards, not on miners settled in the coinbase. It's a small change on top of #404 — the check is already per-coin and the value is in the UTXO: static const int EXTENDED_COINBASE_MATURITY_LONG = 8064;
+/** Coinbase outputs <= this value keep ordinary maturity even inside the window,
+ * so coinbases that pay hashers directly are untouched and only large
+ * (treasury / single-output) rewards are locked. */
+static const CAmount EXTENDED_COINBASE_MATURITY_MIN_VALUE = 5000000; // 0.05 BTC-int Consensus::RequiredCoinbaseMaturity(int coinbase_height, int ext_start_height, int ext_expiry_height)
+int Consensus::RequiredCoinbaseMaturity(int coinbase_height, CAmount coinbase_value, int ext_start_height, int ext_expiry_height)
{
- if (coinbase_height < ext_start_height || coinbase_height >= ext_expiry_height) {
+ if (coinbase_height < ext_start_height || coinbase_height >= ext_expiry_height
+ || coinbase_value <= EXTENDED_COINBASE_MATURITY_MIN_VALUE) {
return COINBASE_MATURITY;
}and pass On the number: it's a deliberate trade-off. A higher threshold exempts more small hashers but makes split-to-dodge cheaper (a reward can be broken into just-under-threshold outputs); a lower one bites the attacker harder but can catch legitimate small payouts. 0.05 BTC is a reasonable middle to start, but it should be chosen on purpose. Happy to help test whichever value the discussion settles on. |
|
You're right, I had the asymmetry backwards. The height tranche is knowable before the work starts, while a hash residue is only readable off a solution that already meets the target — so steering by hash means discarding ~5 of every 6 valid blocks, far costlier than idling by height. Withdrawing the height-over-hash point: on that basis the residue source to consolidate on is the hash (#409). The per-output / small-output-exemption idea above is independent of which residue source is chosen. |
|
Correction to the payout-lag table in my comment above: the The script added the whole coinbase's value for every input spending it, rather than the value of the output actually being spent, so a hasher moving a 0.01 BTC payout out of a 116-output coinbase was credited a full block reward. It also attributed an entire transaction to the first pool whose coinbase it touched, and 130 of the 400 transactions in the window spend coinbases of more than one pool. Window total is 975.2 BTC, not 11194.1. Corrected, same 400 spends, heights 971373-971741, each generation output counted against the pool that created it: Sherpa at 43.8 BTC, 4.5 % of the window, was absent from the table entirely. Some medians moved with the attribution (Catbus 258 to 165, RIPTIDE 570 to 422), so read those as corrected too. The conclusion is unchanged and I would put it harder than I did. The single-output pool is not a minority of what moves: 465.8 of 975.2 BTC, 47.8 %, from the pool that found 790 of the last 2016 blocks. Its median age is 102 blocks, p90 is 105, and 98.7 % of its generation outputs are spent by block 110, into transactions paying 81 to 95 distinct addresses with a 0.01 BTC floor. It does not hold the reward for a day. @Kilombino, that is what the threshold runs into. A small-output exemption protects a hasher paid in a coinbase and does nothing for one paid out of one. AlphaPool's coinbase carries a single spendable output, so it is covered under your rule, and the ~95 people it pays a block later are paid from that covered output. They wait the full extended term either way. On these numbers the exemption spares the smaller half of the hashers and leaves the larger half where they are. It also prices the wrong behavior. Paying in the coinbase gets a pool under the threshold, costs nothing, and is what pool software already does, so the cheap dodge is not splitting a reward into just-under-threshold outputs; it is moving the payout into the coinbase, which is indistinguishable from the thing the exemption exists to protect. I do not think that sinks per-output coverage, but the exemption has to key on something other than output size to keep the delay off hashers, and I do not have a candidate that survives the same test. An output paying a hasher and an output paying a treasury look alike on-chain, and that may be the honest answer here. Method: the corrected table was rebuilt from the node alone, taking parent transactions from |
Same window and wiring as #402 (flag-day start, expires as a creation window with RDTS, mainnet/testnet start unset). Two consensus differences:
Maturity is attached to the creating height, not the spend. Coins created inside the window keep their assigned lock after RDTS expiry. New coinbases after expiry go back to 100. This is Kyle's note on Consensus: Extended coinbase maturity temporary softfork (expires with RDTS) #402: otherwise an opportunistic miner can work the tail of the window and pay almost nothing.
Depth is batched by
(height - activation) % 6so a six-week cliff does not mature on one day:Assignment is by block height. Coinbase transaction size does not change (no extra outputs, DATUM-safe).
#409 is the blockhash variant of this.
#403 is the MTP-duration variant of #402; this branch stays on block-count maturity like #402 and only changes the schedule and the expiry rule.
Wallet,
getdeploymentinfo,getblocktemplate, reorg eviction, and-reindexfollow #402. Functional test is #402's script with the two substitutions above (short tranche at 2016; window coin still rejected after expiry; post-expiry coinbase spends at 100).Regtest:
-extendedcoinbasematurity=<mtp>requires-rdtsexpiryand the start time must be less than the expiry time.