BIP9-based implementation of “speedy trial” activation specification, see https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2021-March/018583.html
Edge cases are tested by fuzzing added in #21380.
BIP9-based implementation of “speedy trial” activation specification, see https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2021-March/018583.html
Edge cases are tested by fuzzing added in #21380.
I take it that this PR doesn’t preclude also migrating to BIP8-style height based activation?
Concept ACK but would much rather have height based activation
20+ case ThresholdState::DEFINED: return "DEFINED";
21+ case ThresholdState::STARTED: return "STARTED";
22+ case ThresholdState::LOCKED_IN: return "LOCKED_IN";
23+ case ThresholdState::ACTIVE: return "ACTIVE";
24+ case ThresholdState::FAILED: return "FAILED";
25+ } // no default case, so the compiler can warn about missing cases
default
statement which does nothing but avoids compiler warning and possible errors in future?
That’s exactly what the lack of default case here does.
If a case is missed, the compiler will warn.
default
. Enums are bounded. The default state can reasonably be either ACTIVE
or FAILED
. This can be future-proofed by using a static_asset
on the enum, which documents the assumption and guarantees catching it at compile time. Warnings aren’t nearly as safe.
return ""
after the switch to execute. The only time the return value of this function is used is to print a message if a test case is already failing, so the consequences of having an empty string are trivial – CI is already failing at that point. Additionally, we bump those missing-case warnings to errors (-Werror=switch
) when configured with enable-werror and I think we have that enabled on most CI builds (all except for win64?).
Concept ACK.
It does seem like there is a preference for height based activation as opposed to time.
i’d further request that – to the extent this is getting BIP’d, that the activation date must not be within the range of the provided interval or we introduce some other sort of quantization / min interval parameter (e.g., every 3 block-months there is a new-rule activation window and we use the next one, or the activation blocks is something like current + max(2016*N, earliest - current)
.
I don’t know (though I can guess) why people are trying to refer to BIP 9 now instead of BIP 8, especially as height based activation seems the superior option.
As a reminder, there have been community IRC meetings (which many Core contributors attended and disclosed their views) where there was broad consensus on using revised BIP 8 e.g. http://gnusha.org/taproot-activation/2021-02-02.log
Rusty’s conclusion post that meeting was “Unanimous support for BIP 8. RIP BIP 9.”
The outline of the various proposals used BIP 8 rather than BIP 9: https://en.bitcoin.it/wiki/Taproot_activation_proposals
And various media articles reporting on the discussion referred to BIP 8 rather than BIP 9.
Though I don’t expect it to scupper the momentum this promising proposal is generating if people want to call this BIP 9 (or do mental gymnastics by opening a new BIP) please be clear on why it doesn’t fit within the template of BIP 8.
Other than that, I am delighted with the progress this proposal is making and having a PR up in such quick time really is great, so many thanks for your work on this.
edit: Thinking about this more I don’t think it would be mental gymnastics to open a new BIP for “Speedy trial” especially if it doesn’t fit cleanly into a more flexible BIP 8. New BIP or more flexible BIP 8. BIP 9 is dead and if “Speedy trial” could be used for other soft forks it shouldn’t be included in the Taproot BIPs.
I believe the best way to proceed would be to rebase a BIP8-height PR on top of this PR. Once this PR is merged, we have the ability to produce an acceptable set of parameters for speedy trial. If then the subsequent BIP8-height PR is merged afterwards then we get, what I believe to be, a superior set of activation parameters, but if the BIP8-height subsequent PR fails for whatever reason, that is okay.
I’m anticipating test cases added to this PR that would in turn be valuable for the BIP8-height PR, and the BIP8-height speedy trial will need an analogous activation_height
field as well.
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.
Reviewers, this pull request conflicts with the following ones:
If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.
@roconnor-blockstream that sounds like one reasonable plan of attack – merging this now is a concrete step, patching to use height can occur independently. I do fear there would not be consensus on a non-height based activation proposal, so it may be important to not rest on laurels for accepting those changes.
Making the case for BIP8 merge first then these changes, the BIP8 code is much more reviewed presently (with no code nacks, just conceptual ones?) and it may be a better utilization of developer time to rebase this onto BIP8.
I think it would be best to hear from @luke-jr and @ajtowns on their preference as they seem to be primary contributors on the implementation/review of both.
Once this PR is merged, we have the ability to produce an acceptable set of parameters for speedy trial. If then the subsequent BIP8-height PR is merged afterwards
This seems dangerous as we could have people compile and start running after the activation params are merged, merging a following change to the params could cause people to be running different activation logic (one time based, one hieght based).
@benthecarman. Not to worry this PR doesn’t have activation parameters in it. I would expect merger of activation parameters to be delayed until after a BIP8-height PR is done consideration.
That being said, I’ve chatted a bit with @achow101 and it may be the case that a BIP8-min-activation-height PR ends up being easier to implement as a stand alone PR rather than on top of this one. There are issues with merging, and backporting etc. So I’m going to stop myself from further back-seat deving and let the pros figure out the best way to hammer out PRs.
Making the case for BIP8 merge first then these changes, the BIP8 code is much more reviewed presently (with no code nacks, just conceptual ones?) and it may be a better utilization of developer time to rebase this onto BIP8.
I don’t think the BIP8 code is adequately reviewed yet – we had an off-by-one bug that would have left lot=true and lot=false nodes out of consensus found this past week, that wasn’t discovered by reviewers or the automated tests. (Currently it isn’t a clean merge either)
The main benefits of bip8 are lockinontimeout=true related – that it specifies the option at all, and that it’s height based, so loss of hashpower (due to miners preferring a chain that is invalid according to lockinontimeout=true) doesn’t reduce the number of retarget periods available for activation. So I don’t think there’s any real benefits to doing this via bip8 rather than bip9 as far as the “Speedy trial” itself goes; but if bip8/height based code is thoroughly reviewed and ready to merge prior to setting activation parameters, that seems fine too as far as I can see.
I don’t think the BIP8 code is adequately reviewed yet – we had an off-by-one bug that would have left lot=true and lot=false nodes out of consensus found this past week, that wasn’t discovered by reviewers or the automated tests. @ajtowns: Are you referring to a bug Sjors found in the tests? That is hardly a consensus bug for LOT=true and LOT=false nodes on mainnet but you are right to say it wasn’t yet ready to be merged. I have since recommended it be closed as some have expressed that they would not merge it with any LOT code (even if dead code).
Concept ACK. Thank you, @ajtowns! I’m happy to see that the change really is minimal.
Do I understand correctly from reading the code that activation will occur in the first block of the next retarget period after the activation_time
? E.g., if the minimum activation time just passed but the height is 1234, the first block where taproot’s rules are enforced still won’t be until height 2016? That seems fine to me for the sake of code simplicity, but it does mean that, if it looks like activation_time
is going to fall near a retarget block, we’ll have to communicate that taproot could either happen tomorrow or two weeks from now. Maybe to avoid that communication challenge, it’d be better to use a height?
@michaelfolkson
I don’t know (though I can guess) why people are trying to refer to BIP 9 now instead of BIP 8, especially as height based activation seems the superior option.
As mentioned in my email, I thought basing the proposal on the existing BIP9 code in Bitcoin Core (as done in this PR) would be the smallest change and so fastest to get reviewed, merged, and released. Since the goal of the Speedy Trial proposal is speed (with safety), that seems highly advantageous to me.
I completely agree that using block heights for the start
and timeout
parameters has advantages for this proposal (giving miners a known number of signaling periods) and that, as also mentioned in my email, getting BIP8 included in the code now would have advantages if Speedy Trial fails and we decide to use BIP8 as originally imagined for a follow-up activation attempt.
My preference would be for whatever solution is most preferred by reviewers.
79@@ -80,7 +80,7 @@ class CMainParams : public CChainParams {
80 consensus.nPowTargetSpacing = 10 * 60;
81 consensus.fPowAllowMinDifficultyBlocks = false;
82 consensus.fPowNoRetargeting = false;
83- consensus.nRuleChangeActivationThreshold = 1916; // 95% of 2016
84+ consensus.nRuleChangeActivationThreshold = 1815; // 90% of 2016
Concept ACK for speedy trial support. However I’m not convinced we should permanently drop the BIP9 threshold to 90%. @JeremyRubin wrote:
merging this now is a concrete step, patching to use height can occur independently
I’m fine with a height based approach too; slightly more code changes, but it seems easier to communicate when things happen. I also agree with Jeremy’s point. Nothing here is set in stone until we actually apply it to Taproot (or another soft fork).
There is one downside though, in addition to more review work: other implementations probably already have BIP 9 code ready to go. Although compared to implementing all of Taproot this is not a big deal (and without implementing Taproot they can’t verify the new consensus rules anyway).
Making the case for BIP8 merge first then these changes, the BIP8 code is much more reviewed presently (with no code nacks, just conceptual ones?)
For me that’s only an option if the LOT=true
stuff is stripped from that PR.
It doesn’t seem terribly complicated to cherry-pick 9762562cc6a2c771ad0ddfb055ea17b90085425c, bcec418304ea3dd524cb3591e4af1b77a04cc968 (though I suggest a fresh BIP for the speedy trial) and a few more.
28@@ -29,6 +29,8 @@ struct BIP9Deployment {
29 int64_t nStartTime;
30 /** Timeout/expiry MedianTime for the deployment attempt. */
31 int64_t nTimeout;
32+ /** If lock in occurs, delay activation until at least this time. */
33+ int64_t activation_time;
reduced_threshold
which, if non-zero, overrides nRuleChangeActivationThreshold
.
nRuleChangeActivationThreshold
is only used for the “the might be an unknown rule change” warning system. Each deployment now has its own threshold.
but it does mean that, if it looks like
activation_time
is going to fall near a retarget block, we’ll have to communicate that taproot could either happen tomorrow or two weeks from now.
I think it’s worse than that – if activation_time
falls very near a retarget block, I think you could have the eleven last blocks of the retarget period have timestamps (relative to activation_time
) like: -3000, -2400, -1800, -1200, -600, 0, 600, 1200, 1800, 2400, 3000
giving the last block a median time which satisfies MedianTimePast() >= activation_time
, so the next block would switch to ACTIVE and people would likely start transacting with taproot, perhaps even racing to get a tx in the first block as some did with segwit, and setting nlocktime to ensure their transaction isn’t mined early and the funds stolen.
However in that case a 2 block reorg could change the last block’s timestamp to -400
(still greater than the mediantime of the prior block which is likely -600), which would change its median time from 0
to -400
, which would then cause the new first block of the next period to still be in state LOCKED_IN. At that point replaying the taproot-using transactions people had sent prior to the reorg would allow funds to be stolen as taproot is not yet active after the reorg, and nlocktime would not have protected you.
(EDIT: fixed mediantime rules misinterpretation – blocks must have a timestamp greater than the previous block’s mediantime; block’s mediantime does include their own timestamp. And add the following…)
I think the conclusion from this is just “the height at which you transition from LOCKED_IN to ACTIVE must be fully determined as soon as you transition from STARTED to LOCKED_IN”. That way the entire LOCKED_IN period has to be reorged if you want to steal funds protected by both nlocktime and the new rules.
min_activation_height
, adds support for the unit and fuzz tests to validate behaviour, and drops the threshold from 95% to 90%.
concept ACK, I agree with @harding’s line of reasoning:
As mentioned in my email, I thought basing the proposal on the existing BIP9 code in Bitcoin Core (as done in this PR) would be the smallest change and so fastest to get reviewed, merged, and released. Since the goal of the Speedy Trial proposal is speed (with safety), that seems highly advantageous to me.
Combined with how minimal this change is- the bulk of this PR is tests, the changes in the source code are +26/-4 lines over 2 commits.
I’ve taken a first pass and it all looks reasonable, but I plan to do a full review soon. These changes are simple but I’m currently trying to gain the appropriate codebase context.
I don’t have a strong view but it appears that @achow101 in #21392 and just by reading the comments in this PR @luke-jr @benthecarman @JeremyRubin @Sjors @roconnor-blockstream @harding all have a preference for a fully height based approach over any use of MTP.
If that isn’t the case please correct me. And if anyone else has a view on entirely block height versus some limited use of MTP can you post it on IRC (##taproot-activation)?
The fuzzing PR looks very cool. On that PR @MarcoFalke also expresses an expectation that activation will be based on block heights.
edit: Some additional comments from today’s Core dev meeting
“it comes down to mtp vs block height” @achow101
“the main difference between these two PRs are using block height vs MTP " @amitiuttarwar
“with mtp, we run the risk of losing 2 signaling periods. with already few signaling periods, this has the possibility of failing to activate due to bad luck” @achow101
“the property that achow101’s PR improves is a fixed (number) of signals” @JeremyRubin
I have a preference for fully height based design, correct. But I think that given the code base as it stands today, and comparing the diffs across the PR, and the tight timeline desired from merge to release using MTP really is a decent option, especially since the bulk of the code is already more widely tested. Longer term it makes sense to fully develop and test height based – whether we do that, I don’t know. @MarcoFalke’s comment is a slight misunderstanding I believe of how the fuzzer simulates time; I recall AJ saying (somewhere) that block times are steady interval in the fuzzing so that it doesn’t matter (can’t find that comment now tho). Is that correct?
I also emphasized numerous times in the meeting that I think for the purposes of a 3 month ST, forecasting to be a mid-period start/stopheight means that drift is very unlikely to hurt us, so the notion that we’re “risking two periods” is a bit moot to me.
The counterarguments can be made that we have bigger issues to solve for if we miss two periods as a result of a hashrate decrease (where did the hashrate go – we probably wouldn’t want to activate if we’re missing a big chunk of hashrate contemporaneously anyways) and that on the flip side, a hashrate increase would yield in the block heights going by more quickly which means there is less real time to coordinate enabling signaling. There are pros and cons either way, I think that height is superior longer term, but in this case I don’t think there is sufficient technical risk to disqualify this PR as an activation method in favor of start/stop height. @luke-jr I would appreciate if you could justify your concept NACK more explicitly as I do not understand your concern. @amitiuttarwar made a great point in the meeting
my two cents: I don’t think either PR is close to being RFM, both need significantly more review. looks like #21392 is currently failing CI. If the goal is to quickly merge with safety, I think #21377 makes more sense (as I stated on the PR)
Rough consensus and running code – it seems to me that this is closer to a mergeable state. If there’s a strong reason to the contrary, or if people decide to invest the requisite energy to get #21392 reviewed and RTM before then ok.
MinActivationHeight
for WarningBitsConditionChecker
.
28@@ -29,6 +29,11 @@ struct BIP9Deployment {
29 int64_t nStartTime;
30 /** Timeout/expiry MedianTime for the deployment attempt. */
31 int64_t nTimeout;
32+ /** If lock in occurs, delay activation until at least this block
33+ * height. Note that activation will only occur on a retarget
34+ * boundary.
35+ */
36+ int min_activation_height{0};
min_activation_height
to a retarget boundary (and assert that)?
With this bip9-amended model, there is no such thing as a “max_activation_height” ? Either we reach LOCKED_IN state during the starttime/timeout range and delay activation until a fixed point or we fail back to FAILED ? Fixed point can be time-based or height-based but in anycase this is not an activation window with lower/upper bounds.
To clear this potential ambiguity, I think this variable should just be called activation_height
and the delayed semantic be documented around ThresholdState::ACTIVE as its state transition condition isn’t true anymore.
Further, consensus risks around this variable setting should be documented.
Let’s say you have client version=X reach locked_in at block height T, and activation height is scheduled for block height T+N. If a client version=Y is deployed with same activation logic but activation height M, with M < N, it will start to enforce taproot rules at height M.
Do you have a risk of chain split between X and Y as this latter client version will consider invalid-taproot block between M and N as valid ?
It’s a minimum activation height, because activation could be later than that if min_activation_height is reached prior to timeout MTP being reached. With a fast enough increase in hashrate, you could probably reach any reasonable min_activation_height prior to timeout MTP being reached; and of course min_activation_height could be deliberately set in prior to or in the middle of the signalling period.
minimum activation height is a fixed value, not a delta; so for your example, if client version X specifies min_activation_height H1 and version Y specifies min_activation_height H2 with H2<H1 then you’d expect Y to be enforcing different rules between H2..H1, because that’s what you’ve told it to do. The only time they’d agree on the rules is if lockin occurs at a height >= H1-2016.
Any change to deployment parameters can cause a change to what a client considers to be a valid block, and cause a consensus split; this isn’t any different to changing the bit, starttime, timeout, period or threshold…
It’s a minimum activation height, because activation could be later than that if min_activation_height is reached prior to timeout MTP being reached. With a fast enough increase in hashrate, you could probably reach any reasonable min_activation_height prior to timeout MTP being reached; and of course min_activation_height could be deliberately set in prior to or in the middle of the signalling period.
Right, that said if you want to achieve ST goal’s of backend delay, you should have a retarget period buffer between expected height of timeout and min_activation_height to swallow even a doubling of hashrate, or any other kind of scaling factor we deem physically unlikely to happen. Though this conversation belongs more to activation parameters selection. I agree, this logic could serve beyond ST-style of deployment.
minimum activation height is a fixed value, not a delta; so for your example, if client version X specifies min_activation_height H1 and version Y specifies min_activation_height H2 with H2<H1 then you’d expect Y to be enforcing different rules between H2..H1, because that’s what you’ve told it to do. The only time they’d agree on the rules is if lockin occurs at a height >= H1-2016.
I’m just concerned about successfully LOCKED_IN then having uasf folks bothered by the backend delay and willingly to fasten activation by tweaking min_activation_height
. I’m not sure the immutable semantic of min_activation_height
is that much understood. Though there is a thousand ways to fork yourself of the network if you have the intent, we can’t prevent all of them to happen…
Any change to deployment parameters can cause a change to what a client considers to be a valid block, and cause a consensus split; this isn’t any different to changing the bit, starttime, timeout, period or threshold…
Yes, but I think “Once activation parameters have been released for a consensus change, they MUST NOT be changed to avoid risks of consensus split until reaching a final deployment state” on top BIP9Deployment
is good practice. Future code reviewers might not be as much familiar than us w.r.t to those consensus subtleties.
I’m just concerned about successfully LOCKED_IN then having uasf folks bothered by the backend delay and willingly to fasten activation by tweaking min_activation_height
I don’t think there’s any community concerns with the backend delay. Making sure economic majority activates is in everyone’s interests.
I think this is also desirable in the future in certain circumstances, and unlikely to be an issue for ST.
56@@ -57,6 +57,7 @@ class AbstractThresholdConditionChecker {
57 virtual bool Condition(const CBlockIndex* pindex, const Consensus::Params& params) const =0;
58 virtual int64_t BeginTime(const Consensus::Params& params) const =0;
59 virtual int64_t EndTime(const Consensus::Params& params) const =0;
60+ virtual int MinActivationHeight(const Consensus::Params& params) const { return 0; }
Is there a reason that this isn’t pure virtual (when the other simple getter functions are)?
0 virtual int MinActivationHeight(const Consensus::Params& params) const =0;
1257@@ -1258,6 +1258,9 @@ static void BIP9SoftForkDescPushBack(UniValue& softforks, const std::string &nam
1258 statsUV.pushKV("possible", statsStruct.possible);
1259 bip9.pushKV("statistics", statsUV);
1260 }
1261+ if (consensusParams.vDeployments[id].min_activation_height != 0) {
1262+ bip9.pushKV("min_activation_height", consensusParams.vDeployments[id].min_activation_height);
getblockchaininfo
documentation around “height” in consequence.
min_activation_height
field; the docs for height
don’t need updating?
:+1: – may as well always return it rather than having client software infer a value from absence.
This can be handled in a follow up PR IMO (which cleans up the BIP9 naming too, since we’ve diverged)
Assuming we would like a ST-style taproot activation logic ready by April 3rd, I’m leaning toward reusing current bip9. It has been successfully exercised at least twice for csv and segwit softforks and the diff to achieve ST semantic is far smaller to focus on.
Ideally, height-based locked_in parameters would be better, as consensus rules around MTP are quite loose. AFAIU, it doesn’t prevent a coalition of miners to scale down their mined blocks nTime
to MTP + 1, thus preventing to reach a bip9 starttime at the expected real-world time. That said, if we see such behavior showing up, it would be at least a good proof of a malicious intent and ST would be a success by providing new, useful data to the softfork conversation . Beyond malicious MTP manipulation, there is a still a concern about a too-small signaling window, but AFAICT selecting starttime/timeoout in mid expected retarget period should minimize such risks.
This PR might also set a precedent to prefer refinement of existent activation logic in function of social consensus aleas. If so, we should be careful about “activation height” not introducing unintended chain split risks or permanent restriction of softfork deployment flexibility like bip34 did it w.r.t to versionbits. If you have time, maybe propose a quick amendment to bip9, formalizing might help reasoning about such technical risks ?
That said, I’ll look on competing #21392 before to focus on one or the other.
Ideally, height-based locked_in parameters would be better, as consensus rules around MTP are quite loose. AFAIU, it doesn’t prevent a coalition of miners to scale down their mined blocks
nTime
to MTP + 1, thus preventing to reach a bip9 starttime at the expected real-world time
Since it uses median time past, you’d need to be doing that for 6 out of every 11 blocks, which would require >50% of hashpower, and you’d have to take care to not manipulate the nTime of the first and last block of each retarget period so as not to make difficulty skyrocket; but you’d only need 10% of hashpower to block signalling.
Losing hashpower was the concern for MTP with bip148, and it could be a theoretical concern here too – if you lost 75% of hashpower, then instead of 14 weeks between timeout and starttime resulting in 6 or 7 retarget periods, it could result in just 1 or 2 retarget periods. But provided no new rules are added to cause blocks to be considered invalid (such as the required signalling rule in bip148), there’s no reason to expect a drastic loss of hashpower.
But as you point out, in any event, we’d learn something new, and could try again afterwards taking that new knowledge into account.
84@@ -77,14 +85,16 @@ class VersionBitsTester
85 int num;
86
87 public:
88- VersionBitsTester() : num(0) {}
89+ VersionBitsTester() : num(1000) {}
90
91 VersionBitsTester& Reset() {
92+ num = num - (num % 1000) + 1000;
In commit “tests: test versionbits delayed activation”
Add a comment: “round up to the next strictly larger multiple of 1000”?
259@@ -226,6 +260,13 @@ BOOST_AUTO_TEST_CASE(versionbits_test)
260 // Make sure that no deployment tries to set an invalid bit.
261 BOOST_CHECK_EQUAL(bitmask & ~(uint32_t)VERSIONBITS_TOP_MASK, bitmask);
262
263+ // Check min_activation_height is on a retarget boundary
264+ BOOST_CHECK_EQUAL(mainnetParams.vDeployments[i].min_activation_height % mainnetParams.nMinerConfirmationWindow, 0);
265+ // Check min_activation_height is 0 for ALWayS_ACTIVE and never active deployments
In commit “tests: test versionbits delayed activation”
Nit: WEirD CapiTAlIZatIoN
review ACK 4c7d858167a7f8cd4cb6b09787f07929d2abc3a1 🍦
Signature:
0-----BEGIN PGP SIGNED MESSAGE-----
1Hash: SHA512
2
3review ACK 4c7d858167a7f8cd4cb6b09787f07929d2abc3a1 🍦
4-----BEGIN PGP SIGNATURE-----
5
6iQGzBAEBCgAdFiEE+rVPoUahrI9sLGYTzit1aX5ppUgFAlwqrYAACgkQzit1aX5p
7pUiG6wwAsBIP2/QWvAMfor3Wt8lSqJnHmHmBzmZY0Vw2Z4QnM+qknHPBHnR9adkb
8x/G5m/gUnBGYOxMW2AJsRvooU8WiVfvxfhlZE95cjknYkTKTiirsum9dU7idbCxr
99ODwd44LwO1Y0KknerNXPyTwQ0slHvtMGGpcWgHdHHqSchrVy9aduBNSGbs+i+dh
10e2YqhcKh2cGtygdpbkDFJrpF3102hDfSr2A85CO0C1lmKpSAE1v0kjltZrshUoVY
11NPL9ap/srainn1f6dAFf1JHErLORug8UOD3T4UO8Xbf9RrRu22xEt5AYd3xumSw+
12ZRNOdtMGGgcMi3LebHOU1OewKb7SQIPMjbWs9BC6kV0vI+i4hvP6pxJLV7+kZi/r
13XXohowtItscV+oP8tmaXcvapPNISSN+DIEXiECObVdvHZ5xsVmag/dKWxQzAK1C5
14Sr1GLCD8/GiIVGyvqDIs100g4EMHs0bluj0qb2Ylmkvl+PZmMlU7TwT9GS9EuGoP
15X3ygOyma
16=xHYS
17-----END PGP SIGNATURE-----
Timestamp of file with hash be5f7748fa7afdbed03e27c4c76aa836a95078861703dc7750d0df1a9da0dbd7 -
Approach NACK.
We need to focus review effort on a single Speedy Trial PR at this stage. Although AJ has done as much as anyone to advance Taproot activation with all sorts of contributions (conceptual, code, tests, fuzzing) I am at a loss as to why to choose a PR with a mix of block height and MTP over a PR with a consistent use of block height.
I have attempted to summarize the discussion on block height versus mix of block height and MTP here. To me a consistent use of block height is a no brainer. If that needs a little more review and testing so be it. (I’m not convinced it does btw, I find it much easier to reason and think about edge cases with entirely block height than a mixture. It is a slightly larger code diff, that is true.)
I also have no idea why block height versus mix of block height and MTP has any impact whatsoever on UASF plans. It doesn’t seem to impact the difficulty of implementing a competing compatible UASF and any possible “marketing” gain for a UASF seems illusory.
Disclosure: I have been happy to work on a UASF project in the recent past when there seemed little chance of progress with Taproot activation in Core. I would also consider doing so again in future depending on the circumstances.
I hope we can choose a Speedy Trial PR as soon as possible to focus all review effort on it. If this PR is chosen over Andrew’s PR I will disagree with that decision but I will live with it. I think it will be the inferior choice (assuming a new rationale isn’t explained to me) but I don’t think it will cause any material problems with activation.
77@@ -78,16 +78,18 @@ class CMainParams : public CChainParams {
78 consensus.nPowTargetSpacing = 10 * 60;
79 consensus.fPowAllowMinDifficultyBlocks = false;
80 consensus.fPowNoRetargeting = false;
81- consensus.nRuleChangeActivationThreshold = 1916; // 95% of 2016
82+ consensus.nRuleChangeActivationThreshold = 1815; // 90% of 2016
ACK using 1815 as the constant.
1814 has lower error from 90% than 1815, but previously we used 1916 which has greater error from 95% than 1915 but is the smallest exceeds threshold number, so we continue the same precedent (not that it matters much).
What am I doing wrong that I can’t get minimum_activation_height to have any effect on regtest?
0diff --git a/src/chainparams.cpp b/src/chainparams.cpp
1index 40c58f0e67..73be5d3d6d 100644
2--- a/src/chainparams.cpp
3+++ b/src/chainparams.cpp
4@@ -412,7 +412,7 @@ public:
5 consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].bit = 2;
6 consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].nStartTime = Consensus::BIP9Deployment::ALWAYS_ACTIVE;
7 consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT;
8- consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].min_activation_height = 0; // No activation delay
9+ consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].min_activation_height = 576; // No activation delay
10
11 consensus.nMinimumChainWork = uint256{};
12 consensus.defaultAssumeValid = uint256{};
$ make
[...]
$ src/bitcoind -regtest -daemon -vbparams=taproot:1:1932252724
[...]
$ src/bitcoin-cli -regtest getblockchaininfo | jq .blocks,.softforks.taproot
0
{
"type": "bip9",
"bip9": {
"status": "defined",
"start_time": 1,
"timeout": 1932252724,
"since": 0
},
"active": false
}
I’m expecting to see a min_activation_height
field in the getblockchaininfo
results, as my reading of the code says it should be there for cases where the min activation is greater than zero.
It’d be nice if -vbparams
here took a fourth argument for the minimum activation height, similar to in #21392 , so recompiling (and whatever mistake I’m making there) wouldn’t be necessary.
-vbparams
setting happens in UpdateVersionBitsParameters
which sets it back to 0 – https://github.com/bitcoin/bitcoin/pull/21377/files#diff-ff53e63501a5e89fd650b378c9708274df8ad5d38fcffa6c64be417c4d438b6dR483
Changed from “delayed activation” to “delayed lock in” – that is, transitions go from STARTED to DELAYED to LOCKED_IN to ACTIVE, with LOCKED_IN not being entered until MTP passes min_lock_in_time. This fixes the problem noted earlier while retaining the ability to know 2016 blocks in advance when activation will occur.
Using MTP timestamps for activation retains compatible with deployments for testnet and signet, where block heights are unpredictable (for testnet) or inconsistent (different custom signets will not have common block heights).
Extends the ComputeBlockVersion unit tests to test all deployments across all chains. Adds an optional parameter to -vbparams
to allow testing, and uses that parameter in the unit tests.
Note that this approach can also be used to make bip-148/bip-91 style mandatory signalling compatible with MTP-based activation; see https://github.com/ajtowns/bitcoin/commits/202103-bip9-uasf for a branch that adds a LAST_CHANCE
signalling phased that will always be reached just prior to failure, so can be used for mandatory signalling to guarantee lock in.
Approach NACK
At this point, it seems like this PR has gotten more complicated due to MTP and reviewing this is going to be more complicated due to the edge cases around MTP. When this used a minimum activation height, I would have agreed that this would be easier and faster to review, but now that it has reverted to using a minimum (lock in) time, I am not comfortable with this approach anymore. While I agree that MTP is conceptually easier to work with, practically it has many edge cases and gotchas that I don’t think we should continue forward with using MTP. Heights are strictly easier to understand. The only edge cases to consider are off by one errors and not miners screwing with timestamps.
Using MTP timestamps for activation retains compatible with deployments for testnet and signet, where block heights are unpredictable (for testnet)
I agree that using MTP for testnet is better, but I don’t think that is important enough to block the use of heights for mainnet. I also think that testnet3 should be abandoned at this point, but that’s a discussion for another place and time.
or inconsistent (different custom signets will not have common block heights).
I think that either approach is incompatible with custom signets. I don’t think it makes sense that every signet would also have the same start time and timeout time, especially for signets that are created afterwards. It would make more sense to refactor the parameter setting and allow for the custom signets to set their own activation parameters, regardless of height or MTP.
It would make more sense to refactor the parameter setting and allow for the custom signets to set their own activation parameters, regardless of height or MTP.
wouldn’t that mean that you can never bury a deployment again because there could be some signet where it hasn’t activated yet?
Transitions go from STARTED to DELAYED to LOCKED_IN to ACTIVE, with LOCKED_IN not being entered until MTP passes min_lock_in_time. This fixes the problem noted earlier while retaining] the ability to know 2016 blocks in advance when activation will occur.
I like that. I think it does a better job of communicating to users what state the deployment is in and, by virtue of min_lock_in_time
being a time, gives a more reliable and easier to understand estimate of when activation will happen (e.g. “min_lock_in_time
+ 10 - 24 days with high confidence” instead of using heights, where it could be as much as “140 to 220 days”).
Adds an optional parameter to -vbparams to allow testing, and uses that parameter in the unit tests.
Thank you! @achow101
it seems like this PR has gotten more complicated due to MTP
Having reviewed both (as of last week), I thought this was moderately simpler than yours. I skimmed the rebase diff and don’t think the last push increases complexity (indeed, I think it simplifies it from the earlier minimum_activation_height
change.
MTP […] practically has many edge cases and gotchas [and] I don’t think we should continue forward with using MTP. Heights are strictly easier to understand.
What edge cases does MTP have for a deployment such as this where there’s no mandatory signaling or mandatory activation? MTP abuse generally assumes miner adversity (usually at least enough miners to execute a selfish mining attack, e.g. ~30%) but a mere 15% of miners opposed to Speedy Trial should be able to prevent its activation by following the allowed protocol.
Heights may be easier to reason about in some ways, but they don’t directly provide the most important assurance we want in a Speedy Trial activation: that we will have a certain minimum amount of time to get many nodes upgraded to enforce taproot when it activates. As far as I’m aware, miners screwing with time stamps—at a level less than that necessary to produce a time warp attack[1]—can only delay a deployment of this PR as written, and only at cost to the miners in the form of needing to do more PoW. I think that’s fine; we can be a little bit more patient. But for a height-based activation, miners can accelerate the deployment either non-deliberately (e.g. because BTC price increases lead to increased mining) or deliberately (again at the cost of doing more PoW); I think that’s an acceptable risk, but to my mind it’s not as good as the assurance MTP can bring us.
Again, if there’s an MTP edge case that applies to this PR that I’m not aware of, I’d certainly be interested in learning about it. Overall, I feel this latest update improves this PR to where I slightly prefer it over height-based activation (although I’m still fine with either approach).
[1] Time warp attacks break important properties of both MTP-based and height-based activation mechanisms; it also breaks important fundamental properties of Bitcoin. Given that, I don’t think we need to consider it here: I think miners know that trying a time warp attack on mainnet could easily lead to a PoW algorithm change.
wouldn’t that mean that you can never bury a deployment again because there could be some signet where it hasn’t activated yet?
Not necessarily. A deployment can be buried in one chain and not in another. With #19438, I think it would also be trivial to have a deployment buried in one chain and not in another.
What edge cases does MTP have for a deployment such as this where there’s no mandatory signaling or mandatory activation? MTP abuse generally assumes miner adversity (usually at least enough miners to execute a selfish mining attack, e.g. ~30%) but a mere 15% of miners opposed to Speedy Trial should be able to prevent its activation by following the allowed protocol.
Sure, but my main concern is about analyzing the correctness of the code around these scenarios. I am sure that the code can handle those situations correctly, but when reviewing it, it is harder to analyze and conclude that all of the state transitions that are based around MTP are safe. It is easier to do this analysis for height based deployments.
wouldn’t that mean that you can never bury a deployment again because there could be some signet where it hasn’t activated yet?
The only plausible way I’ve come up with for burying deployments on signet is via MTP – ie, declare that a soft-fork is unconditionally active on signet once the MTP passes some point, probably the MTP at which it activated on mainnet plus some delay; perhaps something like:
0constexpr uint32_t ACTIVATION_MTP_THRESHOLD = 500000000;
1bool DeploymentActiveAt(block, params, dep) {
2 int h = params.DeploymentHeight(dep);
3 return (h < ACTIVATION_MTP_THRESHOLD ? block.nHeight >= h : block.GetMedianTimePast() >= h);
4}
For custom signets that have chosen to activate the soft fork already, that’s fine, because it’s a soft fork the old blocks will still be valid even if they use rules that won’t be enforced on a -reindex
or by a new node doing IBD, and because blocks are signed, there is no risk of a reorg stealing funds. For custom signets that have not already activated the soft fork, it’s fine as long as either miners upgrade to the new software before the signet flag day activation, or as long as the soft fork is safe and miners are not doing something like -acceptnonstdtxn
so they won’t mine blocks that are invalid under the new rules.
@achow101
It would make more sense to refactor the parameter setting and allow for the custom signets to set their own activation parameters, regardless of height or MTP.
I don’t think that’s feasible from a usability standpoint: to use a custom signet you would have to define all the consensus parameters for all post-taproot forks, and do so in a way that’s exactly compatible with all the other users of that custom signet.
Using MTP-based signalling allows a signet to be upgraded to a new soft fork simply by the signet miners signalling, with no action required by users at all beyond running an updated binary, which I don’t think is achievable otherwise. (At minimum you’d need to update your config to set a “activate this deployment early” flag for each deployment on each custom signet)
Not necessarily. A deployment can be buried in one chain and not in another. With #19438, I think it would also be trivial to have a deployment buried in one chain and not in another.
The latter part of that is not true – #19438 differentiates buried and signalled deployments by data type, which is constant for all chains at compile time.
But the point of burying deployments is so that we don’t need to keep dealing with conditional activation – eg, we don’t need to support the possibility that segwit is activated but p2sh is not. We don’t want to force ourselves to maintain those code paths to support random custom signets.
301@@ -242,81 +302,123 @@ BOOST_AUTO_TEST_CASE(versionbits_test)
302 }
303 }
304
305-BOOST_AUTO_TEST_CASE(versionbits_computeblockversion)
306+void check_computeblockversion(const Consensus::Params& params, Consensus::DeploymentPos dep)
static
270 // In the first chain, test that the bit is set by CBV until it has failed.
271 // In the second chain, test the bit is set by CBV while STARTED and
272 // LOCKED-IN, and then no longer set while ACTIVE.
273 VersionBitsTester firstChain, secondChain;
274
275 // Start generating blocks before nStartTime
In commit “tests: test ComputeBlockVersion for all deployments”
Nit: comment outdated now
315
316 // Use the TESTDUMMY deployment for testing purposes.
317- int64_t bit = mainnetParams.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].bit;
318- int64_t nStartTime = mainnetParams.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].nStartTime;
319- int64_t nTimeout = mainnetParams.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].nTimeout;
320+ int64_t bit = params.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].bit;
vDeployments[Consensus::DEPLOYMENT_TESTDUMMY]
and not vDeployments[dep]
?
2463@@ -2456,6 +2464,7 @@ static void UpdateTip(CTxMemPool& mempool, const CBlockIndex* pindexNew, const C
2464 WarningBitsConditionChecker checker(bit);
2465 ThresholdState state = checker.GetStateFor(pindex, chainParams.GetConsensus(), warningcache[bit]);
2466 if (state == ThresholdState::ACTIVE || state == ThresholdState::LOCKED_IN) {
2467+ // DELAYED unreachable since min_lock_in_time = 0
In commit “versionbits: Add support for delayed lock in”
Why do we know min_lock_in_time == 0?
73@@ -73,6 +74,12 @@ ThresholdState AbstractThresholdConditionChecker::GetStateFor(const CBlockIndex*
74 pindexCount = pindexCount->pprev;
75 }
76 if (count >= nThreshold) {
77+ stateNext = (pindexPrev->GetMedianTimePast() < min_lock_in_time ? ThresholdState::DELAYED : ThresholdState::LOCKED_IN);
78+ }
79+ break;
80+ }
81+ case ThresholdState::DELAYED: {
82+ if (min_lock_in_time <= pindexPrev->GetMedianTimePast()) {
In commit “versionbits: Add support for delayed lock in”
Very nitty: use the exact same expression as above here?
0stateNext = (pindexPrev->GetMedianTimePast() < min_lock_in_time ? ThresholdState::DELAYED : ThresholdState::LOCKED_IN);
You could even use switch case fallthrough, but I don’t think that contributes to readability…
24@@ -25,7 +25,8 @@ static const int32_t VERSIONBITS_NUM_BITS = 29;
25 enum class ThresholdState {
26 DEFINED, // First state that each softfork starts out as. The genesis block is by definition in this state for each deployment.
27 STARTED, // For blocks past the starttime.
28- LOCKED_IN, // For one retarget period after the first retarget period with STARTED blocks of which at least threshold have the associated bit set in nVersion.
29+ DELAYED, // Between the first STARTED period where threshold is reached, until min_lockin_time is reached
In commit “versionbits: Add support for delayed lock in”
Maybe mention that the DELAYED state may be skipped entirely if the threshold is reached after min_lockin_time?
321+ if (exp_state == ThresholdState::STARTED) {
322+ assert(blocks_sig >= threshold);
323+ assert(current_block->GetMedianTimePast() < checker.m_end);
324+ } else {
325+ assert(exp_state == ThresholdState::DELAYED);
326+ }
In commit “fuzz: test versionbits delayed lock in”
In case case ThresholdState::FAILED
you want to check that exp_state
isn’t ThresholdState::DELAYED
either (I think the current fuzz code won’t prevent a transition DELAYED->FAILED).
I believe this is the intended state machine:
To generate:
0$ dot -Tpng >states.png
1digraph versionbits {
2 defined [shape=box,label="DEFINED"];
3 started [shape=box,label="STARTED"];
4 failed [shape=box,label="FAILED"];
5 locked [shape=box,label="LOCKED_IN"];
6 delayed [shape=box,label="DELAYED"];
7 active [shape=box,label="ACTIVE"];
8
9 defined -> defined [label="time < begin\ntime < end"];
10 defined -> started [label="time >= begin\ntime < end"];
11 defined -> failed [label="time >= end"];
12 started -> started [label="time < end\nsig < thresh"];
13 started -> failed [label="time >= end\n"];
14 started -> delayed [label="time < end\nsig >= thresh\ntime < min_lock"];
15 started -> locked [label="time < end\nsig >= thresh\ntime >= min_lock"];
16 delayed -> delayed [label="time < min_lock"];
17 delayed -> locked [label="time >= min_lock"];
18 locked -> active [label="always"];
19 active -> active [label="always"];
20 failed -> failed [label="always"];
21}
When this was updated I was originally a bit frustrated since the addition of the DELAYED state + switch to all MTP invalidates the ACKs on this PR.
However, now that I understand @ajtowns reasoning around signet compatibility, I see the merit with this approach and retract my earlier preference for height based ST. With regards to review burden, I still think this is slightly easier to reason about & a smaller diff, but I think we’ve accepted the risks of delaying the proposed release schedule in favor of creating a more robust mechanism. As a Signet Operator*, a pure MTP design should make it easier for me to activate taproot for users of my network.
My only critique of the modified design is that it might be nice to accommodate a min number of signal periods (e.g., 6) so that signets with 1 hour block rate would have sufficient time to activate. 1 hour isn’t crazy for a signet because such blocks are seen somewhat often on the network, so a long-delay signet might have utility for testing infrastructure robustness.
I don’t think this is a show-stopping issue for merging this code as-is, because the min number of signal periods functionality can be added later relatively easily without affecting any ongoing release (just default set it to 0).
* my signet is currently crashed, hope to bring it online soon when I can add disk space…
a pure MTP design should make it easier for me to activate taproot for users of my network
Note – taproot is already active on any signet; activation on signet is only a concern for any future soft forks (eg CTV).
I wonder if those first three commits should be pulled out into a separate PR?
The could be, but they aren’t terribly compatible with #21392 which stops checking ComputeBlockVersion behaviour against mainnet, and has a bunch of other changes to the ComputeBlockVersion test in order to convert from mtp to heights.
329@@ -302,17 +330,29 @@ static void check_computeblockversion(const Consensus::Params& params, Consensus
330 nHeight += 1;
331 }
332
333- nTime = nTimeout;
334- // FAILED is only triggered at the end of a period, so CBV should be setting
335- // the bit until the period transition.
336- for (uint32_t i = 0; i < params.nMinerConfirmationWindow - 1; i++) {
337+ if (nTimeout != Consensus::BIP9Deployment::NO_TIMEOUT) {
338+ // cannot reach NO_TIMEOUT a uint32_t
190@@ -151,6 +191,11 @@ class VersionBitsTester
191 VersionBitsTester& TestActive() { return TestState(ThresholdState::ACTIVE); }
192 VersionBitsTester& TestFailed() { return TestState(ThresholdState::FAILED); }
193
194+ // non-delayed should be active; delayed should still be locked in
TestActiveLockedIn
, not TestLockedInDelayed
.
I’d like to rescind my previous Approach NACK
It was given in the spirit of wanting all review to be focused on a single Speedy Trial PR (something I think should be top priority above all else) rather than two PRs, thinking an alternative PR (#21392) was better placed to be merged and personal preference too.
However, I am less sure the alternative PR stands a better chance of being merged at this point. And my personal preference for the alternative PR is not strong enough to Approach NACK this PR otherwise.
Hence, Approach ACK for this PR. My preference is #21392 but (to the extent I can) I won’t stand in the way of this PR being merged if it can be.
My only critique of the modified design is that it might be nice to accommodate a min number of signal periods (e.g., 6) so that signets with 1 hour block rate would have sufficient time to activate. 1 hour isn’t crazy for a signet because […]
I’m not sure that a 1 hour block rate isn’t crazy – nPowTargetSpacing
is still 10min, so your difficulty would keep getting automatically lowered unless you had bursts of blocks to compensate so that the average rate hits 10min every 2 weeks. If you have bursts like that to maintain difficulty anyway, you can just use them to get signalling done in a normal timeframe.
But ignoring that, I think an easy fix would be to lower signet’s nMinerConfirmationWindow
from 2016 to 288; that way normal signets can activate changes in four days instead of four weeks; and even a 1-hour-per-block signet that wasn’t willing to do bursts wouldn’t need to to take much longer than a month either (<12 days to hit a 288-block boundary, ~12 days to signal, ~12 days to lock in, so <36 days total).
@ajtowns i thought you could set arbitrary block rates for signet?
The only signet consensus parameter you can change (without everyone recompiling) is the block challenge. The old miner script used to let you target a block rate, but that would have an impact on difficulty that might be annoying to manage, so the new one lets you target a difficulty (--nbits
) and adjusts the block rate to match; when you hit your difficulty target, it’ll just average out at ~10min per block.
I believe this is the intended state machine:
Yeah. It’s somewhat more complicted than it could be because it’s maintaining some of the complexities of the bip9 state machine. It could be simplified a bit by dropping the DEFINED->FAILED case and not ignoring signalling from the last STARTED period:
Excepting the addition of the DELAYED state, that pretty much makes it the same as the corresponding bip8 diagram, as bip8 has already made those changes compared to bip9.
0digraph versionbits {
1 defined [shape=box,label="DEFINED"];
2 started [shape=box,label="STARTED"];
3 failed [shape=box,label="FAILED"];
4 locked [shape=box,label="LOCKED_IN"];
5 delayed [shape=box,label="DELAYED"];
6 active [shape=box,label="ACTIVE"];
7
8 delayorlock [shape=circle,label=""];
9
10 edge [weight=1];
11 defined -> defined [label="time < begin"];
12 started -> started [label="nsig < thresh\ntime < end"];
13 active -> active [label="always"];
14 failed -> failed [label="always"];
15 delayed -> delayorlock [label="always"];
16
17 edge [weight=100];
18 defined -> started [label="time >= begin"];
19 started -> failed [label="nsig < thresh\ntime >= end\n"];
20
21 started -> delayorlock [label="nsig >= thresh"];
22 delayorlock -> delayed [label="time < min_lock"];
23 delayorlock -> locked [label="time >= min_lock"];
24
25 locked -> active [label="always"];
26
27 { rank=same; delayed delayorlock }
28}
28@@ -29,6 +29,11 @@ struct BIP9Deployment {
29 int64_t nStartTime;
30 /** Timeout/expiry MedianTime for the deployment attempt. */
31 int64_t nTimeout;
32+ /** Delay lock in occuring until at least this timestamp.
Should this say median time not a block timestamp (nTime
) ?
IIUC correctly the problem mentions here is solved for the following rational. If a reorg occurs across a retarget period with new block timestamps rolling back the median time, the LOCKED_IN period serves a safety buffer until ACTIVATION ? If so that would be great to document it somewhere.
min_lock_in_time
DEFINED
->FAILED
without passing through STARTED
in @sipa’s state machine here: #21377 (comment) Seems only possible if “end” is before “begin”?
@ajtowns drops it here (mentions it is due to BIP9?) #21377 (comment)
Should STARTED
-> delayorlock
be defined as “nsig >= thresh & time <= end” ?
If it is currently not possible to bury/flag-day activate/vb signal at different heights/times in signet, it seems that this should be made possible via command line options, not by preferring MTP over height based vb code (or the other way round).
Also, I fail to see how MTP solves an issue that height based can’t. Let’s assume the signet default params for deployment mention a start and end parameter for a soft-fork, but someone wants to start a signet with the softfork always active. This can only be fixed by supplying modified source/binaries or by allowing the users/miners to pick the vbparams at runtime. It can’t be fixed by using MTP over height (or the other way round).
the benefit of MTP is not needing to disseminate additional parameters per-signet for activation. If you upgrade to the latest bitcoin-core with a new Soft fork deployment, your existing config file (with whatever signetchallenge) will be able to activate without further configuration.
this is not possible with height.
this is not possible with height.
Is it really that much work to check the place where you copied the signetchallenge to also copy the vbparams? You’ll have to do that anyway, otherwise you won’t know if your signet picked always active, flag day, or signalling activation.
apologies, I just am not going to be awake for a 3am IRC meeting my time, so dropping my feedback here:
I’m unconvinced by the arguments set out here for MTP: the one thing you do get is testnet being an activation testing ground for the MTP-based implementation. Other than that it’s a wash, or it seems to be a net negative for clarity and testing.
If signets require a little more config data to be passed around, so be it. We should not be tying mainnet safety to configs of these networks. Based on my years of experience any proper operation of a “non-official” signet-like chain will be carting around a config file anyways.
fwiw I’m deferring proper review of a PR until I think we can approach consensus on this topic. I think this can be solved provided the review burden of both is similar in magnitude.
Also, I fail to see how MTP solves an issue that height based can’t. Let’s assume the signet default params for deployment mention a start and end parameter for a soft-fork, but someone wants to start a signet with the softfork always active. This can only be fixed by supplying modified source/binaries or by allowing the users/miners to pick the vbparams at runtime. It can’t be fixed by using MTP over height (or the other way round).
The issue for signet that MTP signalling solves that height based signalling doesn’t is allowing multiple signets to operate with the same fixed-at-compile-time parameters. That avoids having to have every user of every signet add custom command line parameters that will change over the lifetime of the signet (as additional soft forks are deployed/activated).
With MTP signalling, if you want a non-buried soft fork “always active” on a new signet, you create the signet and mine 3 periods worth of blocks, signalling in the second period – no need for anyone to set vbparams. (For a soft fork that’s buried via MTP, you simply need to get MTP past the burial MTP, so likely only need to mine two or three blocks if you’re creating a new signet for an already buried fork)
If you’re adding height-based params to signetchallenge, you’re making the challenge incompatible with existing nodes (that obviously don’t support parsing and removing the vbparams from the challenge), but you’re also requiring the challenge to change with each new soft fork: you’ll need to add ‘anyprevout=height’ one day, then ‘ctv=height’ some other day, then ‘consensuscleanup=height’ some other day, and can’t remove any of them, even after they’re buried on mainnet. And you’re also providing an extra way for people to make consensus-breaking mistakes, in hard to detect ways – currently if you get the signet challenge wrong you’ll just end up having a different network magic and not connect to the signet at all, but with optional parameters added you can’t do the same, since you want to be backwards compatible by allowing the optional values to be missing. @Rspigler
I don’t think signet should be a consideration for MTP vs height, since taproot is already activated on signet, and there’s no indication that ST will be used in the future.
If we’re not trying to use this as a stepping stone for future activations, then it would make sense to keep the changes for ST as simple as possible, and sticking with MTP is the smaller and more easily backportable set of changes…
Even restricting the analysis purely to the effect on mainnet, and ignoring testing, implementation or backporting complexity, I think MTP is superior; if it were blocking the ability to reasonably do a UASF then that would still be a showstopper, but as per the LAST_CHANCE comments above, I don’t see any reason why that would be the case.
I think that the min_lock_in_time
should be reverted to min_activation_height
. As noted on the mailing list, min_lock_in_time
adds an additional 14 day uncertainty to the true activation time, whereas predicting the time for min_activation_height
to be reached only gets better as that height approaches. A good enough prediction of the actual activation time can be known several days or weeks ahead. With min_lock_in_time
, miners could also manipulate timestamps to introduce uncertainty in the activation time.
Although I think that block heights is technically superior, many of the specific things that it guarantees are still provided by an MTP implementation probabilistically and some changes can be made to provide the same guarantees. With that in mind, I would like to see that the DEFINED->FAILED
transition is removed and the unconditional STARTED->FAILED
when the timeout is reached. This would make it more like BIP 8 where at least one signaling period is guaranteed, and if the timeout falls in the middle of a signaling period, then that period will still count towards the activation.
Although I think that block heights is technically superior, many of the specific things that it guarantees are still provided by an MTP implementation probabilistically and some changes can be made to provide the same guarantees. With that in mind, I would like to see that the DEFINED->FAILED transition is removed and the unconditional STARTED->FAILED when the timeout is reached. This would make it more like BIP 8 where at least one signaling period is guaranteed, and if the timeout falls in the middle of a signaling period, then that period will still count towards the activation. @achow101 would the LAST_CHANCE https://github.com/ajtowns/bitcoin/commits/202103-bip9-uasf solution work for the guaranteed at least 1 signal period?
@achow101 would the LAST_CHANCE https://github.com/ajtowns/bitcoin/commits/202103-bip9-uasf solution work for the guaranteed at least 1 signal period?
Originally this paragraph suggested that, but @ajtowns pointed out that changing these state transitions achieves the same effect. I prefer changing these transitions rather than adding a new state as these were part of BIP 8 and are thus already (partially) understood. However I think it is important to add LAST_CHANCE later since it enables a UASF.
Updates as per achow101’s suggestions – switches back from fully-mtp-based to using min_activation_height, and drops bip9’s “fail immediately when timeout passes” handling (also adds a NEVER_ACTIVE special case, since the lack of an immediate failure mode means you can’t just use a super-early timeout for that purpose anymore).
I believe this should capture the updated state machine:
[EDIT: note that these all map attributes of the last block in a period to the activation state of all blocks in the next period. Replace “time” by “previous block’s time” and “height + 1” by “height” to map attributes of the first block in a period to the activation state of all blocks in the same period]
I believe this backports to 0.21 cleanly (on top of #21614 anyway).
(CI is currently timing out on the fuzzer run)
81 }
82 case ThresholdState::LOCKED_IN: {
83- // Always progresses into ACTIVE.
84- stateNext = ThresholdState::ACTIVE;
85+ // Progresses into ACTIVE provided activation height will have been reached.
86+ if (pindexPrev->nHeight + 1 >= min_activation_height) {
pindexPrev->nHeight + 1
is used so we measure the current block height, and not the previous block’s height
I :+1: the sentiment that it’s confusing, but think it’s clear enough in this case.
In #21392 there were a lot of examples of pindexPrev->nHeight + 1 < min_activation_height
which I wanted documented better. Despite being the negation, is a lot harder to understand when the transition was happening because it was in states with multiple possible transitions, and the clause was guarding a state transition that becomes impossible rather than one that becomes possible. As such, even if this code contained an off-by-one error in the logic, it would only result in one additional period of LOCKED_IN as opposed to a failure to activate.
That said, there’s no off-by-one error here AFAICT :)
516 if (!ParseInt64(vDeploymentParams[2], &nTimeout)) {
517 throw std::runtime_error(strprintf("Invalid nTimeout (%s)", vDeploymentParams[2]));
518 }
519+ if (vDeploymentParams.size() >= 4 && !ParseInt32(vDeploymentParams[3], &min_activation_height)) {
520+ throw std::runtime_error(strprintf("Invalid min_activation_height (%s)", vDeploymentParams[3]));
521+ }
In aa6a95de12b265cd3dc16b2d1303b89a20331a56 “versionbits: Add support for delayed activation after lockin”
This could also check that all of the parameters are valid. That would be >=0
for all of them and nStartTime
and nTimeout
are either -1
(or -2
when it is introduced). And that min_activation_height
falls on a retarget boundary.
>= 4
test misleading, implying it could be 5+. Obviously the semantics here are identical considering the entire block of code as a whole, but I wonder if the ambiguity could be reduced.
321@@ -323,7 +322,7 @@ FUZZ_TARGET_INIT(versionbits, initialize)
322 assert(exp_state == ThresholdState::ACTIVE || exp_state == ThresholdState::LOCKED_IN);
323 break;
324 case ThresholdState::FAILED:
325- assert(current_block->GetMedianTimePast() >= checker.m_end);
326+ assert(never_active_test || current_block->GetMedianTimePast() >= checker.m_end);
In c7008a1a07ae8bd8dfb9c2001a4fdef6e08eb395 “versionbits: Add explicit NEVER_ACTIVE deployments”
Could add assert(!never_active_test)
in case ThresholdState::STARTED
.
The if (never_active_test)
block at the end asserts both state
and exp_state
are always FAILED so that’s already covered (likewise for always_active_test
).
(This is just a probably-too-clever way of writing if (!never_active_test) { assert(MTP >= m_end); }
)
448+ {
449+ // Use regtest/testdummy to ensure we always exercise the
450+ // min_lock_in_time test, even if we're not using that in a
451+ // live deployment
452+ ArgsManager args;
453+ args.ForceSetArg("-vbparams", "testdummy:1199145601:1230767999:403200"); // January 1, 2008 - December 31, 2008, min act height 403200
In e94fc819d8a996ce3690274c307c537329dfb95b “tests: test versionbits delayed activation”
How come min_activation_height
is set so high? By my calculation, it only needs to be at least 576.
ACK be7d1368d8e98b6f9f692289b7d25ac369bf2984
All of the changes that I suggested have been implemented.
313+
314+ // Mine more blocks (4 less than the adjustment period) at the old time, and check that CBV isn't setting the bit yet.
315+ for (uint32_t i = 1; i < params.nMinerConfirmationWindow - 4; i++) {
316+ lastBlock = firstChain.Mine(params.nMinerConfirmationWindow + i, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
317+ // This works because VERSIONBITS_LAST_OLD_BLOCK_VERSION happens
318+ // to be 4, and the bit we're testing happens to be bit 28.
In commit “tests: test ComputeBlockVersion for all deployments”
I don’t understand this comment. (1) in this generalized check for all deployments, the “bit we’re testing happens to be bit 28” isn’t the case; (2) the exact comparison on the next line is run several times prior to here without this comment. Was there maybe an additional check here that got deleted?
84@@ -77,14 +85,18 @@ class VersionBitsTester
85 int num;
86
87 public:
88- VersionBitsTester() : num(0) {}
num
never modified before?
num++
at the end of TestState
and TestStateSinceHeight
utACK https://github.com/bitcoin/bitcoin/pull/21377/commits/be7d1368d8e98b6f9f692289b7d25ac369bf2984
logic appears to match the latest state machine image, read through all the various discussions on the design choices, agree that the current state machine in the PR is much better than previous iterations.
Will do some follow-on testing soon.
279+static void check_computeblockversion(const Consensus::Params& params, Consensus::DeploymentPos dep)
280 {
281+ // This implicitly uses versionbitscache, so clear it every time
282+ versionbitscache.Clear();
283+
284 // Check that ComputeBlockVersion will set the appropriate bit correctly
304+ if (nStartTime == Consensus::BIP9Deployment::NEVER_ACTIVE) return;
305+
306+ BOOST_REQUIRE(nStartTime < nTimeout);
307+ BOOST_REQUIRE(nStartTime >= 0);
308+ BOOST_REQUIRE(nTimeout <= std::numeric_limits<uint32_t>::max() || nTimeout == Consensus::BIP9Deployment::NO_TIMEOUT);
309+ BOOST_REQUIRE(0 <= bit && bit < 32);
Any reason not to make this more tightly bounded:
0 BOOST_REQUIRE(0 <= bit && bit < 29);
or even:
0 BOOST_REQUIRE(0 <= bit && bit < VERSIONBITS_NUM_BITS);
It’s already checked via BOOST_REQUIRE(((1 << bit) & VERSIONBITS_TOP_MASK) == 0);
– the “requires” are there because the later checks will break if these aren’t true; if it were possible for bit
to be NUM_BITS
or greater, but not clash with TOP_MASK
the other tests would be fine.
(I had also thought versionbits_sanity
was checking the bit was in [0, NUM_BITS)
so didn’t “duplicate” the test here, but it’s not actually doing that – perhaps I saw that in some patch implementing BIP320)
Noting that this check is required before computing on the bit (https://en.cppreference.com/w/cpp/language/operator_arithmetic):
In any case, if the value of the right operand is negative or is greater or equal to the number of bits in the promoted left operand, the behavior is undefined.
This also impacts where, if taken, https://github.com/bitcoin/bitcoin/pull/21377/commits/754ab11f2085a0c5e417cfbba80cd024fc8ea67a#r609525131 should be created.
305+
306+ BOOST_REQUIRE(nStartTime < nTimeout);
307+ BOOST_REQUIRE(nStartTime >= 0);
308+ BOOST_REQUIRE(nTimeout <= std::numeric_limits<uint32_t>::max() || nTimeout == Consensus::BIP9Deployment::NO_TIMEOUT);
309+ BOOST_REQUIRE(0 <= bit && bit < 32);
310+ BOOST_REQUIRE(((1 << bit) & VERSIONBITS_TOP_MASK) == 0);
1 << bit
(or 1<<bit
) is used all over this test. Consider making an alias for readability:
0 uint32_t deployment_mask{1 << bit};
318
319- // Start generating blocks before nStartTime
320- int64_t nTime = nStartTime - 1;
321+ int64_t nTime = nStartTime;
322+
323+ const CBlockIndex *lastBlock = nullptr;
If you’re touching this:
0 const CBlockIndex* lastBlock = nullptr;
360+
361+ // Mine more blocks (4 less than the adjustment period) at the old time, and check that CBV isn't setting the bit yet.
362+ for (uint32_t i = 1; i < params.nMinerConfirmationWindow - 4; i++) {
363+ lastBlock = firstChain.Mine(params.nMinerConfirmationWindow + i, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
364+ // This works because VERSIONBITS_LAST_OLD_BLOCK_VERSION happens
365+ // to be 4, and the bit we're testing happens to be bit 28.
“the bit we’re testing happens to be bit 28.”
Not true!
370+ nTime = nStartTime;
371+ for (uint32_t i = params.nMinerConfirmationWindow - 4; i <= params.nMinerConfirmationWindow; i++) {
372+ lastBlock = firstChain.Mine(params.nMinerConfirmationWindow + i, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
373+ BOOST_CHECK_EQUAL(ComputeBlockVersion(lastBlock, params) & (1<<bit), 0);
374+ }
375+ // Next we will advance to the next period and transition to STARTED,
params.nMinerConfirmationWindow * 3 - 1
here and verify that we stay in DEFINED, and then the next block (below) transitions us to STARTED.
312+ BOOST_REQUIRE_EQUAL(min_activation_height % params.nMinerConfirmationWindow, 0);
313
314 // In the first chain, test that the bit is set by CBV until it has failed.
315 // In the second chain, test the bit is set by CBV while STARTED and
316 // LOCKED-IN, and then no longer set while ACTIVE.
317 VersionBitsTester firstChain, secondChain;
VersionBitsTester
objects, which have their own activation parameters, but the thing that’s actually being tested is the global versionbitscache
state. In fact, these VersionBitsTester
objects are only being used for their Mine()
method. I think this code would be a lot clearer if that was pulled out into a utility function.
344+ if (nTime == 0) {
345+ // since CBlockIndex::nTime is uint32_t we can't represent any
346+ // earlier time, so will transition from DEFINED to STARTED at the
347+ // end of the first period by mining blocks at nTime == 0
348+ lastBlock = firstChain.Mine(params.nMinerConfirmationWindow - 1, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
349+ BOOST_CHECK_EQUAL(ComputeBlockVersion(lastBlock, params) & (1<<bit), 0);
Perhaps out of scope here, but I find that the bit masking in these checks is a little difficult to read and detracts from the intent of the test by emphasizing the syntax rather than the meaning. May be worth pulling out the checks into a utility:
0diff --git a/src/test/versionbits_tests.cpp b/src/test/versionbits_tests.cpp
1index 5f53da8aff..f952935e58 100644
2--- a/src/test/versionbits_tests.cpp
3+++ b/src/test/versionbits_tests.cpp
4@@ -275,6 +275,31 @@ BOOST_AUTO_TEST_CASE(versionbits_sanity)
5 }
6 }
7
8+static bool check_bit_set(const CBlockIndex* lastBlock, const Consensus::Params& params, int64_t bit)
9+{
10+ if ((ComputeBlockVersion(lastBlock, params) & (1 << bit)) == 0) {
11+ BOOST_ERROR("version bit not set!");
12+ return false;
13+ }
14+ if ((ComputeBlockVersion(lastBlock, params) & VERSIONBITS_TOP_MASK) != VERSIONBITS_TOP_BITS) {
15+ BOOST_ERROR("versionbits top mask not set!");
16+ return false;
17+ }
18+ return true;
19+}
20+
21+static bool check_bit_unset(const CBlockIndex* lastBlock, const Consensus::Params& params, int64_t bit)
22+{
23+ if ((ComputeBlockVersion(lastBlock, params) & (1 << bit)) != 0) {
24+ BOOST_ERROR("version bit set!");
25+ return false;
26+ }
27+ if ((ComputeBlockVersion(lastBlock, params) & VERSIONBITS_TOP_MASK) != VERSIONBITS_TOP_BITS) {
28+ BOOST_ERROR("versionbits top mask not set!");
29+ return false;
30+ }
31+ return true;
32+}
33 static void check_computeblockversion(const Consensus::Params& params, Consensus::DeploymentPos dep)
34 {
35 // This implicitly uses versionbitscache, so clear it every time
36@@ -318,9 +343,10 @@ static void check_computeblockversion(const Consensus::Params& params, Consensus
37 // earlier time, so will transition from DEFINED to STARTED at the
38 // end of the first period by mining blocks at nTime == 0
39 lastBlock = firstChain.Mine(params.nMinerConfirmationWindow - 1, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
40- BOOST_CHECK_EQUAL(ComputeBlockVersion(lastBlock, params) & (1<<bit), 0);
41+ BOOST_CHECK(check_bit_unset(lastBlock, params, bit));
42+
43 lastBlock = firstChain.Mine(params.nMinerConfirmationWindow, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
44- BOOST_CHECK((ComputeBlockVersion(lastBlock, params) & (1<<bit)) != 0);
45+ BOOST_CHECK(check_bit_set(lastBlock, params, bit));
46 // then we'll keep mining at nStartTime...
47 } else {
48 // use a time 1s earlier than start time to check we stay DEFINED
49@@ -328,30 +354,28 @@ static void check_computeblockversion(const Consensus::Params& params, Consensus
50
51 // Start generating blocks before nStartTime
52 lastBlock = firstChain.Mine(params.nMinerConfirmationWindow, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
53- BOOST_CHECK_EQUAL(ComputeBlockVersion(lastBlock, params) & (1<<bit), 0);
54+ BOOST_CHECK(check_bit_unset(lastBlock, params, bit));
55
56 // Mine more blocks (4 less than the adjustment period) at the old time, and check that CBV isn't setting the bit yet.
57 for (uint32_t i = 1; i < params.nMinerConfirmationWindow - 4; i++) {
58 lastBlock = firstChain.Mine(params.nMinerConfirmationWindow + i, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
59 // This works because VERSIONBITS_LAST_OLD_BLOCK_VERSION happens
60 // to be 4, and the bit we're testing happens to be bit 28.
61- BOOST_CHECK_EQUAL(ComputeBlockVersion(lastBlock, params) & (1<<bit), 0);
62+ BOOST_CHECK(check_bit_unset(lastBlock, params, bit));
63 }
64 // Now mine 5 more blocks at the start time -- MTP should not have passed yet, so
65 // CBV should still not yet set the bit.
66 nTime = nStartTime;
67 for (uint32_t i = params.nMinerConfirmationWindow - 4; i <= params.nMinerConfirmationWindow; i++) {
68 lastBlock = firstChain.Mine(params.nMinerConfirmationWindow + i, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
69- BOOST_CHECK_EQUAL(ComputeBlockVersion(lastBlock, params) & (1<<bit), 0);
70+ BOOST_CHECK(check_bit_unset(lastBlock, params, bit));
71 }
72 // Next we will advance to the next period and transition to STARTED,
73 }
74
75 lastBlock = firstChain.Mine(params.nMinerConfirmationWindow * 3, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
76 // so ComputeBlockVersion should now set the bit,
77- BOOST_CHECK((ComputeBlockVersion(lastBlock, params) & (1<<bit)) != 0);
78- // and should also be using the VERSIONBITS_TOP_BITS.
79- BOOST_CHECK_EQUAL(ComputeBlockVersion(lastBlock, params) & VERSIONBITS_TOP_MASK, VERSIONBITS_TOP_BITS);
80+ BOOST_CHECK(check_bit_set(lastBlock, params, bit));
81
82 // Check that ComputeBlockVersion will set the bit until nTimeout
83 nTime += 600;
84@@ -360,8 +384,7 @@ static void check_computeblockversion(const Consensus::Params& params, Consensus
85 // These blocks are all before nTimeout is reached.
86 while (nTime < nTimeout && blocksToMine > 0) {
87 lastBlock = firstChain.Mine(nHeight+1, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
88- BOOST_CHECK((ComputeBlockVersion(lastBlock, params) & (1<<bit)) != 0);
89- BOOST_CHECK_EQUAL(ComputeBlockVersion(lastBlock, params) & VERSIONBITS_TOP_MASK, VERSIONBITS_TOP_BITS);
90+ BOOST_CHECK(check_bit_set(lastBlock, params, bit));
91 blocksToMine--;
92 nTime += 600;
93 nHeight += 1;
94@@ -375,7 +398,7 @@ static void check_computeblockversion(const Consensus::Params& params, Consensus
95 // finish the last period before we start timing out
96 while (nHeight % params.nMinerConfirmationWindow != 0) {
97 lastBlock = firstChain.Mine(nHeight+1, nTime - 1, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
98- BOOST_CHECK((ComputeBlockVersion(lastBlock, params) & (1<<bit)) != 0);
99+ BOOST_CHECK(check_bit_set(lastBlock, params, bit));
100 nHeight += 1;
101 }
102
103@@ -383,12 +406,12 @@ static void check_computeblockversion(const Consensus::Params& params, Consensus
104 // the bit until the period transition.
105 for (uint32_t i = 0; i < params.nMinerConfirmationWindow - 1; i++) {
106 lastBlock = firstChain.Mine(nHeight+1, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
107- BOOST_CHECK((ComputeBlockVersion(lastBlock, params) & (1<<bit)) != 0);
108+ BOOST_CHECK(check_bit_set(lastBlock, params, bit));
109 nHeight += 1;
110 }
111 // The next block should trigger no longer setting the bit.
112 lastBlock = firstChain.Mine(nHeight+1, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
113- BOOST_CHECK_EQUAL(ComputeBlockVersion(lastBlock, params) & (1<<bit), 0);
114+ BOOST_CHECK(check_bit_unset(lastBlock, params, bit));
115 }
116
117 // On a new chain:
118@@ -399,25 +422,26 @@ static void check_computeblockversion(const Consensus::Params& params, Consensus
119 // Mine one period worth of blocks, and check that the bit will be on for the
120 // next period.
121 lastBlock = secondChain.Mine(params.nMinerConfirmationWindow, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
122- BOOST_CHECK((ComputeBlockVersion(lastBlock, params) & (1<<bit)) != 0);
123+ BOOST_CHECK(check_bit_set(lastBlock, params, bit));
124
125 // Mine another period worth of blocks, signaling the new bit.
126 lastBlock = secondChain.Mine(params.nMinerConfirmationWindow * 2, nTime, VERSIONBITS_TOP_BITS | (1<<bit)).Tip();
127 // After one period of setting the bit on each block, it should have locked in.
128 // We keep setting the bit for one more period though, until activation.
129- BOOST_CHECK((ComputeBlockVersion(lastBlock, params) & (1<<bit)) != 0);
130+ BOOST_CHECK(check_bit_set(lastBlock, params, bit));
131
132 // Now check that we keep mining the block until the end of this period, and
133 // then stop at the beginning of the next period.
134 lastBlock = secondChain.Mine((params.nMinerConfirmationWindow * 3) - 1, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
135- BOOST_CHECK((ComputeBlockVersion(lastBlock, params) & (1 << bit)) != 0);
136+ BOOST_CHECK(check_bit_set(lastBlock, params, bit));
137+
138 lastBlock = secondChain.Mine(params.nMinerConfirmationWindow * 3, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
139
140 if (lastBlock->nHeight + 1 < min_activation_height) {
141 lastBlock = secondChain.Mine(min_activation_height, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
142 }
143
144- BOOST_CHECK_EQUAL(ComputeBlockVersion(lastBlock, params) & (1<<bit), 0);
145+ BOOST_CHECK(check_bit_unset(lastBlock, params, bit));
146 }
434 }
435- // The next block should trigger no longer setting the bit.
436- lastBlock = firstChain.Mine(nHeight+1, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
437- BOOST_CHECK_EQUAL(ComputeBlockVersion(lastBlock, mainnetParams) & (1<<bit), 0);
438
439 // On a new chain:
452+ check_computeblockversion(chainParams->GetConsensus(), Consensus::DEPLOYMENT_TESTDUMMY);
453+ }
454+
455+ {
456+ // Use regtest/testdummy to ensure we always exercise the
457+ // min_lock_in_time test, even if we're not using that in a
0 // min_activation_height test, even if we're not using that in a
43@@ -44,6 +44,12 @@ class TestConditionChecker : public AbstractThresholdConditionChecker
44 int GetStateSinceHeightFor(const CBlockIndex* pindexPrev) const { return AbstractThresholdConditionChecker::GetStateSinceHeightFor(pindexPrev, paramsDummy, cache); }
45 };
46
47+class TestDelayedActivationConditionChecker : public TestConditionChecker
48+{
49+public:
50+ int MinActivationHeight(const Consensus::Params& params) const override { return 15000; }
MinActivationHeight()
in the base TestConditionChecker
class?
TestConditionChecker
and MinActivationHeight
is not is that MinActivationHeight
is defined to have a sane default function, whereas the others are not.
ConditionChecker
to not be abstract and just decode the Consensus::Params
structure directly, so that tests just pass in different data, but are running the same chunks of code. Leaving this as-is until later refactoring either way though.
83@@ -77,14 +84,18 @@ class VersionBitsTester
84 int num;
85
86 public:
87- VersionBitsTester() : num(0) {}
88+ VersionBitsTester() : num(1000) {}
89
90 VersionBitsTester& Reset() {
91+ // Have each group of tests be counted by the 1000s part, starting at 1000
92+ num = num - (num % 1000) + 1000;
test_group
and test_index
rather than have some magic 1000 value.
83@@ -77,14 +84,18 @@ class VersionBitsTester
84 int num;
85
86 public:
87- VersionBitsTester() : num(0) {}
88+ VersionBitsTester() : num(1000) {}
num
to have default initialization and remove the empty ctor.
424+ lastBlock = secondChain.Mine((params.nMinerConfirmationWindow * 3) - 1, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
425+ BOOST_CHECK((ComputeBlockVersion(lastBlock, params) & (1 << bit)) != 0);
426+ lastBlock = secondChain.Mine(params.nMinerConfirmationWindow * 3, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
427+
428+ if (lastBlock->nHeight + 1 < min_activation_height) {
429+ lastBlock = secondChain.Mine(min_activation_height, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
min_act_height - 1
still signals.
234@@ -214,10 +235,15 @@ BOOST_AUTO_TEST_CASE(versionbits_test)
235 .Mine(3000, TestTime(10000), 0).TestStarted().TestStateSinceHeight(3000)
236 .Mine(4000, TestTime(10000), 0).TestStarted().TestStateSinceHeight(3000)
237 .Mine(5000, TestTime(10000), 0).TestStarted().TestStateSinceHeight(3000)
238+ .Mine(5999, TestTime(20000), 0).TestStarted().TestStateSinceHeight(3000)
239 .Mine(6000, TestTime(20000), 0).TestFailed().TestStateSinceHeight(6000)
240- .Mine(7000, TestTime(20000), 0x100).TestFailed().TestStateSinceHeight(6000);
241+ .Mine(8000, TestTime(20000), 0x100).TestFailed().TestStateSinceHeight(6000)
Seems reasonable, although it may be worth explicitly commenting that:
0diff --git a/src/test/versionbits_tests.cpp b/src/test/versionbits_tests.cpp
1index 5f53da8aff..19bf3f948f 100644
2--- a/src/test/versionbits_tests.cpp
3+++ b/src/test/versionbits_tests.cpp
4@@ -237,7 +237,8 @@ BOOST_AUTO_TEST_CASE(versionbits_test)
5 .Mine(5000, TestTime(10000), 0).TestStarted().TestStateSinceHeight(3000)
6 .Mine(5999, TestTime(20000), 0).TestStarted().TestStateSinceHeight(3000)
7 .Mine(6000, TestTime(20000), 0).TestFailed().TestStateSinceHeight(6000)
8- .Mine(8000, TestTime(20000), 0x100).TestFailed().TestStateSinceHeight(6000)
9+ .Mine(7000, TestTime(20000), 0x100).TestFailed().TestStateSinceHeight(6000)
10+ .Mine(8000, TestTime(20000), 0x100).TestFailed().TestStateSinceHeight(6000) // Make sure we don't transition out of FAILED
11 ;
12 }
13 }
Reviewed everything. All the changes look good to me. utACK be7d1368d8e98b6f9f692289b7d25ac369bf2984.
I’ve left a bunch of style suggestions inline. Nothing critical, so feel free to ignore.
Thank you for persevering with this, @ajtowns, and for all of the prerequisite work that you’ve done over the last couple of years to improve testing and develop activation proposals. Thanks also to @achow101 and all other reviewers whose suggestions have improved and strengthened this code.
From @maaku on using block height versus a mix of block height and MTP. Understandably he doesn’t want to be involved.
“Segwit activation exposed a number of problems with the BIP9 activation state machine which was based on MTP windows. These concerns include:
Difficulty of interfacing with in-parallel activation mechanisms like BIP148, BIP91, or the presumed future LOT=true UASF for taproot. (You may not like UASF. You may be against UASF and wish is never happened. Well reality doesn’t care. There will be a UASF client, that is simply a fact. That cat is out of the bag. Our job is to make sure that UASF risks are minimized for the sake of the network, which height-based activation mechanisms do better. I’m not saying MTP can’t be UASF compatible, I’m just saying it’s way easier to avoid foot-gunning a UASF deployment with height-based windows. And while your inner schadenfreude might laugh at a UASF foot-gun, such a chain-splitting failure affects everyone.)
Perverse timestamp-manipulation incentives for miners, who can use timestamp manipulation to slow or speed up MTP to delay or speed up the closing of an activation window. People rely on accurate MTP timestamps for things like lock times, and of course it affects difficulty adjustments, and so it is very bad for there to be incentives for miners to manipulate timestamps.
Two-week uncertainty over activation window start or end times based on alignment of 2016-block window with calendar dates. Makes it very difficult for people to schedule travel or events around a single likely activation date (+/- a day or two). This affects not just activation parties (which is a silly reason IMHO, even more so in the midst of a pandemic), but also mining and infrastructure organizations which will want staff at hand in case there are activation issues.
BIP8 was specifically created to codify our lessons learned from activating segwit into a new activation protocol to be used in the next soft-fork. It got significant review 4 years ago and in the time since, and was strengthened by that review. There is no way that some new state machine for MTP-based activation could receive the same amount of review in the speedy trial timeline. We should use the reviewed mechanisms that are already in place (or very nearly so) and which incorporate lessons learned. If you think there are flaws in BIP8(LOT=false), then start working now on an alternative activation mechanism for the next soft-fork.
Every single one of the above points has been made on the mailing list or in relevant Github discussions. I don’t think that any of them have been adequately addressed by MTP advocates. For example, pointing out that “timewarp could cause more uncertainty than +/- 2 weeks” ignores a few facts: (1) timewarp is NOT a normal condition and would itself be seen as an attack on the network; (2) timewarp would require the coordination of more miners than it would take to block taproot anyway; and (3) having monotonically decreasing estimation error without a two-week floor is critical to scheduling needs.
I think many of us who were involved in that prior work that went into segwit lessons learned and BIP8 review are sitting around dumbfounded and speechless at what has happened. What is the point of doing all that advanced planning if it’s immediately thrown away precisely when it is needed? I’m not surprised if Luke-Jr and Jorge seem obstinate on this. They’ve invested literally years on this problem so that we’d have a solution when it is needed, as it is now, and getting buy-in from the developers at the time… only to have all that casually thrown away. (Because of a freaking coin flip?)”
56@@ -57,18 +57,12 @@ ThresholdState AbstractThresholdConditionChecker::GetStateFor(const CBlockIndex*
57
58 switch (state) {
59 case ThresholdState::DEFINED: {
60- if (pindexPrev->GetMedianTimePast() >= nTimeTimeout) {
61- stateNext = ThresholdState::FAILED;
In commit ccfe854884276fbdd2ddbcc65689dbd3306f6332 “versionbits: simplify state transitions”
If I restore the logic to go straight to FAILED here, the unit tests don’t complain. I didn’t try with the fuzz tester. Maybe a chance to expand the tests here?
Good catch! I think this oughta do it:
0diff --git a/src/test/versionbits_tests.cpp b/src/test/versionbits_tests.cpp
1index 5f53da8aff..57d9f1bedf 100644
2--- a/src/test/versionbits_tests.cpp
3+++ b/src/test/versionbits_tests.cpp
4@@ -200,6 +200,15 @@ BOOST_AUTO_TEST_CASE(versionbits_test)
5 .Mine(3000, TestTime(20000), 0).TestFailed().TestStateSinceHeight(3000) // 50 old blocks (so 899 out of the past 1000)
6 .Mine(4000, TestTime(20010), 0x100).TestFailed().TestStateSinceHeight(3000)
7
8+ // DEFINED -> STARTED after timeout reached -> FAILED
9+ .Reset().TestDefined().TestStateSinceHeight(0)
10+ .Mine(1, TestTime(1), 0).TestDefined().TestStateSinceHeight(0)
11+ .Mine(1000, TestTime(10000) - 1, 0x100).TestDefined().TestStateSinceHeight(0) // One second more and it would be started
12+ .Mine(1999, TestTime(20000), 0).TestDefined().TestStateSinceHeight(0) // Still not started
13+ .Mine(2000, TestTime(20000), 0).TestStarted().TestStateSinceHeight(2000) // Skip straight to the timeout MTP. We guarantee one period of started
14+ .Mine(2999, TestTime(30000), 0).TestStarted().TestStateSinceHeight(2000) // 999 new blocks
15+ .Mine(3000, TestTime(30000), 0).TestFailed().TestStateSinceHeight(3000) // ... and fail
16+
17 // DEFINED -> STARTED -> LOCKEDIN after timeout reached -> ACTIVE
18 .Reset().TestDefined().TestStateSinceHeight(0)
19 .Mine(1, TestTime(1), 0).TestDefined().TestStateSinceHeight(0)
One way to manually run the fuzz tests without running fuzzing is something like this:
0ls ../../../qa-assets/fuzz_seed_corpus/versionbits/* --sort=s -r |
1while read a; do
2 if ! FUZZ=versionbits test/fuzz/fuzz < $a; then
3 echo $a;
4 echo 'base64:' $(base64 < $a);
5 break;
6 fi;
7done
(that runs the versionbits fuzzer using the qa-assets seed corpus, in order of smallest seed to largest, and prints out the filename of the first failing seed; assumes you’re in src/ already)
When I run that with the DEFINED condition changed to if (GetMedianTimePast >= nTimeTimeout) stateNext = FAILED; else if (GetMedianTimePast >= nTimeStart) stateNext = STARTED;
, I get:
0fuzz: test/fuzz/versionbits.cpp:322: void (anonymous namespace)::versionbits_fuzz_target(FuzzBufferType): Assertion `exp_state == ThresholdState::FAILED' failed.
1../../../qa-assets/fuzz_seed_corpus/versionbits/ed29b8233d29abb3e0398be2221b9bdb8623daf6
2base64: /5SUlL29vb29va0Axr3y
If you don’t want to download the qa-assets repo, you should be able to duplicate that specific error with echo '/5SUlL29vb29va0Axr3y' | base64 -d | FUZZ=versionbits test/fuzz/fuzz
DEFINED -> FAILED
test case to become DEFINED -> STARTED after timeout reached -> FAILED
rather than taking the case above.
Reviewed the code of each commit individually and tested its tests. Found one comment that confuses me and one scenario where there could maybe be a better test. See line comments below; complete review and testing journal here: https://gist.github.com/harding/e622323eaf80d620826a7cb74ab3fb40
Tomorrow I’ll try to do a more holistic review of the final mechanism to convince myself all the state transitions are well tested. I’d also like to play around a bit more with the fuzz testing to get a better handle on it.
Overall I’m pleased with the code, especially it’s simplicity (which is emphasized when going commit-by-commit with the PR’s excellent commit organization).
CR-ACK be7d1368d8e98b6f9f692289b7d25ac369bf2984
Will put some additional effort into futzing with the fuzzers and regtests and stuff before a tAck. Seems like there are some fuzzing edge cases that need better coverage, but overall looks great (and those can be added as follow on commits).
The intent here is to allow checking ComputeBlockVersion behaviour with
each deployment, rather than only testdummy on mainnet. This commit does
the trivial refactoring component of that change.
This generalises the ComputeBlockVersion test so that it can apply to
any activation parameters we might set, and checks all the parameters
set for each deployment on each chain, to simultaneously ensure that the
deployments we have configured work sensibly, and that the test code
does not suffer bitrot in the event that all interesting deployments
are buried.
Simplify the versionbits unit test slightly to make the next set of
changes a little easier to follow.
Previously we used deployments that would timeout prior to Bitcoin's
invention, which allowed the deployment to still be activated in unit
tests. This switches those deployments to be truly never active.
This removes the DEFINED->FAILED transition and changes the
STARTED->FAILED transition to only occur if signalling didn't pass the
threshold. This ensures that it is always possible for activation to
occur, no matter what settings are chosen, or the speed at which blocks
are found.
Updated for feedback, git diff be7d136..ffe33df
should be easy to follow if you’d reviewed the previous head.
[EDIT: bumped the final commit with no changes and re-pushed so as to retrigger CI since the incident has apparently finished]
re-utACK https://github.com/bitcoin/bitcoin/pull/21377/commits/ffe33dfbd4c3b11e3475b022b6c1dd077613de79
API changed to return min_height_activation regardless of value, other assorted comments. I’ll verify the new test case triggers when mutating logic later today to change to a tACK
tACK https://github.com/bitcoin/bitcoin/pull/21377/commits/ffe33dfbd4c3b11e3475b022b6c1dd077613de79
I can’t trigger any more mutation false-successful runs.
utACK ffe33dfbd4c3b11e3475b022b6c1dd077613de79
Verified range-diff from be7d1368d8
review ACK ffe33dfbd4c3b11e3475b022b6c1dd077613de79 💈
Also checked that it addresses all my style feedback I left on the other pull ( #21392#pullrequestreview-621891984 )
Signature:
0-----BEGIN PGP SIGNED MESSAGE-----
1Hash: SHA512
2
3review ACK ffe33dfbd4c3b11e3475b022b6c1dd077613de79 💈
4
5Also checked that it addresses all my style feedback I left on the other pull ( [#21392](/bitcoin-bitcoin/21392/)#pullrequestreview-621891984 )
6-----BEGIN PGP SIGNATURE-----
7
8iQGzBAEBCgAdFiEE+rVPoUahrI9sLGYTzit1aX5ppUgFAlwqrYAACgkQzit1aX5p
9pUjIiwv/elbuMaynxLCVMxZAuduzypZgl4ZM/6IysmHZSw9FxhYgXWih3UrIVlHG
10jQpEqUorssfPE0dUeVNG7ToVCs9w5t+DSzJpKJUDzHsESnoXkxvZm/PqWc9q3+vr
11fVv3azJ6LOnyUmxBUy8vZf+FMck66cJq6VPhfG24kL57MgX24JRk8NT0swiuWLS9
12Ekz1N2bSKB7iGeTyhLbyjrNiSNK29BUeT0dqKNTx2jqsnrDAyzD4skZLT8ZubWdY
13kW+njZwz2ZNvvs+ffHcG/EJpCeRAmlDBw+F6TD1bm+WilcmXY2bp9uLiUE1Uo2eR
14kPk2RlQxJtVDGwFpUfGYX7847eePYrMgUsDVsrSCkdqhHh+2UAwlyzqQrr0Z8lAS
15NF02RYBFcFmwbuJ2Z7+Fu5pvlqcZ4q9YHepj8R3BTHKJURCj6ss9FNby0mEUYdfJ
16jF63r+KwMljW3jbox10SEe1T32MZHuOyu4VaBv+hs1Rjna9JgKbZQaHPo1pOAcid
17AlhYcuHs
18=pYnx
19-----END PGP SIGNATURE-----
Timestamp of file with hash 9edb49a6fd6bab8f19ff268a5fde21eb741d236d3f07ee453643334bfb0a3cac -
453+{
454+ // check that any deployment on any chain can conceivably reach both
455+ // ACTIVE and FAILED states in roughly the way we expect
456+ for (const auto& chain_name : {CBaseChainParams::MAIN, CBaseChainParams::TESTNET, CBaseChainParams::SIGNET, CBaseChainParams::REGTEST}) {
457+ const auto chainParams = CreateChainParams(*m_node.args, chain_name);
458+ for (int i = 0; i < (int)Consensus::MAX_VERSION_BITS_DEPLOYMENTS; ++i) {
in commit f054f6bcd2c2ce5fea84cf8681013f85a444e7ea:
It would be nice to check that no bits are re-used (within the same chain). I guess previously there was no test for this because BIP 9 allowed (though discouraged) this. With the changes here this should be disallowed because the transition to FAILED (on timeout) no longer takes precedence over STARTED/LOCKED_IN. So with the changes here all softforks on the same bit might overlap, even when non-overlapping start-end times are picked.
593274445004506c921d5d851361aefb3434d744 https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#es49-if-you-must-use-a-cast-use-a-named-cast
0 for (int i = 0; i < static_cast<int>(Consensus::MAX_VERSION_BITS_DEPLOYMENTS); ++i) {
77@@ -78,7 +78,7 @@ class CMainParams : public CChainParams {
78 consensus.nPowTargetSpacing = 10 * 60;
79 consensus.fPowAllowMinDifficultyBlocks = false;
80 consensus.fPowNoRetargeting = false;
81- consensus.nRuleChangeActivationThreshold = 1916; // 95% of 2016
82+ consensus.nRuleChangeActivationThreshold = 1815; // 90% of 2016
301+ // This implicitly uses versionbitscache, so clear it every time
302+ versionbitscache.Clear();
303+
304+ int64_t bit = params.vDeployments[dep].bit;
305+ int64_t nStartTime = params.vDeployments[dep].nStartTime;
306+ int64_t nTimeout = params.vDeployments[dep].nTimeout;
63879f0a4760c0c0f784029849cb5d21ee088abb these can all be const
(and eventually braced initialization to check for narrowing)
0- int64_t bit = params.vDeployments[dep].bit;
1- int64_t nStartTime = params.vDeployments[dep].nStartTime;
2- int64_t nTimeout = params.vDeployments[dep].nTimeout;
3+ const int64_t bit{params.vDeployments[dep].bit};
4+ const int64_t nStartTime{params.vDeployments[dep].nStartTime};
5+ const int64_t nTimeout{params.vDeployments[dep].nTimeout};
I think this is mostly personal style choice, so more as a general suggestion: if you really just want to get shorter aliases for other expressions, this may be even better (guaranteed to have the same type, and will avoid a copy in case it’s a nontrivial object).
0const auto& bit = params.vDeployments[dep].bit;
1...
Even if the right hand is a nontrivial expression that computes a temporary, lifetime extension makes this formula safe to use.
372+ // Start generating blocks before nStartTime
373+ lastBlock = firstChain.Mine(params.nMinerConfirmationWindow, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
374+ BOOST_CHECK_EQUAL(ComputeBlockVersion(lastBlock, params) & (1<<bit), 0);
375+
376+ // Mine more blocks (4 less than the adjustment period) at the old time, and check that CBV isn't setting the bit yet.
377+ for (uint32_t i = 1; i < params.nMinerConfirmationWindow - 4; i++) {
63879f0a4760c0c0f784029849cb5d21ee088abb here and 593274445004506c921d5d851361aefb3434d744 lines 355 and 396, prefix iterator
0 for (uint32_t i = 1; i < params.nMinerConfirmationWindow - 4; ++i) {
443@@ -447,6 +444,15 @@ BOOST_AUTO_TEST_CASE(versionbits_computeblockversion)
444 }
445 }
446
447+ {
448+ // Use regtest/testdummy to ensure we always exercise some
449+ // deployment that's not always/never active
450+ ArgsManager args;
451+ args.ForceSetArg("-vbparams", "testdummy:1199145601:1230767999"); // January 1, 2008 - December 31, 2008
-2
here?
@ajtowns maybe I’m missing something, but January 1, 2008 - December 31, 2008
is also never active. Or do we use earlier fake dates in the test?
Ah I see: #21377 (review) . The casual reader - and me - might understand the test better when using a realistic range, but no need to change things here.
450+ {
451+ // Use regtest/testdummy to ensure we always exercise the
452+ // min_activation_height test, even if we're not using that in a
453+ // live deployment
454+ ArgsManager args;
455+ args.ForceSetArg("-vbparams", "testdummy:1199145601:1230767999:403200"); // January 1, 2008 - December 31, 2008, min act height 403200
dd85d5411c1702c8ae259610fe55050ba212e21e: why is the timeout set to 2008 here? In 55ac5f568a3b73d6f1ef4654617fb76e8bcbccdf we don’t the start height to -2
here, so the test won’t early-exit, but I think it’s more clear to just set a far future timeout date:
0args.ForceSetArg("-vbparams", "testdummy:1199145601:4102441200:403200"); // January 1, 2008 - January 1, 2100, min act height 403200
ACK ffe33dfbd4c3b11e3475b022b6c1dd077613de79
Concept ACK on the state transition simplification for BIP 9. I made a PR to the BIP repo to reflect these changes: https://github.com/bitcoin/bips/pull/1101
21@@ -22,7 +22,7 @@ void SetupChainParamsBaseOptions(ArgsManager& argsman)
22 "This is intended for regression testing tools and app development. Equivalent to -chain=regtest.", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS);
23 argsman.AddArg("-segwitheight=<n>", "Set the activation height of segwit. -1 to disable. (regtest-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);
24 argsman.AddArg("-testnet", "Use the test chain. Equivalent to -chain=test.", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS);
25- argsman.AddArg("-vbparams=deployment:start:end", "Use given start/end times for specified version bits deployment (regtest-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS);
26+ argsman.AddArg("-vbparams=deployment:start:end[:min_activation_height]", "Use given start/end times and min_activation_height for specified version bits deployment (regtest-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS);
73d4a706393e6dbd6b6d6b6428f8d3233ac0a2d8 maybe mention “optional” in addition to the brackets, as done for e.g, -rpcbind
0 argsman.AddArg("-vbparams=deployment:start:end[:min_activation_height]", "Use given start/end times and optional min_activation_height for specified version bits deployment (regtest-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS);
240- .Mine(14333, TestTime(30003), 0).TestActive().TestStateSinceHeight(4000)
241- .Mine(24000, TestTime(40000), 0).TestActive().TestStateSinceHeight(4000)
242+ .Mine(4000, TestTime(30002), 0).TestActiveDelayed().TestStateSinceHeight(4000, 3000) // delayed will not become active until height=15000
243+ .Mine(14333, TestTime(30003), 0).TestActiveDelayed().TestStateSinceHeight(4000, 3000)
244+ .Mine(15000, TestTime(40000), 0).TestActive().TestStateSinceHeight(4000, 15000)
245+ .Mine(24000, TestTime(40000), 0).TestActive().TestStateSinceHeight(4000, 15000)
dd85d5411c1702c8ae259610fe55050ba212e21e for these four calls to TestStateSinceHeight(int height, int height_delayed)
0 .Mine(24000, TestTime(40000), 0).TestActive().TestStateSinceHeight(4000, /* height_delayed */ 15000)
172+ start_time = Consensus::BIP9Deployment::NEVER_ACTIVE;
173 never_active_test = true;
174 }
175+ timeout = fuzzed_data_provider.ConsumeBool() ? Consensus::BIP9Deployment::NO_TIMEOUT : fuzzed_data_provider.ConsumeIntegral<int64_t>();
176 }
177+ int min_activation = fuzzed_data_provider.ConsumeIntegralInRange<int>(0, period * max_periods);
dd07e6da48040dc7eae46bc7941db48d98a669fd can be const
0 const int min_activation{fuzzed_data_provider.ConsumeIntegralInRange<int>(0, period * max_periods)};
git diff be7d136..ffe33df
I’m going to try to make this my last comment on this PR because I’m wary of continuously adding noise and disrupting review. I just want to make sure we are all happy with a mix of block height and MTP rather than a consistent use of block height. Personally I just about am but my preference would definitely be a consistent use of block height.
A minor preference for consistent use of block height from: @achow101 (link) @Sjors (link) @roconnor-blockstream (link) @benthecarman (link) @michaelfolkson @MarcoFalke (link) @instagibbs (link)
A minor preference for mix of block height and MTP from: No one
Entirely neutral: @harding @jonatack
A literal NACK for consistent use of block height from: @ajtowns (link)
A literal NACK for mix of block height and MTP from: @luke-jr (link) @maaku (link)
I’m personally not sure where to put them: @jeremyrubin @ariard
If I have mischaracterized anyone’s view please message me on IRC or on ##taproot-activation channel and I will update this comment. Please don’t comment on the PR as the focus should be on code and testing. However, if people like AJ’s PR but they’d prefer consistent block height than mix of block height and MTP (essentially they just don’t like BIP 8) then we should be going with this PR, a consistent use of block height and either BIP 9 or a new BIP.
73@@ -74,12 +74,16 @@ ThresholdState AbstractThresholdConditionChecker::GetStateFor(const CBlockIndex*
74 }
75 if (count >= nThreshold) {
I think this is behavior is an explicit deviation from bip9.
If the last block of the retarget period is fulfilling the threshold condition but with a MTP past with the timeout, the locked_in takes precedence over the failure transition. This sounds contrary to the bip 9 : “MTP < timeout AND (threshold reached)” ?
If you retouch the branch or as a follow-up, I think it would be worthy to document.
https://github.com/bitcoin/bips/pull/1104#issuecomment-819068299
That’s probably what you want to look at.
Code Review ACK ffe33df
I did exercise the unit test coverage by manually mutating I think all the transition logics of the versionbits state machine. I didn’t find holes.
I’ll try to have a look on fuzz testing coverage.
@michaelfolkson I’m neutral on this debate and more preoccupied by the size of the changeset and thoroughness of its review than other concerns raised so far in the MTP vs consistent height use debate.
Thanks to share maaku arguments, especially about malicious timestamp-manipulation by miners that I also raised in this PR previously. Honestly, if we see a mining cartel starting timewarp attacks even with the auxiliary intent of blocking taproot activation, I would be more worried of fixing the timewarp vuln rather than taproot not activating.
If those perverse miners behaviors do happen and ST doesn’t succeed, we’ll at least have done a glaring step forward in the taproot activation debate. Assuming this hypothetical scenario, I think we can discuss again safe UASF mechanisms built-in in Core.
LOT=true
BIP 8 implementation?
ACK ffe33dfbd4c3b11e3475b022b6c1dd077613de79
I haven’t been a Bitcoin developer for several years now but I am a user and investor with a solid technical background, having materially contributed to I believe every intentional consensus change since Satoshi’s last. I still follow along with some technical developments, including taproot activation related PRs.
I’ve reviewed the PR and (AFAIK) all the publicly available conversation and spoken to many users I know in the ecosystem. From my research and discussion it is unambiguous that this is not just perfectly acceptable but a good path forward and also extremely broadly supported. Generally I’ve found users are of the perspective that whatever activation gets taproot activated quickly without causing relevant problems is a good thing, along with a lot of frustration over what they perceive to be arguing over pointless things.
502@@ -492,22 +503,26 @@ void CRegTestParams::UpdateActivationParametersFromArgs(const ArgsManager& args)
503 for (const std::string& strDeployment : args.GetArgs("-vbparams")) {
504 std::vector<std::string> vDeploymentParams;
505 boost::split(vDeploymentParams, strDeployment, boost::is_any_of(":"));
506- if (vDeploymentParams.size() != 3) {
507- throw std::runtime_error("Version bits parameters malformed, expecting deployment:start:end");
508+ if (vDeploymentParams.size() < 3 || 4 < vDeploymentParams.size()) {
nit: Apologies if this is bikeshedding, but the asymmetry of <
and >
causes more mental load than the code it replaces since inequality is commutative. It took me a sec to wrap my head around it and I wonder if something like
0 if (vDeploymentParams.size() != 3 && vDeploymentParams.size() != 4) {
would be more readable. Obviously this is not strictly necessary, and if more sizes become permissible than just the two acceptable here, the code you wrote would be more future proof.
I have few questions and issues:
Is this strictly based on BIP 9 or modified version of BIP 9? If modified, what are the changes and reasons for it?
What are the trade-offs involved in using ST|BIP9|MTP vs ST|BIP8|Block Height?
Use of BIP 9:
Quoting few things from https://medium.com/@elombrozo/forks-signaling-and-activation-d60b6abda49a since I had followed all the things in 2017 and Eric Lombrozo had been a part of lot of discussions I read or watched in different podcasts.
In hindsight, it’s become clear that miner activation of soft forks cannot be relied upon when there exists a divergence of interests between miners and users. In the cooperative case, it is a tried and tested mechanism which if done correctly is known to work smoothly. However, in the adversarial case it simply does not work. BIP9 effectively gives a small amount of hashpower veto power over any soft fork activation no matter how widely popular it is among users and industry.
So given these lessons of the past, it seems that a combination of MASF and UASF will be required for future deployments to properly address these adversarial scenarios. It is unlikely BIP9 will be ever used again.
Quoting few things from https://medium.com/hackernoon/peter-todd-on-the-essence-of-bitcoin-b8d0c6d16f43
No, definitely not. No. Most programmers in general, do you not have the capability to work on this stuff, because they don’t think it adversarially. It’s a tough thing for people to imagine. Most people are just not used to imagining, “All right, what are the bad things that could happen?”
Lot of people think its okay to ignore few things in this activation because we all want taproot and some think being prepared is PTSD from 2017. I don’t agree with it because its Bitcoin and we need to be prepared for every bad scenario. We don’t trust anyone if using full nodes for Bitcoin, why should we trust the miners who got incentives to mine blocks and in some case incentives to attack the network?
I had mentioned few things in this email: https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2021-March/018613.html
There was one line written on the wall of a school I still remember: The more we sweat in peace, the less we bleed in war
Although SE answer and few other posts mentioned few trade-offs, I am still not sure if using MTP has no risk in a soft fork activation mechanism. Coinflip even if not considered for deciding MTP vs block height was wrong way to decide something involved in soft fork activation mechanism which can change lot of risk/rewards for miners to attack.
http://gnusha.org/taproot-activation/
@prayank23 The activation mechanism is not the same as BIP9. The spec is at https://github.com/bitcoin/bips/pull/1104 because this is “speedy trial” and not BIP9 it has shorter duration, a lower threshold, and a minimum activation point (to give people adequate time to update).
In terms of preparation– it is not feasible to prepare when you don’t know what you’re preparing for and the preparations come at great cost. The idea with speedy trial is that is an inexpensive way to learn the lay of the land such that further moves (if required at all) can be informed by actual objective information, rather than just having community members set their subjective speculations, hopes, and fears in a battle of rhetoric against each other. Preparing for the wrong thing can easily be worse than not preparing at all.
It sounds like you may misunderstand what the “speedy” part refers to here– it isn’t about implementation complexity. It’s about learning how activation might activate or fail, in detail, with objective facts quickly. The implementation here is also designed to leverage the existing stored testing and experience because that is just prudent engineering and no amount of additional time or added developers can fully replace that. Even with all the resources in the world, you’re better off not introducing changes without good cause– those resources can better be used some other way.
As such the most valuable thing for ST to accomplish is to be over with soon (activated or expired) and to do so without continuing to burn up goodwill among community members fighting over opinions rather than facts. If it activates, GREAT. If it doesn’t activate– that’s great too– because we will have learned about how it didn’t activate and that will inform the required preparations and tradeoffs for further efforts. The veto comment is directly addressed by ST (and also really was partially a criticism not of BIP9 but of the fact that the NODE_WITNESS changes made segwit difficult to reissue, leading to a mistaken belief that if it didn’t activate it couldn’t be activated later.)
ST is in some sense the opposite of trusting. Instead of “trusting” participants to behave in particular ways, we just set things up so that no matter what happens things will be okay and then we see what actually happens. This is much better than speculating, which is really just trusting our guesses and trusting others to behave in particular ways (perhaps you trust them to behave badly, but that is still an expectation). ST is all about minimizing subjective assumptions in favor of learning objective facts.
In fact, if anything, I’d characterize allowing a broadly supported improvement to get jammed over the fine details of activation is a perfect example of failing to think adversarially. Even to someone who is of the view that the fine details of the activation here aren’t the best possible under their preferred weighing of the tradeoffs should recognize that after ST is over (via activation or expiration) these activation criteria will not be relevant at all anymore. This puts a strict upper bound on how much these details matter.
Coinflip even if not considered for deciding MTP vs block height
Since it appears you know it wasn’t actually used bringing it up here is not really appropriate. Please don’t do that. I’d like no more to discuss with you at length why I am confident you are mistaken, but it would be entirely off-topic here.
- Is this strictly based on BIP 9 or modified version of BIP 9? If modified, what are the changes and reasons for it?
It’s modified it two ways. First, the timeout is only evaluated at the end of a STARTED period after all signaling blocks in that period have been counted. This prevents miners from skipping over the signaling period altogether; that’s mainly a concern for attempts to require mandatory signaling (e.g. a UASF) but it also simplifies things here a bit.
Second, it contains a minimum_activation_height parameter that delays activation of a locked_in fork until the specified height, making it compatible with the ST proposal.
- What are the trade-offs involved in using ST|BIP9|MTP vs ST|BIP8|Block Height?
ST|BIP9|MTP is compatible with activating future soft forks on signet. ST|BIP9|Height is slightly simpler to review due to heights generally being monotonicly increasing and always being impossible to skip (although, again, this PR eliminates the skipping problem for MTP).
3. Use of BIP 9:
[Quoting from Eric Lombrozo] it’s become clear that miner activation of soft forks cannot be relied upon when there exists a divergence of interests between miners and users.
That remains clear. In the case of taproot, miners have said that there’s a convergence of interests and Speedy Trial is an attempt to see if that’s actually the case. It’s designed either quickly succeed or quickly fail so that we can, correspondingly, quickly move on to the next thing or get everyone in agreement that we need some sort of UASF.
it seems that a combination of MASF and UASF will be required for future deployments to properly address these adversarial scenarios.
ST is exactly that. It’s a MASF which, if it fails, is almost certain to lead to a UASF. The insight here is that we don’t need to bundle a MASF and a UASF together; we can do them independently to allow us to focus on one piece of the puzzle at a time.
It is unlikely BIP9 will be ever used again._
If you’d like, I believe I can do some digging to find other developers who disagreed with Lombrozo at the time he said that. Moreover, it’s also worth noting that what’s proposed here is a modified version of BIP9—it has much the same relationship with BIP9 that BIP8 itself has with BIP9: an re-use of the same basic mechanism but tweaked for slightly different goals.
1. Use of MTP:
Although SE answer and few other posts mentioned few trade-offs, I am still not sure if using MTP has no risk in a soft fork activation mechanism.
No change to consensus code is without risk. MTP is slightly more complex to think about than height, but IMO not to the degree height should automatically outweigh any advantage of MTP.
I understand its a “Speedy” Trial and things need to be done in less time with less people who go the right skills and experience
This PR has so far been reviewed in depth by most of the experts I would expect to review any other activation mechanism. I don’t think we’ve short circuited the review process; indeed, perhaps because of the controversy, I think we’ve all been more careful here than we might’ve been had there been no controversy at all.
1. Its not just [@luke-jr](/bitcoin-bitcoin/contributor/luke-jr/) who has different opinion about the approach
To my knowledge, the technical objectors to this approach are:
@luke-jr, on the basis that height is better than MTP and that he believes there’s consensus to use BIP8. IMO, the differences between height and MTP aren’t significant enough to warrant a NACK and the previous widespread discussion about BIP8 doesn’t represent consensus any more than widespread discussion of a block size increase in early 2015 represented consensus to hard fork.
@maaku, on the basis that using MTP encourages miners to manipulate their block times and that BIP8/heights are easier and safer to use for non-devs creating their own UASFs. IMO, significant time manipulation problems are extremely unlikely in the first place, super duper extremely unlikely for a ST, and are easier to mitigate than equivalent miner attempts to delay BIP8. Also IMO, with a little bit of documentation, we can make it just as easy and safe for non-devs to create UASFs using this PR and MTP as for using BIP8.
@michaelfolkson who I think has only echoed the arguments above.
Several other people have expressed a preference for heights over MTP, but I personally don’t think that’s a reason to prevent merging this PR. We will hopefully have many additional opportunities to activate feature-adding soft forks in the future, so we can iterate on the mechanism as we go—just as we’ve done from BIP30 flag day to BIP34 ISM to BIP9 to BIP148 to this PR. It’s better to do that work in parallel with waiting to see if miners use ST to lock in taproot than it is to delay taproot further while we argue over the subjective value of different people’s preferences.
(Note: I didn’t see @gmaxwell’s reply to your comment until I was previewing my draft of this comment; sorry if there’s any redundancy.)
266@@ -226,6 +267,13 @@ BOOST_AUTO_TEST_CASE(versionbits_test)
267 // Make sure that no deployment tries to set an invalid bit.
268 BOOST_CHECK_EQUAL(bitmask & ~(uint32_t)VERSIONBITS_TOP_MASK, bitmask);
269
270+ // Check min_activation_height is on a retarget boundary
271+ BOOST_CHECK_EQUAL(mainnetParams.vDeployments[i].min_activation_height % mainnetParams.nMinerConfirmationWindow, 0);
This can be addressed in a followup:
0warning: comparison of integers of different signs: 'const unsigned int' and 'const int' [-Wsign-compare]
1...
2test/versionbits_tests.cpp:271:9: note: in instantiation of function template specialization 'boost::test_tools::tt_detail::check_frwd<boost::test_tools::tt_detail::equal_impl_frwd, unsigned int, int>' requested here
3 BOOST_CHECK_EQUAL(mainnetParams.vDeployments[i].min_activation_height % mainnetParams.nMinerConfirmationWindow, 0);
I can’t reproduce (#19123), but the warning seems correct:
0#include <iostream>
1#include <type_traits>
2#include <cstdint>
3
4int main()
5{
6 std::cout << std::boolalpha;
7
8 std::cout << std::is_same_v<int , decltype(int{1} % unsigned{2})> << '\n'; // false
9 std::cout << std::is_same_v<unsigned, decltype(int{1} % unsigned{2})> << '\n'; // true
10}
Concept NACK. BIP9 is a step backward from lessons learned during segwit activation. The user community that I am in wants BIP8 so we can UASF if miners don’t cooperate. @rodasmith Just NACKing this, without even bothering to read the discussion in this very thread is only going to hurt your reputation. Your argument doesn’t even make any sense since ST with MTP is absolutely compatible with a UASF.
If you had read at least the last few comments, you’d know this. Please educate yourself before wasting precious developer time.
Post-merge tested ACK ffe33dfbd4c3b11e3475b022b6c1dd077613de79
getblockchaininfo
and the tests since previous review; also performed additional testing on the earlier vbparams changes and ensured getblocktemplate
returned reasonable-looking results for a regtest deploymentMy updated detailed review notes here: https://gist.github.com/harding/e622323eaf80d620826a7cb74ab3fb40
My thanks to AJ, Andrew, and all the other test writers and reviewers. This seems to me to be extraordinarily well considered code.
Post merge Code Review ACK ffe33dfbd4c3b11e3475b022b6c1dd077613de79
Also built and ran unit tests locally.
92 consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].bit = 2;
93- consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].nStartTime = 1199145601; // January 1, 2008
94- consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].nTimeout = 1230767999; // December 31, 2008
95+ consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].nStartTime = Consensus::BIP9Deployment::NEVER_ACTIVE;
96+ consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT;
97+ consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].min_activation_height = 0; // No activation delay
min_activation_height
should be 709632
but this code contains 0
.
ajtowns
evoskuil
jonatack
michaelfolkson
roconnor-blockstream
benthecarman
sipa
Sjors
JeremyRubin
DrahtBot
harding
MaxHillebrand
MarcoFalke
amitiuttarwar
luke-jr
jnewbery
ariard
instagibbs
ProofOfKeags
achow101
flack
Rspigler
rxgrant
earonesty
jamesob
molxyz
zndtoshi
kastravec
guerillaV2
cnavigato
gmaxwell
katesalazar
ghost
fanquake
hsjoberg
Ziya-Sadr
rodasmith
viaj3ro
jaybny
fjahr
Kixunil
Labels
Validation
Consensus