diff --git a/doc/bips.md b/doc/bips.md index ec519166dbbef..faec8184e9478 100644 --- a/doc/bips.md +++ b/doc/bips.md @@ -1,6 +1,6 @@ BIPs that are implemented by Bitcoin Core (up-to-date up to **v0.21.0**): -* [`BIP 8`](https://github.com/bitcoin/bips/blob/master/bip-0008.mediawiki): The changes for parallel, rapid deployment based on block height miner activation have been implemented since **v0.21.1** ([PR #21392](https://github.com/bitcoin/bitcoin/pull/21392)). The UASF fallback with forced signaling (`LOT=true`) has not yet been implemented. The current implementation is the equivalent of `LOT=false`. +* [`BIP 8`](https://github.com/bitcoin/bips/blob/master/bip-0008.mediawiki): The changes for parallel, rapid deployment based on block height miner activation with UASF failover have been implemented since **v0.21.1** ([PR #19573](https://github.com/bitcoin/bitcoin/pull/19573)). * [`BIP 9`](https://github.com/bitcoin/bips/blob/master/bip-0009.mediawiki): The changes allowing multiple soft-forks to be deployed in parallel have been implemented since **v0.12.1** ([PR #7575](https://github.com/bitcoin/bitcoin/pull/7575)) Support was removed in **v0.21.1** ([PR #21392](https://github.com/bitcoin/bitcoin/pull/21392)). * [`BIP 11`](https://github.com/bitcoin/bips/blob/master/bip-0011.mediawiki): Multisig outputs are standard since **v0.6.0** ([PR #669](https://github.com/bitcoin/bitcoin/pull/669)). * [`BIP 13`](https://github.com/bitcoin/bips/blob/master/bip-0013.mediawiki): The address format for P2SH addresses has been implemented since **v0.6.0** ([PR #669](https://github.com/bitcoin/bitcoin/pull/669)). diff --git a/doc/release-notes-21392.md b/doc/release-notes-21392.md index 76f8310291647..fa02e599cde50 100644 --- a/doc/release-notes-21392.md +++ b/doc/release-notes-21392.md @@ -4,4 +4,4 @@ Low-level changes RPC --- -* BIP 9 has been replaced with a partial implementation of BIP 8. This change is reflected in `getblockchaininfo` where references to BIP 9 have been replaced with references to BIP 8. +* BIP 9 has been replaced with BIP 8. This change is reflected in `getblockchaininfo` where references to BIP 9 have been replaced with references to BIP 8. diff --git a/src/chainparams.cpp b/src/chainparams.cpp index e62cc152ff30b..7030715b89727 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -457,11 +457,12 @@ class CRegTestParams : public CChainParams { /** * Allows modifying the Version Bits regtest parameters. */ - void UpdateVersionBitsParameters(Consensus::DeploymentPos d, int startheight, int timeoutheight, int min_activation_height) + void UpdateVersionBitsParameters(Consensus::DeploymentPos d, int startheight, int timeoutheight, int min_activation_height, bool lockinontimeout) { consensus.vDeployments[d].startheight = startheight; consensus.vDeployments[d].timeoutheight = timeoutheight; consensus.vDeployments[d].m_min_activation_height = min_activation_height; + consensus.vDeployments[d].lockinontimeout = lockinontimeout; } void UpdateActivationParametersFromArgs(const ArgsManager& args); }; @@ -529,8 +530,12 @@ void CRegTestParams::UpdateActivationParametersFromArgs(const ArgsManager& args) for (const std::string& strDeployment : args.GetArgs("-vbparams")) { std::vector vDeploymentParams; boost::split(vDeploymentParams, strDeployment, boost::is_any_of(":")); - if (vDeploymentParams.size() < 3 || vDeploymentParams.size() > 4) { - throw std::runtime_error("Version bits parameters malformed, expecting deployment:@startheight:@timeoutheight[:@min_activation_height]"); + if (vDeploymentParams.size() == 3 && vDeploymentParams[1].compare(0, 2, "@-") == 0) { + // Don't require lockinontimeout for always/never-active special cases + vDeploymentParams.emplace_back("0"); + } + if (vDeploymentParams.size() < 4 || vDeploymentParams.size() > 5) { + throw std::runtime_error("Version bits parameters malformed, expecting deployment:@startheight:@timeoutheight[:@min_activation_height]:lockinontimeout"); } int32_t startheight = 0, timeoutheight = 0, min_activation_height = 0; if (vDeploymentParams[1].empty() || vDeploymentParams[1].front() != '@' || !ParseInt32(vDeploymentParams[1].substr(1), &startheight)) { @@ -539,9 +544,15 @@ void CRegTestParams::UpdateActivationParametersFromArgs(const ArgsManager& args) if (vDeploymentParams[2].empty() || vDeploymentParams[2].front() != '@' || !ParseInt32(vDeploymentParams[2].substr(1), &timeoutheight)) { throw std::runtime_error(strprintf("Invalid timeoutheight (%s)", vDeploymentParams[2])); } - if (vDeploymentParams.size() == 4 && (vDeploymentParams[3].front() != '@' || !ParseInt32(vDeploymentParams[3].substr(1), &min_activation_height))) { + if (vDeploymentParams.size() == 5 && (vDeploymentParams[3].front() != '@' || !ParseInt32(vDeploymentParams[3].substr(1), &min_activation_height))) { throw std::runtime_error(strprintf("Invalid min_activation_height (%s)", vDeploymentParams[3])); } + bool lockinontimeout = false; + if (vDeploymentParams.back().size() != 1 || (vDeploymentParams.back().front() != '0' && vDeploymentParams.back().front() != '1')) { + throw std::runtime_error(strprintf("Invalid lockinontimeout (%s)", vDeploymentParams.back())); + } else { + lockinontimeout = (vDeploymentParams.back().front() == '1'); + } std::string error; if (!CheckVBitsHeights(error, consensus, startheight, timeoutheight, min_activation_height)) { throw std::runtime_error(error); @@ -549,9 +560,9 @@ void CRegTestParams::UpdateActivationParametersFromArgs(const ArgsManager& args) bool found = false; for (int j=0; j < (int)Consensus::MAX_VERSION_BITS_DEPLOYMENTS; ++j) { if (vDeploymentParams[0] == VersionBitsDeploymentInfo[j].name) { - UpdateVersionBitsParameters(Consensus::DeploymentPos(j), startheight, timeoutheight, min_activation_height); + UpdateVersionBitsParameters(Consensus::DeploymentPos(j), startheight, timeoutheight, min_activation_height, lockinontimeout); found = true; - LogPrintf("Setting version bits activation parameters for %s to startheight=%ld, timeoutheight=%ld, min_activation_height=%ld\n", vDeploymentParams[0], startheight, timeoutheight, min_activation_height); + LogPrintf("Setting version bits activation parameters for %s to startheight=%ld, timeoutheight=%ld, min_activation_height=%ld, lockinontimeout=%d\n", vDeploymentParams[0], startheight, timeoutheight, min_activation_height, lockinontimeout); break; } } diff --git a/src/chainparamsbase.cpp b/src/chainparamsbase.cpp index 2b213b91e5333..6fa47d9e75cb6 100644 --- a/src/chainparamsbase.cpp +++ b/src/chainparamsbase.cpp @@ -23,7 +23,7 @@ void SetupChainParamsBaseOptions(ArgsManager& argsman) "This is intended for regression testing tools and app development. Equivalent to -chain=regtest.", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); argsman.AddArg("-segwitheight=", "Set the activation height of segwit. -1 to disable. (regtest-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); argsman.AddArg("-testnet", "Use the test chain. Equivalent to -chain=test.", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS); - argsman.AddArg("-vbparams=deployment:@startheight:@timeoutheight[:@min_activation_height]", "Use given start, timeout, and minimum activation heights for specified version bits deployment (regtest-only). For an always active deployment, use @-1:@-1. For a never active deployment, use @-2:@-2.", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + argsman.AddArg("-vbparams=deployment:@startheight:@timeoutheight[:@min_activation_height]:lockinontimeout", "Use given start, timeout, and minimum activation heights and lockinontimeout (0/1) for specified version bits deployment (regtest-only). For an always active deployment, use @-1:@-1. For a never active deployment, use @-2:@-2.", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); argsman.AddArg("-signet", "Use the signet chain. Equivalent to -chain=signet. Note that the network is defined by the -signetchallenge parameter", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS); argsman.AddArg("-signetchallenge", "Blocks must satisfy the given script to be considered valid (only for signet networks; defaults to the global default signet test network challenge)", ArgsManager::ALLOW_STRING, OptionsCategory::CHAINPARAMS); argsman.AddArg("-signetseednode", "Specify a seed node for the signet network, in the hostname[:port] format, e.g. sig.net:1234 (may be used multiple times to specify multiple seed nodes; defaults to the global default signet test network seed node(s))", ArgsManager::ALLOW_STRING, OptionsCategory::CHAINPARAMS); diff --git a/src/consensus/params.h b/src/consensus/params.h index f491f1333fd67..dc78a09f386bc 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -35,6 +35,8 @@ struct BIP9Deployment { * If lock in occurs, delay activation until at least this block height. Activations only occur on retargets. */ int m_min_activation_height{0}; + /** If true, final period before timeout will transition to MUST_SIGNAL. */ + bool lockinontimeout{false}; /** Constant for timeoutheight very far in the future. */ static constexpr int NO_TIMEOUT = std::numeric_limits::max(); diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index cb8091fb5724a..aa776fddc84d7 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -1223,25 +1223,27 @@ static void BIP9SoftForkDescPushBack(UniValue& softforks, const std::string &nam switch (thresholdState) { case ThresholdState::DEFINED: bip9.pushKV("status", "defined"); break; case ThresholdState::STARTED: bip9.pushKV("status", "started"); break; + case ThresholdState::MUST_SIGNAL: bip9.pushKV("status", "must_signal"); break; case ThresholdState::LOCKED_IN: bip9.pushKV("status", "locked_in"); break; case ThresholdState::ACTIVE: bip9.pushKV("status", "active"); break; case ThresholdState::FAILED: bip9.pushKV("status", "failed"); break; } - if (ThresholdState::STARTED == thresholdState) - { + if (ThresholdState::STARTED == thresholdState || ThresholdState::MUST_SIGNAL == thresholdState || ThresholdState::LOCKED_IN == thresholdState) { bip9.pushKV("bit", consensusParams.vDeployments[id].bit); } bip9.pushKV("startheight", consensusParams.vDeployments[id].startheight); bip9.pushKV("timeoutheight", consensusParams.vDeployments[id].timeoutheight); bip9.pushKV("minimum_activation_height", consensusParams.vDeployments[id].m_min_activation_height); + bip9.pushKV("lockinontimeout", consensusParams.vDeployments[id].lockinontimeout); int64_t since_height = VersionBitsTipStateSinceHeight(consensusParams, id); bip9.pushKV("since", since_height); - if (ThresholdState::STARTED == thresholdState) - { + if (ThresholdState::STARTED == thresholdState || ThresholdState::MUST_SIGNAL == thresholdState || ThresholdState::LOCKED_IN == thresholdState) { UniValue statsUV(UniValue::VOBJ); BIP9Stats statsStruct = VersionBitsTipStatistics(consensusParams, id); statsUV.pushKV("period", statsStruct.period); - statsUV.pushKV("threshold", statsStruct.threshold); + if (thresholdState != ThresholdState::LOCKED_IN) { + statsUV.pushKV("threshold", statsStruct.threshold); + } statsUV.pushKV("elapsed", statsStruct.elapsed); statsUV.pushKV("count", statsStruct.count); statsUV.pushKV("possible", statsStruct.possible); @@ -1288,11 +1290,12 @@ RPCHelpMan getblockchaininfo() {RPCResult::Type::STR, "type", "one of \"buried\", \"bip8\""}, {RPCResult::Type::OBJ, "bip8", "status of BIP 8 softforks (only for \"bip8\" type)", { - {RPCResult::Type::STR, "status", "one of \"defined\", \"started\", \"locked_in\", \"active\", \"failed\""}, + {RPCResult::Type::STR, "status", "one of \"defined\", \"started\", \"must_signal\", \"locked_in\", \"active\", \"failed\""}, {RPCResult::Type::NUM, "bit", "the bit (0-28) in the block version field used to signal this softfork (only for \"started\" status)"}, {RPCResult::Type::NUM, "startheight", "the minimum height of a block at which the bit gains its meaning"}, {RPCResult::Type::NUM, "timeoutheight", "the height of a block at which the deployment is considered failed if not yet locked in"}, {RPCResult::Type::NUM, "minimum_activation_height", "the minimum block height at which activation is allowed to occur"}, + {RPCResult::Type::BOOL, "lockinontimeout", "true if the period before timeoutheight transitions to must_signal"}, {RPCResult::Type::NUM, "since", "height of the first block to which the status applies"}, {RPCResult::Type::OBJ, "statistics", "numeric statistics about BIP8 signalling for a softfork (only for \"started\" status)", { diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index 07d95e50f3b35..4b7dd58b1f414 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -815,6 +815,7 @@ static RPCHelpMan getblocktemplate() aRules.push_back("csv"); if (!fPreSegWit) aRules.push_back("!segwit"); UniValue vbavailable(UniValue::VOBJ); + int32_t vbrequired{0}; for (int j = 0; j < (int)Consensus::MAX_VERSION_BITS_DEPLOYMENTS; ++j) { Consensus::DeploymentPos pos = Consensus::DeploymentPos(j); ThresholdState state = VersionBitsState(pindexPrev, consensusParams, pos, versionbitscache); @@ -823,6 +824,10 @@ static RPCHelpMan getblocktemplate() case ThresholdState::FAILED: // Not exposed to GBT at all break; + case ThresholdState::MUST_SIGNAL: + // Bit must be set in block version + vbrequired |= VersionBitsMask(consensusParams, pos); + // FALL THROUGH to set nVersion and get vbavailable set... case ThresholdState::LOCKED_IN: // Ensure bit is set in block version pblock->nVersion |= VersionBitsMask(consensusParams, pos); @@ -858,7 +863,7 @@ static RPCHelpMan getblocktemplate() result.pushKV("version", pblock->nVersion); result.pushKV("rules", aRules); result.pushKV("vbavailable", vbavailable); - result.pushKV("vbrequired", int(0)); + result.pushKV("vbrequired", vbrequired); if (nMaxVersionPreVB >= 2) { // If VB is supported by the client, nMaxVersionPreVB is -1, so we won't get here diff --git a/src/test/fuzz/versionbits.cpp b/src/test/fuzz/versionbits.cpp index 5c96b865286c5..e87e65cbcd0f6 100644 --- a/src/test/fuzz/versionbits.cpp +++ b/src/test/fuzz/versionbits.cpp @@ -31,9 +31,10 @@ class TestConditionChecker : public AbstractThresholdConditionChecker const int m_period; const int m_threshold; const int m_bit; + const bool m_lockinontimeout; - TestConditionChecker(int begin, int end, int min_act, int period, int threshold, int bit) - : m_begin{begin}, m_end{end}, m_min_activation(min_act), m_period{period}, m_threshold{threshold}, m_bit{bit} + TestConditionChecker(int begin, int end, int min_act, int period, int threshold, int bit, bool lockinontimeout) + : m_begin{begin}, m_end{end}, m_min_activation(min_act), m_period{period}, m_threshold{threshold}, m_bit{bit}, m_lockinontimeout{lockinontimeout} { assert(m_period > 0); assert(0 <= m_threshold && m_threshold <= m_period); @@ -43,6 +44,7 @@ class TestConditionChecker : public AbstractThresholdConditionChecker bool Condition(const CBlockIndex* pindex, const Consensus::Params& params) const override { return Condition(pindex->nVersion); } int StartHeight(const Consensus::Params& params) const override { return m_begin; } int TimeoutHeight(const Consensus::Params& params) const override { return m_end; } + bool LockinOnTimeout(const Consensus::Params& params) const override { return m_lockinontimeout; } int MinActivationHeight(const Consensus::Params& params) const override { return m_min_activation; } int Period(const Consensus::Params& params) const override { return m_period; } int Threshold(const Consensus::Params& params) const override { return m_threshold; } @@ -133,11 +135,17 @@ void test_one_input(const std::vector& buffer) int startheight; int timeoutheight; int min_activation = 0; + bool lockinontimeout = false; if (fuzzed_data_provider.ConsumeBool()) { // pick the timestamp to switch based on a block startheight = fuzzed_data_provider.ConsumeIntegralInRange(0, period * (max_periods - 2)); timeoutheight = fuzzed_data_provider.ConsumeIntegralInRange(0, period * (max_periods - 2)); min_activation = fuzzed_data_provider.ConsumeIntegralInRange(0, period * (max_periods - 1)); + if (startheight < int(period * (max_periods - 3)) && threshold < period) { + // LOT=True requires 3 periods (STARTED->MUST_SIGNAL->LOCKED_IN), pushing it past the deadline + // Furthermore, this fuzzer doesn't let us easily guarantee the signal of the first block in a period, so skip LOT=True when threshold is 100% + lockinontimeout = fuzzed_data_provider.ConsumeBool(); + } } else { if (fuzzed_data_provider.ConsumeBool()) { startheight = Consensus::BIP9Deployment::ALWAYS_ACTIVE; @@ -150,7 +158,7 @@ void test_one_input(const std::vector& buffer) } } - TestConditionChecker checker(startheight, timeoutheight, min_activation, period, threshold, bit); + TestConditionChecker checker(startheight, timeoutheight, min_activation, period, threshold, bit, lockinontimeout); // Early exit if the versions don't signal sensibly for the deployment if (!checker.Condition(ver_signal)) return; @@ -209,7 +217,11 @@ void test_one_input(const std::vector& buffer) // mine (period-1) blocks and check state for (int b = 1; b < period; ++b) { - const bool signal = (signalling_mask >> (b % 32)) & 1; + bool signal = (signalling_mask >> (b % 32)) & 1; + if (exp_state == ThresholdState::MUST_SIGNAL && threshold - blocks_sig >= period - b) { + // Further blocks need to signal to be valid + signal = true; + } if (signal) ++blocks_sig; CBlockIndex* current_block = blocks.mine_block(signal); @@ -223,8 +235,8 @@ void test_one_input(const std::vector& buffer) assert(state == exp_state); assert(since == exp_since); - // GetStateStatistics may crash when state is not STARTED - if (state != ThresholdState::STARTED) continue; + // GetStateStatistics may crash when state is not STARTED or MUST_SIGNAL + if (state != ThresholdState::STARTED && state != ThresholdState::MUST_SIGNAL) continue; // check that after mining this block stats change as expected const BIP9Stats stats = checker.GetStateStatisticsFor(current_block); @@ -236,7 +248,7 @@ void test_one_input(const std::vector& buffer) last_stats = stats; } - if (exp_state == ThresholdState::STARTED) { + if (exp_state == ThresholdState::STARTED || exp_state == ThresholdState::MUST_SIGNAL) { // double check that stats.possible is sane if (blocks_sig >= threshold - 1) assert(last_stats.possible); } @@ -289,11 +301,15 @@ void test_one_input(const std::vector& buffer) assert(exp_state == ThresholdState::DEFINED); } break; + case ThresholdState::MUST_SIGNAL: + assert(height >= checker.m_end - period); + assert(exp_state == ThresholdState::STARTED); + break; case ThresholdState::LOCKED_IN: if (exp_state == ThresholdState::LOCKED_IN) { assert(height < checker.m_min_activation); } else { - assert(exp_state == ThresholdState::STARTED); + assert(exp_state == ThresholdState::STARTED || exp_state == ThresholdState::MUST_SIGNAL); assert(blocks_sig >= threshold); } break; diff --git a/src/test/versionbits_tests.cpp b/src/test/versionbits_tests.cpp index 25303b7b103bf..9d3f4aae12bde 100644 --- a/src/test/versionbits_tests.cpp +++ b/src/test/versionbits_tests.cpp @@ -19,6 +19,7 @@ static const std::string StateName(ThresholdState state) switch (state) { case ThresholdState::DEFINED: return "DEFINED"; case ThresholdState::STARTED: return "STARTED"; + case ThresholdState::MUST_SIGNAL: return "MUST_SIGNAL"; case ThresholdState::LOCKED_IN: return "LOCKED_IN"; case ThresholdState::ACTIVE: return "ACTIVE"; case ThresholdState::FAILED: return "FAILED"; @@ -34,8 +35,11 @@ class TestConditionChecker : public AbstractThresholdConditionChecker mutable ThresholdConditionCache cache; public: + bool m_lockinontimeout{false}; + int StartHeight(const Consensus::Params& params) const override { return 100; } int TimeoutHeight(const Consensus::Params& params) const override { return 200; } + bool LockinOnTimeout(const Consensus::Params& params) const override { return m_lockinontimeout; } int Period(const Consensus::Params& params) const override { return 10; } int Threshold(const Consensus::Params& params) const override { return 9; } int MinActivationHeight(const Consensus::Params& params) const override { return 0; } @@ -107,6 +111,16 @@ class VersionBitsTester Reset(); } + VersionBitsTester& SetLockinOnTimeout(const bool lockinontimeout) { + for (unsigned int i = 0; i < CHECKERS; i++) { + checker[i].m_lockinontimeout = lockinontimeout; + checker_delayed[i].m_lockinontimeout = lockinontimeout; + checker_always[i].m_lockinontimeout = lockinontimeout; + checker_never[i].m_lockinontimeout = lockinontimeout; + } + return *this; + } + VersionBitsTester& Mine(unsigned int height, int32_t nTime, int32_t nVersion) { while (vpblock.size() < height) { CBlockIndex* pindex = new CBlockIndex(); @@ -177,6 +191,7 @@ class VersionBitsTester VersionBitsTester& TestDefined() { return TestState(ThresholdState::DEFINED); } VersionBitsTester& TestStarted() { return TestState(ThresholdState::STARTED); } + VersionBitsTester& TestMustSignal() { return TestState(ThresholdState::MUST_SIGNAL); } VersionBitsTester& TestLockedIn() { return TestState(ThresholdState::LOCKED_IN); } VersionBitsTester& TestActive() { return TestState(ThresholdState::ACTIVE); } VersionBitsTester& TestFailed() { return TestState(ThresholdState::FAILED); } @@ -217,6 +232,22 @@ BOOST_AUTO_TEST_CASE(versionbits_test) .Mine(250, TestTime(30004), 0).TestActive().TestStateSinceHeight(120, 250) .Mine(300, TestTime(40000), 0).TestActive().TestStateSinceHeight(120, 250) + // DEFINED -> STARTED -> MUST_SIGNAL -> LOCKEDIN -> ACTIVE + .Reset().TestDefined() + .SetLockinOnTimeout(true) + .Mine(1, TestTime(1), 0).TestDefined().TestStateSinceHeight(0) + .Mine(99, TestTime(10000) - 1, 0x101).TestDefined().TestStateSinceHeight(0) // One second more and it would be started + .Mine(100, TestTime(10000), 0x101).TestStarted().TestStateSinceHeight(100) // So that's what happens the next period + .Mine(189, TestTime(10010), 0).TestStarted().TestStateSinceHeight(100) + .Mine(190, TestTime(10020), 0).TestMustSignal().TestStateSinceHeight(190) + .Mine(199, TestTime(10030), 0).TestMustSignal().TestStateSinceHeight(190) // 9 new blocks + .Mine(200, TestTime(29999), 0x200).TestLockedIn().TestStateSinceHeight(200) // 1 old block (so 9 out of the past 10) + .Mine(209, TestTime(30001), 0).TestLockedIn().TestStateSinceHeight(200) + .Mine(210, TestTime(30002), 0).TestActiveDelayed().TestStateSinceHeight(210, 200) + .Mine(290, TestTime(30003), 0).TestActive().TestStateSinceHeight(210, 250) + .Mine(390, TestTime(40000), 0).TestActive().TestStateSinceHeight(210, 250) + .SetLockinOnTimeout(false) + // DEFINED multiple periods -> STARTED multiple periods -> FAILED .Reset().TestDefined().TestStateSinceHeight(0) .Mine(9, TestTime(999), 0).TestDefined().TestStateSinceHeight(0) @@ -278,7 +309,7 @@ BOOST_AUTO_TEST_CASE(versionbits_computeblockversion) { // Check that ComputeBlockVersion will set the appropriate bit correctly const auto period = CreateChainParams(*m_node.args, CBaseChainParams::REGTEST)->GetConsensus().nMinerConfirmationWindow; - gArgs.ForceSetArg("-vbparams", strprintf("testdummy:@%s:@%s", period, period * 3)); + gArgs.ForceSetArg("-vbparams", strprintf("testdummy:@%s:@%s:0", period, period * 3)); const auto chainParams = CreateChainParams(*m_node.args, CBaseChainParams::REGTEST); const Consensus::Params ¶ms = chainParams->GetConsensus(); diff --git a/src/validation.cpp b/src/validation.cpp index 2c8641f8e7c43..4d8dc7f64d87c 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -47,6 +47,7 @@ #include #include #include +#include #include #include @@ -1834,7 +1835,7 @@ int32_t ComputeBlockVersion(const CBlockIndex* pindexPrev, const Consensus::Para for (int i = 0; i < (int)Consensus::MAX_VERSION_BITS_DEPLOYMENTS; i++) { ThresholdState state = VersionBitsState(pindexPrev, params, static_cast(i), versionbitscache); - if (state == ThresholdState::LOCKED_IN || state == ThresholdState::STARTED) { + if (state == ThresholdState::LOCKED_IN || state == ThresholdState::MUST_SIGNAL || state == ThresholdState::STARTED) { nVersion |= VersionBitsMask(params, static_cast(i)); } } @@ -1855,6 +1856,7 @@ class WarningBitsConditionChecker : public AbstractThresholdConditionChecker int StartHeight(const Consensus::Params& params) const override { return 0; } int TimeoutHeight(const Consensus::Params& params) const override { return std::numeric_limits::max(); } + bool LockinOnTimeout(const Consensus::Params& params) const override { return false; } int Period(const Consensus::Params& params) const override { return params.nMinerConfirmationWindow; } int Threshold(const Consensus::Params& params) const override { return params.m_vbits_min_threshold; } int MinActivationHeight(const Consensus::Params& params) const override { return 0; } @@ -1941,6 +1943,8 @@ static int64_t nTimeCallbacks = 0; static int64_t nTimeTotal = 0; static int64_t nBlocksTotal = 0; +static bool ContextualCheckBlockHeaderVolatile(const CBlockHeader&, BlockValidationState&, const CChainParams&, const CBlockIndex* pindexPrev) EXCLUSIVE_LOCKS_REQUIRED(cs_main); + /** Apply the effects of this block (with given index) on the UTXO set represented by coins. * Validity checks that depend on the UTXO set are also done; ConnectBlock() * can fail if those validity checks fail (among other reasons). */ @@ -1975,6 +1979,10 @@ bool CChainState::ConnectBlock(const CBlock& block, BlockValidationState& state, return error("%s: Consensus::CheckBlock: %s", __func__, state.ToString()); } + if (!ContextualCheckBlockHeaderVolatile(block, state, chainparams, pindex->pprev)) { + return error("%s: Consensus::ContextualCheckBlockHeaderVolatile: %s", __func__, state.ToString()); + } + // verify that the view's current state corresponds to the previous block uint256 hashPrevBlock = pindex->pprev == nullptr ? uint256() : pindex->pprev->GetBlockHash(); assert(hashPrevBlock == view.GetBestBlock()); @@ -3532,6 +3540,38 @@ static bool ContextualCheckBlockHeader(const CBlockHeader& block, BlockValidatio return state.Invalid(BlockValidationResult::BLOCK_INVALID_HEADER, strprintf("bad-version(0x%08x)", block.nVersion), strprintf("rejected nVersion=0x%08x block", block.nVersion)); + if (!ContextualCheckBlockHeaderVolatile(block, state, params, pindexPrev)) return false; + + return true; +} + +/** Context-dependent validity checks, but rechecked in ConnectBlock(). + * Note that -reindex-chainstate skips the validation that happens here! + */ +static bool ContextualCheckBlockHeaderVolatile(const CBlockHeader& block, BlockValidationState& state, const CChainParams& params, const CBlockIndex* pindexPrev) EXCLUSIVE_LOCKS_REQUIRED(cs_main) +{ + const Consensus::Params& consensusParams = params.GetConsensus(); + + // Enforce MUST_SIGNAL status of deployments + for (int j = 0; j < (int)Consensus::MAX_VERSION_BITS_DEPLOYMENTS; ++j) { + Consensus::DeploymentPos deployment_pos = Consensus::DeploymentPos(j); + ThresholdState deployment_state = VersionBitsState(pindexPrev, consensusParams, deployment_pos, versionbitscache); + if (deployment_state == ThresholdState::MUST_SIGNAL) { + if ((block.nVersion & VersionBitsMask(consensusParams, deployment_pos)) == 0 || (block.nVersion & VERSIONBITS_TOP_MASK) != VERSIONBITS_TOP_BITS) { + BIP9Stats stats = VersionBitsStatistics(pindexPrev, consensusParams, deployment_pos); + if (stats.elapsed == stats.period) { + // first block in new period + stats.count = stats.elapsed = 0; + } + ++stats.elapsed; + if (stats.count + (stats.period - stats.elapsed) < stats.threshold) { + const auto& deployment_name = VersionBitsDeploymentInfo[deployment_pos].name; + return state.Invalid(BlockValidationResult::BLOCK_RECENT_CONSENSUS_CHANGE, std::string{"bad-vbit-unset-"} + deployment_name, std::string{deployment_name} + " must be signalled"); + } + } + } + } + return true; } diff --git a/src/versionbits.cpp b/src/versionbits.cpp index fa2269ff5c15b..25535b5aa1b54 100644 --- a/src/versionbits.cpp +++ b/src/versionbits.cpp @@ -12,6 +12,7 @@ ThresholdState AbstractThresholdConditionChecker::GetStateFor(const CBlockIndex* int height_start = StartHeight(params); int height_timeout = TimeoutHeight(params); int height_active_min = MinActivationHeight(params); + const bool lockinontimeout = LockinOnTimeout(params); // Check if this deployment is never active. if (height_start == Consensus::BIP9Deployment::NEVER_ACTIVE && height_timeout == Consensus::BIP9Deployment::NEVER_ACTIVE) { @@ -79,11 +80,18 @@ ThresholdState AbstractThresholdConditionChecker::GetStateFor(const CBlockIndex* } if (count >= nThreshold) { stateNext = ThresholdState::LOCKED_IN; + } else if (lockinontimeout && height + nPeriod >= height_timeout) { + stateNext = ThresholdState::MUST_SIGNAL; } else if (height >= height_timeout) { stateNext = ThresholdState::FAILED; } break; } + case ThresholdState::MUST_SIGNAL: { + // Always progresses into LOCKED_IN. + stateNext = ThresholdState::LOCKED_IN; + break; + } case ThresholdState::LOCKED_IN: { // Only progress into ACTIVE if minimum activation height has been reached if (height >= height_active_min) { @@ -179,6 +187,7 @@ class VersionBitsConditionChecker : public AbstractThresholdConditionChecker { protected: int StartHeight(const Consensus::Params& params) const override { return params.vDeployments[id].startheight; } int TimeoutHeight(const Consensus::Params& params) const override { return params.vDeployments[id].timeoutheight; } + bool LockinOnTimeout(const Consensus::Params& params) const override { return params.vDeployments[id].lockinontimeout; } int Period(const Consensus::Params& params) const override { return params.nMinerConfirmationWindow; } int Threshold(const Consensus::Params& params) const override { return params.vDeployments[id].threshold; } int MinActivationHeight(const Consensus::Params& params) const override { return params.vDeployments[id].m_min_activation_height; } diff --git a/src/versionbits.h b/src/versionbits.h index 0244339b2ab51..0d8b973714a09 100644 --- a/src/versionbits.h +++ b/src/versionbits.h @@ -25,6 +25,7 @@ static const int32_t VERSIONBITS_NUM_BITS = 29; enum class ThresholdState { DEFINED, // First state that each softfork starts out as. The genesis block is by definition in this state for each deployment. STARTED, // For blocks past the startheight. + MUST_SIGNAL, // If lockinontimeout is true, the period immediately before timeoutheight unless LOCKED_IN is reached first 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. ACTIVE, // For all blocks after the LOCKED_IN retarget period (final state) if the minimum activation height has been reached. FAILED, // For all blocks once the first retarget period after the timeout height is hit, if LOCKED_IN wasn't already reached (final state) @@ -57,6 +58,7 @@ class AbstractThresholdConditionChecker { virtual bool Condition(const CBlockIndex* pindex, const Consensus::Params& params) const =0; virtual int StartHeight(const Consensus::Params& params) const =0; virtual int TimeoutHeight(const Consensus::Params& params) const =0; + virtual bool LockinOnTimeout(const Consensus::Params& params) const =0; virtual int Period(const Consensus::Params& params) const =0; virtual int Threshold(const Consensus::Params& params) const =0; virtual int MinActivationHeight(const Consensus::Params& params) const =0; diff --git a/test/functional/feature_bip8.py b/test/functional/feature_bip8.py index 9dda344e721d8..57d56c26a7933 100755 --- a/test/functional/feature_bip8.py +++ b/test/functional/feature_bip8.py @@ -15,8 +15,8 @@ def set_test_params(self): self.setup_clean_chain = True self.extra_args = [ ['-vbparams=testdummy:@-2:@-2'], # Node 0 has TestDummy inactive - ['-vbparams=testdummy:@144:@{}'.format(144 * 3)], # Node 1 has regular activation window - ['-vbparams=testdummy:@144:@{}:@{}'.format(144 * 3, 144 * 5)], # Node 2 has minimum activation height + ['-vbparams=testdummy:@144:@{}:0'.format(144 * 3)], # Node 1 has regular activation window + ['-vbparams=testdummy:@144:@{}:@{}:0'.format(144 * 3, 144 * 5)], # Node 2 has minimum activation height ['-vbparams=testdummy:@-2:@-2'], # Node 3 has TestDummy inactive, but will be restarted with different params ] @@ -57,6 +57,7 @@ def test_height(self, height, status, mine_from, restart): if restart: # Restart this node and check that the status is what we expect + restart.extra_args[0] += ':0' # LockinOnTimeout self.restart_node(restart.node, restart.extra_args) info = self.nodes[restart.node].getblockchaininfo() assert_equal(info["blocks"], height) @@ -78,18 +79,18 @@ def run_test(self): self.log.info("Checking -vbparams") self.stop_node(3) self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@-2:@1"], expected_msg="Error: When one of startheight or timeoutheight is -2, both must be -2") - self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@1:@-2"], expected_msg="Error: When one of startheight or timeoutheight is -2, both must be -2") - self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@1:@1"], expected_msg="Error: Invalid startheight (1), must be a multiple of 144") - self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@144:@1"], expected_msg="Error: Invalid timeoutheight (1), must be a multiple of 144") - self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@144:@144:@-1"], expected_msg="Error: Invalid minimum activation height (-1), cannot be negative") - self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@144:@144:@1"], expected_msg="Error: Invalid minimum activation height (1), must be a multiple of 144") - self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@288:@144"], expected_msg="Error: Invalid timeoutheight (144), must be at least two periods greater than the startheight (288)") + self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@1:@-2:0"], expected_msg="Error: When one of startheight or timeoutheight is -2, both must be -2") + self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@1:@1:0"], expected_msg="Error: Invalid startheight (1), must be a multiple of 144") + self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@144:@1:0"], expected_msg="Error: Invalid timeoutheight (1), must be a multiple of 144") + self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@144:@144:@-1:0"], expected_msg="Error: Invalid minimum activation height (-1), cannot be negative") + self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@144:@144:@1:0"], expected_msg="Error: Invalid minimum activation height (1), must be a multiple of 144") + self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@288:@144:0"], expected_msg="Error: Invalid timeoutheight (144), must be at least two periods greater than the startheight (288)") self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@-3:@144"], expected_msg="Error: Invalid startheight (-3), cannot be negative (except for never or always active special cases)") - self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@144:@-1"], expected_msg="Error: Invalid timeoutheight (-1), cannot be negative (except for never or always active special cases)") - self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@{}:@144".format(0x7fffffff + 1)], expected_msg="Error: Invalid startheight (@{})".format(0x7fffffff + 1)) - self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@144:@{}".format(0x7fffffff + 1)], expected_msg="Error: Invalid timeoutheight (@{})".format(0x7fffffff + 1)) - self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@{}:@144".format(-(0x7fffffff + 2))], expected_msg="Error: Invalid startheight (@{})".format(-(0x7fffffff + 2))) - self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@144:@{}".format(-(0x7fffffff + 2))], expected_msg="Error: Invalid timeoutheight (@{})".format(-(0x7fffffff + 2))) + self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@144:@-1:0"], expected_msg="Error: Invalid timeoutheight (-1), cannot be negative (except for never or always active special cases)") + self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@{}:@144:0".format(0x7fffffff + 1)], expected_msg="Error: Invalid startheight (@{})".format(0x7fffffff + 1)) + self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@144:@{}:0".format(0x7fffffff + 1)], expected_msg="Error: Invalid timeoutheight (@{})".format(0x7fffffff + 1)) + self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@{}:@144:0".format(-(0x7fffffff + 2))], expected_msg="Error: Invalid startheight (@{})".format(-(0x7fffffff + 2))) + self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@144:@{}:0".format(-(0x7fffffff + 2))], expected_msg="Error: Invalid timeoutheight (@{})".format(-(0x7fffffff + 2))) self.start_node(3, self.extra_args[3]) self.height = 0 diff --git a/test/functional/feature_bip8b.py b/test/functional/feature_bip8b.py new file mode 100755 index 0000000000000..9f4113c242fff --- /dev/null +++ b/test/functional/feature_bip8b.py @@ -0,0 +1,277 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test bip8 activation +""" + +import random +import time + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal, assert_greater_than_or_equal +from test_framework.blocktools import create_block, create_coinbase +from test_framework.p2p import P2PDataStore, p2p_lock +from test_framework.messages import CBlockHeader, msg_headers + +SEED = random.randrange(2**128) + +VB_SIGNAL = 0x20000000 | (0x01 << 28) +VB_NOSIGNAL = 0x20000000 + +MAX_HEADERS = 2000 + +BASE_TIME = int(time.time()) - 12*60*60 + +class P2PBlockCheck(P2PDataStore): + def __init__(self, node, blocks): + super().__init__() + self.node = node + with p2p_lock: + for block in blocks: + self.block_store[block.sha256] = block + self.last_block_hash = block.sha256 + + def refresh_headers(self): + headers = [] + with p2p_lock: + for _, block in self.block_store.items(): + headers.append(CBlockHeader(block)) + while headers: + self.send_message(msg_headers(headers[:MAX_HEADERS])) + headers = headers[MAX_HEADERS:] + + def send_blocks(self, blocks, *, reject_reason=None, timeout=20): + tiphash = blocks[-1].hash + + with p2p_lock: + for block in blocks: + self.block_store[block.sha256] = block + self.last_block_hash = block.sha256 + + reject_reason = [reject_reason] if reject_reason else [] + success = reject_reason == [] + + with self.node.assert_debug_log(expected_msgs=reject_reason, timeout=timeout): + self.send_message(msg_headers([CBlockHeader(block) for block in blocks])) + self.sync_with_ping(timeout) + if success: + self.wait_until(lambda: self.node.getbestblockhash() == tiphash, timeout=timeout) + + if not success: + assert self.node.getbestblockhash() != tiphash + +class BIP8Test(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 4 + + def setup_network(self): + # don't want to connect the bitcoinds to each other + self.setup_nodes() + + def generate_blocks(self, versioniter, prevblock, tipheight): + test_blocks = [] + + for version in versioniter: + blocktime = BASE_TIME + (tipheight * 6) # 6 seconds between blocks + block = create_block(prevblock, create_coinbase(tipheight), blocktime) + block.nVersion = version + block.rehash() + block.solve() + test_blocks.append(block) + prevblock = block.sha256 + tipheight = tipheight + 1 + + return test_blocks + + def apply_blocks_and_check(self, versions, base_height, state, height=None): + start = base_height + 288 + stop = base_height + 720 + + if height is None: + bci = self.nodes[0].getblockchaininfo() + prevblock = int("0x" + bci["bestblockhash"], 0) + tipheight = bci["blocks"] + 1 + else: + prevblock = int("0x" + self.nodes[0].getblockhash(height), 0) + tipheight = height + 1 + + blocks = self.generate_blocks(versions, prevblock, tipheight) + + self.helper[0].send_blocks(blocks) + self.helper[1].send_blocks(blocks) + self.helper[2].send_blocks(blocks) + if state[0] == "stopped": + ok_cnt = state[1]-tipheight + if ok_cnt > 0: + self.helper[3].send_blocks(blocks[:ok_cnt]) + if ok_cnt >= 0: + self.helper[3].send_blocks(blocks[ok_cnt:], reject_reason="bad-vbit-unset-testdummy") + else: + self.helper[3].send_blocks(blocks) + + heights, status = self.get_softfork_status() + + # compare results + assert_equal(heights[0], heights[1], heights[2]) + assert_greater_than_or_equal(heights[0], heights[3]) + if state[0] != "stopped": + assert_equal(heights[0], heights[3]) + + if state[0] != "stopped": + assert_equal(status[2]["bip8"].get("statistics", None), status[3]["bip8"].get("statistics", None)) + assert_equal(status[2]["active"], status[3]["active"]) + + # never active + assert_equal(status[0], None) + + # always active + assert_equal(status[1], {'type': 'bip8', 'bip8': {'status': 'active', 'startheight': -1, 'timeoutheight': 0, 'minimum_activation_height': 0, 'lockinontimeout': True, 'since': 0}, 'height': 0, 'active': True}) + + # lockinontimeout=false + if "statistics" in status[2]["bip8"]: + status[2]["bip8"]["statistics"] = None + + if state[0] == "defined": + assert_equal(status[2], {'type': 'bip8', 'bip8': {'status': 'defined', 'startheight': start, 'timeoutheight': stop, 'minimum_activation_height': 0, 'lockinontimeout': False, 'since': state[1]}, 'active': False}) + elif state[0] == "started" or state[0] == "must_signal": + since = state[1] if state[0] == "started" else (state[1] - 288) + assert_equal(status[2], {'type': 'bip8', 'bip8': {'status': 'started', 'bit': 28, 'startheight': start, 'timeoutheight': stop, 'minimum_activation_height': 0, 'lockinontimeout': False, 'since': since, 'statistics': None}, 'active': False}) + elif state[0] == "locked_in": + assert_equal(status[2], {'type': 'bip8', 'bip8': {'status': 'locked_in', 'bit': 28, 'startheight': start, 'timeoutheight': stop, 'minimum_activation_height': 0, 'lockinontimeout': False, 'since': state[1], 'statistics': None}, 'active': False}) + elif state[0] == "active": + assert_equal(status[2], {'type': 'bip8', 'bip8': {'status': 'active', 'startheight': start, 'timeoutheight': stop, 'minimum_activation_height': 0, 'lockinontimeout': False, 'since': state[1]}, 'height': state[1], 'active': True}) + elif state[0] == "stopped": + failat = state[1] - (state[1] % 144) + 144 + assert_equal(status[2], {'type': 'bip8', 'bip8': {'status': 'failed', 'startheight': start, 'timeoutheight': stop, 'minimum_activation_height': 0, 'lockinontimeout': False, 'since': failat}, 'active': False}) + else: + assert False, ("bad state %r" % (state)) + + # lockinontimeout=true + if "statistics" in status[3]["bip8"]: + assert_equal(status[3]["bip8"]["statistics"]["possible"], True) + status[3]["bip8"]["statistics"] = None + + if state[0] == "defined": + assert_equal(status[3], {'type': 'bip8', 'bip8': {'status': 'defined', 'startheight': start, 'timeoutheight': stop, 'minimum_activation_height': 0, 'lockinontimeout': True, 'since': state[1]}, 'active': False}) + elif state[0] == "started": + assert_equal(status[3], {'type': 'bip8', 'bip8': {'status': 'started', 'bit': 28, 'startheight': start, 'timeoutheight': stop, 'minimum_activation_height': 0, 'lockinontimeout': True, 'since': state[1], 'statistics': None}, 'active': False}) + elif state[0] == "must_signal" or state[0] == "stopped": + since = state[1] - state[1] % 144 + assert_equal(status[3], {'type': 'bip8', 'bip8': {'status': 'must_signal', 'bit': 28, 'startheight': start, 'timeoutheight': stop, 'minimum_activation_height': 0, 'lockinontimeout': True, 'since': since, 'statistics': None}, 'active': False}) + elif state[0] == "locked_in": + assert_equal(status[3], {'type': 'bip8', 'bip8': {'status': 'locked_in', 'bit': 28, 'startheight': start, 'timeoutheight': stop, 'minimum_activation_height': 0, 'lockinontimeout': True, 'since': state[1], 'statistics': None}, 'active': False}) + elif state[0] == "active": + assert_equal(status[3], {'type': 'bip8', 'bip8': {'status': 'active', 'startheight': start, 'timeoutheight': stop, 'minimum_activation_height': 0, 'lockinontimeout': True, 'since': state[1]}, 'height': state[1], 'active': True}) + else: + assert False, ("bad state %r" % (state)) + + return blocks + + + def get_softfork_status(self): + info = [node.getblockchaininfo() for node in self.nodes] + return zip( *[(i["blocks"], i['softforks'].get('testdummy', None)) + for i in info] ) + + def setup_vbparams(self): + base_height = self.nodes[0].getblockcount() + if base_height: + base_height += 1 + assert base_height % 144 == 0 + start = base_height + 288 + stop = base_height + 720 + + for node in self.nodes: + node.disconnect_p2ps() + self.helper = [] + self.stop_nodes() + + self.start_nodes(extra_args = [ + # never active + ["-vbparams=testdummy:@-2:@-2:0"], + # always active + ["-vbparams=testdummy:@-1:@0:1"], + # abandon softfork if not locked in by timeout + ["-vbparams=testdummy:@%s:@%s:0" % (start, stop)], + # reject blocks if not locked in by timeout + ["-vbparams=testdummy:@%s:@%s:1" % (start, stop)], + ]) + for n in range(1, 4): + self.connect_nodes(0, n) + self.sync_all() + for n in range(1, 4): + self.disconnect_nodes(0, n) + self.helper = [n.add_p2p_connection(P2PBlockCheck(n, self.all_blocks)) for n in self.nodes] + return base_height + + def do_test(self, signalling, expstate, expheight): + base_height = self.setup_vbparams() + + # track the expected state/height for rejecting-node + state = "defined", 0 + + period = 0 + for cnt in signalling: + nblocks = 144 + if base_height == 0 and period == 0: + nblocks -= 1 + cnt = max(0, min(cnt, nblocks)) + bits = [VB_SIGNAL]*cnt + [VB_NOSIGNAL]*(nblocks - cnt) + + random.shuffle(bits) + + # what will the state be after these blocks are mined for a lockinontimeout=true node? + period += 1 + if state[0] == "defined" and period == 2: + state = "started", base_height + (period*144) + elif state[0] == "started": + if cnt >= 108: + state = "locked_in", base_height + (period*144) + elif period == 4: + state = "must_signal", base_height + (period*144) + elif state[0] == "must_signal": + if cnt >= 108: + state = "locked_in", base_height + (period*144) + else: + oknosig = 144-108 + howmany = None + for i, x in enumerate(bits): + if x != VB_SIGNAL: + if oknosig == 0: + howmany = i + break + oknosig -= 1 + state = "stopped", base_height + (period*144) - 144 + howmany + elif state[0] == "locked_in": + state = "active", base_height + (period*144) + + self.all_blocks.extend(self.apply_blocks_and_check(bits, base_height, state)) + + assert_equal(state[0], expstate) + assert_greater_than_or_equal(state[1] - base_height, expheight) + if expstate != "stopped": + assert_equal(state[1] - base_height, expheight) + else: + last_possible = expheight - expheight%144 + 143 + assert_greater_than_or_equal(last_possible, state[1] - base_height) + self.log.info("Completed test %s %s" % (base_height, state)) + + def run_test(self): + self.all_blocks = [] + + random.seed(SEED) + N = random.randrange(0,108) + Y = random.randrange(108,145) + self.log.info("seed %s, signal vals %s/%s" % (SEED, Y, N)) + + self.do_test([N,N,N,N,107,144], "stopped", 612) + self.do_test([144,144,Y,0,0,0], "active", 576) + self.do_test([Y,Y,0,0,0,Y], "stopped", 612) + self.do_test([N,N,N,N,N,144], "stopped", 612) + self.do_test([0,0,0,Y,0,0], "active", 720) + self.do_test([0,144,N,N,Y,0], "active", 864) + +if __name__ == '__main__': + BIP8Test().main() diff --git a/test/functional/rpc_blockchain.py b/test/functional/rpc_blockchain.py index fb0e333185801..e0b9266664813 100755 --- a/test/functional/rpc_blockchain.py +++ b/test/functional/rpc_blockchain.py @@ -138,6 +138,7 @@ def _test_getblockchaininfo(self): 'startheight': 144, 'timeoutheight': 0x7fffffff, # testdummy does not have a timeout so is set to the max int value 'minimum_activation_height': 0, + 'lockinontimeout': False, 'since': 144, 'statistics': { 'period': 144, @@ -156,6 +157,7 @@ def _test_getblockchaininfo(self): 'startheight': -1, 'timeoutheight': 0x7fffffff, 'minimum_activation_height': 0, + 'lockinontimeout': False, 'since': 0 }, 'height': 0, diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index c2ee370cc1ec7..22e0a674e5c98 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -90,6 +90,7 @@ 'wallet_hd.py --descriptors', 'wallet_backup.py', 'wallet_backup.py --descriptors', + 'feature_bip8b.py', # vv Tests less than 5m vv 'mining_getblocktemplate_longpoll.py', 'feature_maxuploadtarget.py',