diff --git a/doc/REST-interface.md b/doc/REST-interface.md index 4d7c0a873ac1b..f27bb9f3eff4b 100644 --- a/doc/REST-interface.md +++ b/doc/REST-interface.md @@ -85,6 +85,15 @@ Returns various state info regarding block chain processing. Only supports JSON as output format. Refer to the `getblockchaininfo` RPC help for details. +#### Deployment info +`GET /rest/deploymentinfo.json` +`GET /rest/deploymentinfo/.json` + +Returns an object containing various state info regarding deployments of +consensus changes at the current chain tip, or at if provided. +Only supports JSON as output format. +Refer to the `getdeploymentinfo` RPC help for details. + #### Query UTXO set - `GET /rest/getutxos/-/-/.../-.` - `GET /rest/getutxos/checkmempool/-/-/.../-.` diff --git a/doc/release-notes-6888.md b/doc/release-notes-6888.md new file mode 100644 index 0000000000000..82021659f1291 --- /dev/null +++ b/doc/release-notes-6888.md @@ -0,0 +1,17 @@ +Updated RPCs +------------ + +- Information on soft fork status has been moved from `getblockchaininfo` + to the new `getdeploymentinfo` RPC which allows querying soft fork status at any + block, rather than just at the chain tip. Inclusion of soft fork + status in `getblockchaininfo` can currently be restored using the + configuration `-deprecatedrpc=softforks`, but this will be removed in + a future release. Note that in either case, the `status` field + now reflects the status of the current block rather than the next + block. + +New REST endpoint +----------------- + +- A new `/rest/deploymentinfo` endpoint has been added for fetching various + state info regarding deployments of consensus changes. diff --git a/src/rest.cpp b/src/rest.cpp index 5b6222724572a..196dbd60f3bb8 100644 --- a/src/rest.cpp +++ b/src/rest.cpp @@ -620,6 +620,48 @@ static bool rest_chaininfo(const CoreContext& context, HTTPRequest* req, const s } } + +RPCHelpMan getdeploymentinfo(); + +static bool rest_deploymentinfo(const CoreContext& context, HTTPRequest* req, const std::string& str_uri_part) +{ + if (!CheckWarmup(req)) return false; + + std::string hash_str; + const RESTResponseFormat rf = ParseDataFormat(hash_str, str_uri_part); + + switch (rf) { + case RESTResponseFormat::JSON: { + JSONRPCRequest jsonRequest; + jsonRequest.context = context; + jsonRequest.params = UniValue(UniValue::VARR); + + if (!hash_str.empty()) { + uint256 hash; + if (!ParseHashStr(hash_str, hash)) { + return RESTERR(req, HTTP_BAD_REQUEST, "Invalid hash: " + hash_str); + } + + const ChainstateManager* chainman = GetChainman(context, req); + if (!chainman) return false; + if (!WITH_LOCK(::cs_main, return chainman->m_blockman.LookupBlockIndex(ParseHashV(hash_str, "blockhash")))) { + return RESTERR(req, HTTP_BAD_REQUEST, "Block not found"); + } + + jsonRequest.params.pushKV("blockhash", hash_str); + } + + req->WriteHeader("Content-Type", "application/json"); + req->WriteReply(HTTP_OK, getdeploymentinfo().HandleRequest(jsonRequest).write() + "\n"); + return true; + } + default: { + return RESTERR(req, HTTP_NOT_FOUND, "output format not found (available: json)"); + } + } + +} + static bool rest_mempool_info(const CoreContext& context, HTTPRequest* req, const std::string& strURIPart) { if (!CheckWarmup(req)) @@ -986,6 +1028,8 @@ static const struct { {"/rest/mempool/contents", rest_mempool_contents}, {"/rest/headers/", rest_headers}, {"/rest/getutxos", rest_getutxos}, + {"/rest/deploymentinfo/", rest_deploymentinfo}, + {"/rest/deploymentinfo", rest_deploymentinfo}, {"/rest/blockhashbyheight/", rest_blockhash_by_height}, }; diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index 505fce3303a96..18c85b12f4ee4 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -1367,7 +1367,7 @@ static RPCHelpMan verifychain() }; } -static void SoftForkDescPushBack(const CBlockIndex* active_chain_tip, UniValue& softforks, const Consensus::Params& params, Consensus::BuriedDeployment dep) +static void SoftForkDescPushBack(const CBlockIndex* blockindex, UniValue& softforks, const Consensus::Params& params, Consensus::BuriedDeployment dep) { // For buried deployments. @@ -1375,29 +1375,39 @@ static void SoftForkDescPushBack(const CBlockIndex* active_chain_tip, UniValue& UniValue rv(UniValue::VOBJ); rv.pushKV("type", "buried"); - // getblockchaininfo reports the softfork as active from when the chain height is + // getdeploymentinfo reports the softfork as active from when the chain height is // one below the activation height - rv.pushKV("active", DeploymentActiveAfter(active_chain_tip, params, dep)); + rv.pushKV("active", DeploymentActiveAfter(blockindex, params, dep)); rv.pushKV("height", params.DeploymentHeight(dep)); softforks.pushKV(DeploymentName(dep), rv); } -static void SoftForkDescPushBack(const CBlockIndex* active_chain_tip, const std::unordered_map& signals, UniValue& softforks, const Consensus::Params& consensusParams, Consensus::DeploymentPos id) +static void SoftForkDescPushBack(const CBlockIndex* blockindex, const std::unordered_map& signals, UniValue& softforks, const Consensus::Params& consensusParams, Consensus::DeploymentPos id) { // For BIP9 deployments. if (!DeploymentEnabled(consensusParams, id)) return; + if (blockindex == nullptr) return; + + auto get_state_name = [](const ThresholdState state) -> std::string { + switch (state) { + case ThresholdState::DEFINED: return "defined"; + case ThresholdState::STARTED: return "started"; + case ThresholdState::LOCKED_IN: return "locked_in"; + case ThresholdState::ACTIVE: return "active"; + case ThresholdState::FAILED: return "failed"; + } + return "invalid"; + }; UniValue bip9(UniValue::VOBJ); - const ThresholdState thresholdState = g_versionbitscache.State(active_chain_tip, consensusParams, id); - switch (thresholdState) { - case ThresholdState::DEFINED: bip9.pushKV("status", "defined"); break; - case ThresholdState::STARTED: bip9.pushKV("status", "started"); 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; - } - const bool has_signal = (ThresholdState::STARTED == thresholdState || ThresholdState::LOCKED_IN == thresholdState); + + const ThresholdState next_state = g_versionbitscache.State(blockindex, consensusParams, id); + const ThresholdState current_state = g_versionbitscache.State(blockindex->pprev, consensusParams, id); + + const bool has_signal = (ThresholdState::STARTED == current_state || ThresholdState::LOCKED_IN == current_state); + + // BIP9 parameters if (has_signal) { bip9.pushKV("bit", consensusParams.vDeployments[id].bit); } @@ -1407,38 +1417,60 @@ static void SoftForkDescPushBack(const CBlockIndex* active_chain_tip, const std: if (auto it = signals.find(consensusParams.vDeployments[id].bit); it != signals.end()) { bip9.pushKV("ehf_height", it->second); } - int64_t since_height = g_versionbitscache.StateSinceHeight(active_chain_tip, consensusParams, id); + bip9.pushKV("min_activation_height", consensusParams.vDeployments[id].min_activation_height); + + // BIP9 status + bip9.pushKV("status", get_state_name(current_state)); + int64_t since_height = g_versionbitscache.StateSinceHeight(blockindex->pprev, consensusParams, id); bip9.pushKV("since", since_height); + bip9.pushKV("status_next", get_state_name(next_state)); + + // BIP9 signalling status, if applicable if (has_signal) { UniValue statsUV(UniValue::VOBJ); - BIP9Stats statsStruct = g_versionbitscache.Statistics(active_chain_tip, consensusParams, id); + std::vector signals; + BIP9Stats statsStruct = g_versionbitscache.Statistics(blockindex, consensusParams, id, &signals); statsUV.pushKV("period", statsStruct.period); statsUV.pushKV("elapsed", statsStruct.elapsed); statsUV.pushKV("count", statsStruct.count); - if (ThresholdState::LOCKED_IN != thresholdState) { + if (ThresholdState::LOCKED_IN != current_state) { statsUV.pushKV("threshold", statsStruct.threshold); statsUV.pushKV("possible", statsStruct.possible); } bip9.pushKV("statistics", statsUV); + + std::string sig; + sig.reserve(signals.size()); + for (const bool s : signals) { + sig.push_back(s ? '#' : '-'); + } + bip9.pushKV("signalling", sig); } - if (ThresholdState::LOCKED_IN == thresholdState) { + if (ThresholdState::LOCKED_IN == current_state) { bip9.pushKV("activation_height", since_height + static_cast(consensusParams.vDeployments[id].nWindowSize)); } - bip9.pushKV("min_activation_height", consensusParams.vDeployments[id].min_activation_height); UniValue rv(UniValue::VOBJ); rv.pushKV("type", "bip9"); - rv.pushKV("bip9", bip9); - if (ThresholdState::ACTIVE == thresholdState) { - rv.pushKV("height", since_height); + if (ThresholdState::ACTIVE == next_state) { + rv.pushKV("height", g_versionbitscache.StateSinceHeight(blockindex, consensusParams, id)); } - rv.pushKV("active", ThresholdState::ACTIVE == thresholdState); + rv.pushKV("active", ThresholdState::ACTIVE == next_state); + rv.pushKV("bip9", bip9); softforks.pushKV(DeploymentName(id), rv); } +namespace { +/* TODO: when -deprecatedrpc=softforks is removed, drop these */ +UniValue DeploymentInfo(const CBlockIndex* tip, const CMNHFManager::Signals& ehf_signals, const Consensus::Params& consensusParams); +extern const std::vector RPCHelpForDeployment; +} + +// used by rest.cpp:rest_chaininfo, so cannot be static RPCHelpMan getblockchaininfo() { + /* TODO: from v24, remove -deprecatedrpc=softforks */ return RPCHelpMan{"getblockchaininfo", "Returns an object containing various state info regarding blockchain processing.\n", {}, @@ -1461,37 +1493,17 @@ RPCHelpMan getblockchaininfo() {RPCResult::Type::NUM, "pruneheight", /*optional=*/true, "height of the last block pruned, plus one (only present if pruning is enabled)"}, {RPCResult::Type::BOOL, "automatic_pruning", /*optional=*/true, "whether automatic pruning is enabled (only present if pruning is enabled)"}, {RPCResult::Type::NUM, "prune_target_size", /*optional=*/true, "the target size used by pruning (only present if automatic pruning is enabled)"}, - {RPCResult::Type::OBJ, "softforks", "status of softforks in progress", + {RPCResult::Type::OBJ_DYN, "softforks", /*optional=*/true, "(DEPRECATED, returned only if config option -deprecatedrpc=softforks is passed) status of softforks in progress", { - {RPCResult::Type::STR, "type", "one of \"buried\", \"bip9\""}, - {RPCResult::Type::OBJ, "bip9", /*optional=*/true, "status of bip9 softforks (only for \"bip9\" type)", - { - {RPCResult::Type::STR, "status", "one of \"defined\", \"started\", \"locked_in\", \"active\", \"failed\""}, - {RPCResult::Type::NUM, "bit", /*optional=*/true, "the bit (0-28) in the block version field used to signal this softfork (only for \"started\" and \"locked_in\" status)"}, - {RPCResult::Type::NUM_TIME, "start_time", "the minimum median time past of a block at which the bit gains its meaning"}, - {RPCResult::Type::NUM_TIME, "timeout", "the median time past of a block at which the deployment is considered failed if not yet locked in"}, - {RPCResult::Type::BOOL, "ehf", "returns true for EHF activated forks"}, - {RPCResult::Type::NUM, "ehf_height", /*optional=*/true, "the minimum height when miner's signals for the deployment matter. Below this height miner signaling cannot trigger hard fork lock-in. Not specified for non-EHF forks"}, - {RPCResult::Type::NUM, "since", "height of the first block to which the status applies"}, - {RPCResult::Type::NUM, "activation_height", "expected activation height for this softfork (only for \"locked_in\" status)"}, - {RPCResult::Type::NUM, "min_activation_height", "minimum height of blocks for which the rules may be enforced"}, - {RPCResult::Type::OBJ, "statistics", /*optional=*/true, "numeric statistics about signalling for a softfork (only for \"started\" and \"locked_in\" status)", - { - {RPCResult::Type::NUM, "period", "the length in blocks of the signalling period"}, - {RPCResult::Type::NUM, "threshold", /*optional=*/true, "the number of blocks with the version bit set required to activate the feature (only for \"started\" status)"}, - {RPCResult::Type::NUM, "elapsed", "the number of blocks elapsed since the beginning of the current period"}, - {RPCResult::Type::NUM, "count", "the number of blocks with the version bit set in the current period"}, - {RPCResult::Type::BOOL, "possible", /*optional=*/true, "returns false if there are not enough blocks left in this period to pass activation threshold (only for \"started\" status)"}, - }}, - }}, - {RPCResult::Type::NUM, "height", /*optional=*/true, "height of the first block which the rules are or will be enforced (only for \"buried\" type, or \"bip9\" type with \"active\" status)"}, - {RPCResult::Type::BOOL, "active", "true if the rules are enforced for the mempool and the next block"}, + {RPCResult::Type::OBJ, "xxxx", "name of the softfork", + RPCHelpForDeployment + }, }}, {RPCResult::Type::STR, "warnings", "any network and blockchain warnings"}, }}, RPCExamples{ HelpExampleCli("getblockchaininfo", "") - + HelpExampleRpc("getblockchaininfo", "") + + HelpExampleRpc("getblockchaininfo", "") }, [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { @@ -1506,8 +1518,6 @@ RPCHelpMan getblockchaininfo() const CBlockIndex& tip{*CHECK_NONFATAL(active_chainstate.m_chain.Tip())}; const int height{tip.nHeight}; - const auto ehfSignals{CHECK_NONFATAL(node.mnhf_manager)->GetSignalsStage(&tip)}; - UniValue obj(UniValue::VOBJ); if (args.IsArgSet("-devnet")) { obj.pushKV("chain", args.GetDevNetName()); @@ -1536,7 +1546,49 @@ RPCHelpMan getblockchaininfo() } } - const Consensus::Params& consensusParams = Params().GetConsensus(); + if (IsDeprecatedRPCEnabled("softforks")) { + const auto ehf_signals{CHECK_NONFATAL(node.mnhf_manager)->GetSignalsStage(&tip)}; + const Consensus::Params& consensusParams = Params().GetConsensus(); + obj.pushKV("softforks", DeploymentInfo(&tip, ehf_signals, consensusParams)); + } + + obj.pushKV("warnings", GetWarnings(false).original); + return obj; +}, + }; +} + +namespace { +const std::vector RPCHelpForDeployment{ + {RPCResult::Type::STR, "type", "one of \"buried\", \"bip9\""}, + {RPCResult::Type::NUM, "height", /*optional=*/true, "height of the first block which the rules are or will be enforced (only for \"buried\" type, or \"bip9\" type with \"active\" status)"}, + {RPCResult::Type::BOOL, "active", "true if the rules are enforced for the mempool and the next block"}, + {RPCResult::Type::OBJ, "bip9", /*optional=*/true, "status of bip9 softforks (only for \"bip9\" type)", + { + {RPCResult::Type::NUM, "bit", /*optional=*/true, "the bit (0-28) in the block version field used to signal this softfork (only for \"started\" and \"locked_in\" status)"}, + {RPCResult::Type::NUM_TIME, "start_time", "the minimum median time past of a block at which the bit gains its meaning"}, + {RPCResult::Type::NUM_TIME, "timeout", "the median time past of a block at which the deployment is considered failed if not yet locked in"}, + {RPCResult::Type::BOOL, "ehf", "returns true for EHF activated forks"}, + {RPCResult::Type::NUM, "ehf_height", /*optional=*/true, "the minimum height when miner's signals for the deployment matter. Below this height miner signaling cannot trigger hard fork lock-in. Not specified for non-EHF forks"}, + {RPCResult::Type::NUM, "activation_height", "expected activation height for this softfork (only for \"locked_in\" status)"}, + {RPCResult::Type::NUM, "min_activation_height", "minimum height of blocks for which the rules may be enforced"}, + {RPCResult::Type::STR, "status", "status of deployment at specified block (one of \"defined\", \"started\", \"locked_in\", \"active\", \"failed\")"}, + {RPCResult::Type::NUM, "since", "height of the first block to which the status applies"}, + {RPCResult::Type::STR, "status_next", "status of deployment at the next block"}, + {RPCResult::Type::OBJ, "statistics", /*optional=*/true, "numeric statistics about signalling for a softfork (only for \"started\" and \"locked_in\" status)", + { + {RPCResult::Type::NUM, "period", "the length in blocks of the signalling period"}, + {RPCResult::Type::NUM, "threshold", /*optional=*/true, "the number of blocks with the version bit set required to activate the feature (only for \"started\" status)"}, + {RPCResult::Type::NUM, "elapsed", "the number of blocks elapsed since the beginning of the current period"}, + {RPCResult::Type::NUM, "count", "the number of blocks with the version bit set in the current period"}, + {RPCResult::Type::BOOL, "possible", /*optional=*/true, "returns false if there are not enough blocks left in this period to pass activation threshold (only for \"started\" status)"}, + }}, + {RPCResult::Type::STR, "signalling", /*optional=*/true, "indicates blocks that signalled with a # and blocks that did not with a -"}, + }}, +}; + +UniValue DeploymentInfo(const CBlockIndex* blockindex, const CMNHFManager::Signals& ehf_signals, const Consensus::Params& consensusParams) +{ UniValue softforks(UniValue::VOBJ); for (auto deploy : { /* sorted by activation block */ Consensus::DEPLOYMENT_HEIGHTINCB, @@ -1555,18 +1607,63 @@ RPCHelpMan getblockchaininfo() Consensus::DEPLOYMENT_MN_RR, Consensus::DEPLOYMENT_WITHDRAWALS, }) { - SoftForkDescPushBack(&tip, softforks, consensusParams, deploy); + SoftForkDescPushBack(blockindex, softforks, consensusParams, deploy); } for (auto ehf_deploy : { /* sorted by activation block */ Consensus::DEPLOYMENT_V24, Consensus::DEPLOYMENT_TESTDUMMY }) { - SoftForkDescPushBack(&tip, ehfSignals, softforks, consensusParams, ehf_deploy); + SoftForkDescPushBack(blockindex, ehf_signals, softforks, consensusParams, ehf_deploy); } - obj.pushKV("softforks", softforks); + return softforks; +} +} // anon namespace - obj.pushKV("warnings", GetWarnings(false).original); - return obj; -}, +RPCHelpMan getdeploymentinfo() +{ + return RPCHelpMan{"getdeploymentinfo", + "Returns an object containing various state info regarding deployments of consensus changes.", + { + {"blockhash", RPCArg::Type::STR_HEX, RPCArg::Default{"hash of current chain tip"}, "The block hash at which to query deployment state"}, + }, + RPCResult{ + RPCResult::Type::OBJ, "", "", { + {RPCResult::Type::STR, "hash", "requested block hash (or tip)"}, + {RPCResult::Type::NUM, "height", "requested block height (or tip)"}, + {RPCResult::Type::OBJ_DYN, "deployments", "", { + {RPCResult::Type::OBJ, "xxxx", "name of the deployment", RPCHelpForDeployment} + }}, + } + }, + RPCExamples{ HelpExampleCli("getdeploymentinfo", "") + HelpExampleRpc("getdeploymentinfo", "") }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue + { + const NodeContext& node = EnsureAnyNodeContext(request.context); + + const ChainstateManager& chainman = EnsureChainman(node); + LOCK(cs_main); + const CChainState& active_chainstate = chainman.ActiveChainstate(); + + const CBlockIndex* blockindex; + if (request.params[0].isNull()) { + blockindex = active_chainstate.m_chain.Tip(); + CHECK_NONFATAL(blockindex); + } else { + const uint256 hash(ParseHashV(request.params[0], "blockhash")); + blockindex = chainman.m_blockman.LookupBlockIndex(hash); + if (!blockindex) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found"); + } + } + + const Consensus::Params& consensusParams = Params().GetConsensus(); + const auto ehf_signals{CHECK_NONFATAL(node.mnhf_manager)->GetSignalsStage(blockindex)}; + + UniValue deploymentinfo(UniValue::VOBJ); + deploymentinfo.pushKV("hash", blockindex->GetBlockHash().ToString()); + deploymentinfo.pushKV("height", blockindex->nHeight); + deploymentinfo.pushKV("deployments", DeploymentInfo(blockindex, ehf_signals, consensusParams)); + return deploymentinfo; + }, }; } @@ -2777,9 +2874,10 @@ void RegisterBlockchainRPCCommands(CRPCTable& t) {"blockchain", &getblockhash}, {"blockchain", &getblockheader}, {"blockchain", &getblockheaders}, - {"blockchain", &getmerkleblocks}, {"blockchain", &getchaintips}, + {"blockchain", &getdeploymentinfo}, {"blockchain", &getdifficulty}, + {"blockchain", &getmerkleblocks}, {"blockchain", &getspecialtxes}, {"blockchain", &gettxout}, {"blockchain", &gettxoutsetinfo}, diff --git a/src/test/fuzz/rpc.cpp b/src/test/fuzz/rpc.cpp index b95b93425e8e1..ab53f9ceaee3b 100644 --- a/src/test/fuzz/rpc.cpp +++ b/src/test/fuzz/rpc.cpp @@ -118,6 +118,7 @@ const std::vector RPC_COMMANDS_SAFE_FOR_FUZZING{ "getchaintips", "getchaintxstats", "getconnectioncount", + "getdeploymentinfo", "getdescriptorinfo", "getdifficulty", "getindexinfo", diff --git a/src/test/fuzz/versionbits.cpp b/src/test/fuzz/versionbits.cpp index a0a7369909b9d..5db0854e93e25 100644 --- a/src/test/fuzz/versionbits.cpp +++ b/src/test/fuzz/versionbits.cpp @@ -52,7 +52,7 @@ class TestConditionChecker : public AbstractThresholdConditionChecker ThresholdState GetStateFor(const CBlockIndex* pindexPrev) const { return AbstractThresholdConditionChecker::GetStateFor(pindexPrev, dummy_params, m_cache); } int GetStateSinceHeightFor(const CBlockIndex* pindexPrev) const { return AbstractThresholdConditionChecker::GetStateSinceHeightFor(pindexPrev, dummy_params, m_cache); } - BIP9Stats GetStateStatisticsFor(const CBlockIndex* pindexPrev) const { return AbstractThresholdConditionChecker::GetStateStatisticsFor(pindexPrev, dummy_params, m_cache); } + BIP9Stats GetStateStatisticsFor(const CBlockIndex* pindex, std::vector* signals=nullptr) const { return AbstractThresholdConditionChecker::GetStateStatisticsFor(pindex, dummy_params, m_cache, signals); } bool Condition(int32_t version) const { @@ -221,7 +221,14 @@ FUZZ_TARGET(versionbits, .init = initialize_versionbits) CBlockIndex* prev = blocks.tip(); const int exp_since = checker.GetStateSinceHeightFor(prev); const ThresholdState exp_state = checker.GetStateFor(prev); - BIP9Stats last_stats = checker.GetStateStatisticsFor(prev); + + // get statistics from end of previous period, then reset + BIP9Stats last_stats; + last_stats.period = period; + last_stats.threshold = threshold; + last_stats.count = last_stats.elapsed = 0; + last_stats.possible = (period >= threshold); + std::vector last_signals{}; int prev_next_height = (prev == nullptr ? 0 : prev->nHeight + 1); assert(exp_since <= prev_next_height); @@ -242,17 +249,25 @@ FUZZ_TARGET(versionbits, .init = initialize_versionbits) assert(state == exp_state); assert(since == exp_since); - // GetStateStatistics may crash when state is not STARTED - if (state != ThresholdState::STARTED) continue; - // check that after mining this block stats change as expected - const BIP9Stats stats = checker.GetStateStatisticsFor(current_block); + std::vector signals; + const BIP9Stats stats = checker.GetStateStatisticsFor(current_block, &signals); + const BIP9Stats stats_no_signals = checker.GetStateStatisticsFor(current_block); + assert(stats.period == stats_no_signals.period && stats.threshold == stats_no_signals.threshold + && stats.elapsed == stats_no_signals.elapsed && stats.count == stats_no_signals.count + && stats.possible == stats_no_signals.possible); + assert(stats.period == period); assert(stats.threshold == threshold); assert(stats.elapsed == b); assert(stats.count == last_stats.count + (signal ? 1 : 0)); assert(stats.possible == (stats.count + period >= stats.elapsed + threshold)); last_stats = stats; + + assert(signals.size() == last_signals.size() + 1); + assert(signals.back() == signal); + last_signals.push_back(signal); + assert(signals == last_signals); } if (exp_state == ThresholdState::STARTED) { @@ -266,14 +281,12 @@ FUZZ_TARGET(versionbits, .init = initialize_versionbits) CBlockIndex* current_block = blocks.mine_block(signal); assert(checker.Condition(current_block) == signal); - // GetStateStatistics is safe on a period boundary - // and has progressed to a new period const BIP9Stats stats = checker.GetStateStatisticsFor(current_block); assert(stats.period == period); assert(stats.threshold == threshold); - assert(stats.elapsed == 0); - assert(stats.count == 0); - assert(stats.possible == true); + assert(stats.elapsed == period); + assert(stats.count == blocks_sig); + assert(stats.possible == (stats.count + period >= stats.elapsed + threshold)); // More interesting is whether the state changed. const ThresholdState state = checker.GetStateFor(current_block); diff --git a/src/versionbits.cpp b/src/versionbits.cpp index f3386d4cfb946..ed402d64aeb7e 100644 --- a/src/versionbits.cpp +++ b/src/versionbits.cpp @@ -131,21 +131,25 @@ ThresholdState AbstractThresholdConditionChecker::GetStateFor(const CBlockIndex* return state; } -BIP9Stats AbstractThresholdConditionChecker::GetStateStatisticsFor(const CBlockIndex* pindex, const Consensus::Params& params, ThresholdConditionCache& cache) const +BIP9Stats AbstractThresholdConditionChecker::GetStateStatisticsFor(const CBlockIndex* pindex, const Consensus::Params& params, ThresholdConditionCache& cache, std::vector* signalling_blocks) const { BIP9Stats stats = {}; stats.period = Period(params); stats.threshold = Threshold(params, 0); - if (pindex == nullptr) - return stats; + if (pindex == nullptr) return stats; - // Find beginning of period - const CBlockIndex* pindexEndOfPrevPeriod = pindex->GetAncestor(pindex->nHeight - ((pindex->nHeight + 1) % stats.period)); - stats.elapsed = pindex->nHeight - pindexEndOfPrevPeriod->nHeight; + // Find how many blocks are in the current period + int blocks_in_period = 1 + (pindex->nHeight % stats.period); + + // Reset signalling_blocks + if (signalling_blocks) { + signalling_blocks->assign(blocks_in_period, false); + } // Re-calculate current threshold + const CBlockIndex* pindexEndOfPrevPeriod = pindex->GetAncestor(pindex->nHeight - ((pindex->nHeight + 1) % stats.period)); int nAttempt{0}; const ThresholdState state = GetStateFor(pindexEndOfPrevPeriod, params, cache); if (state == ThresholdState::STARTED) { @@ -155,14 +159,20 @@ BIP9Stats AbstractThresholdConditionChecker::GetStateStatisticsFor(const CBlockI stats.threshold = Threshold(params, nAttempt); // Count from current block to beginning of period + int elapsed = 0; int count = 0; const CBlockIndex* currentIndex = pindex; - while (pindexEndOfPrevPeriod->nHeight != currentIndex->nHeight){ - if (Condition(currentIndex, params)) - count++; + do { + ++elapsed; + --blocks_in_period; + if (Condition(currentIndex, params)) { + ++count; + if (signalling_blocks) signalling_blocks->at(blocks_in_period) = true; + } currentIndex = currentIndex->pprev; - } + } while (blocks_in_period > 0); + stats.elapsed = elapsed; stats.count = count; stats.possible = (stats.period - stats.threshold ) >= (stats.elapsed - count); @@ -263,10 +273,10 @@ ThresholdState VersionBitsCache::State(const CBlockIndex* pindexPrev, const Cons return VersionBitsConditionChecker(pos).GetStateFor(pindexPrev, params, m_caches[pos]); } -BIP9Stats VersionBitsCache::Statistics(const CBlockIndex* pindexPrev, const Consensus::Params& params, Consensus::DeploymentPos pos) +BIP9Stats VersionBitsCache::Statistics(const CBlockIndex* pindex, const Consensus::Params& params, Consensus::DeploymentPos pos, std::vector* signalling_blocks) { LOCK(m_mutex); - return VersionBitsConditionChecker(pos).GetStateStatisticsFor(pindexPrev, params, m_caches[pos]); + return VersionBitsConditionChecker(pos).GetStateStatisticsFor(pindex, params, m_caches[pos], signalling_blocks); } int VersionBitsCache::StateSinceHeight(const CBlockIndex* pindexPrev, const Consensus::Params& params, Consensus::DeploymentPos pos) diff --git a/src/versionbits.h b/src/versionbits.h index 036fba26b4233..46f5bb48d8cae 100644 --- a/src/versionbits.h +++ b/src/versionbits.h @@ -10,6 +10,7 @@ #include #include +#include /** What block version to use for new blocks (pre versionbits) */ static const int32_t VERSIONBITS_LAST_OLD_BLOCK_VERSION = 4; @@ -66,8 +67,10 @@ class AbstractThresholdConditionChecker { virtual int Threshold(const Consensus::Params& params, int nAttempt) const =0; public: - /** Returns the numerical statistics of an in-progress BIP9 softfork in the current period */ - BIP9Stats GetStateStatisticsFor(const CBlockIndex* pindex, const Consensus::Params& params, ThresholdConditionCache& cache) const; + /** Returns the numerical statistics of an in-progress BIP9 softfork in the period including pindex + * If provided, signalling_blocks is set to true/false based on whether each block in the period signalled + */ + BIP9Stats GetStateStatisticsFor(const CBlockIndex* pindex, const Consensus::Params& params, ThresholdConditionCache& cache, std::vector* signalling_blocks = nullptr) const; /** Returns the state for pindex A based on parent pindexPrev B. Applies any state transition if conditions are present. * Caches state from first block of period. */ ThresholdState GetStateFor(const CBlockIndex* pindexPrev, const Consensus::Params& params, ThresholdConditionCache& cache) const; @@ -84,8 +87,10 @@ class VersionBitsCache ThresholdConditionCache m_caches[Consensus::MAX_VERSION_BITS_DEPLOYMENTS] GUARDED_BY(m_mutex); public: - /** Get the numerical statistics for a given deployment for the signalling period that includes the block after pindexPrev. */ - BIP9Stats Statistics(const CBlockIndex* pindexPrev, const Consensus::Params& params, Consensus::DeploymentPos pos) EXCLUSIVE_LOCKS_REQUIRED(!m_mutex); + /** Get the numerical statistics for a given deployment for the signalling period that includes pindex. + * If provided, signalling_blocks is set to true/false based on whether each block in the period signalled + */ + BIP9Stats Statistics(const CBlockIndex* pindex, const Consensus::Params& params, Consensus::DeploymentPos pos, std::vector* signalling_blocks = nullptr) EXCLUSIVE_LOCKS_REQUIRED(!m_mutex); static uint32_t Mask(const Consensus::Params& params, Consensus::DeploymentPos pos); diff --git a/test/functional/feature_asset_locks.py b/test/functional/feature_asset_locks.py index d482d1061de4e..8f092dcb9a442 100755 --- a/test/functional/feature_asset_locks.py +++ b/test/functional/feature_asset_locks.py @@ -248,7 +248,7 @@ def run_test(self): self.set_sporks() - assert_equal(self.nodes[0].getblockchaininfo()['softforks']['v20']['active'], True) + assert_equal(self.nodes[0].getdeploymentinfo()['deployments']['v20']['active'], True) for _ in range(2): self.dynamically_add_masternode(evo=True) diff --git a/test/functional/feature_cltv.py b/test/functional/feature_cltv.py index 907811c47250e..0e7ba3f61d7d4 100755 --- a/test/functional/feature_cltv.py +++ b/test/functional/feature_cltv.py @@ -99,7 +99,7 @@ def set_test_params(self): self.rpc_timeout = 480 def test_cltv_info(self, *, is_active): - assert_equal(self.nodes[0].getblockchaininfo()['softforks']['bip65'], { + assert_equal(self.nodes[0].getdeploymentinfo()['deployments']['bip65'], { "active": is_active, "height": CLTV_HEIGHT, "type": "buried", diff --git a/test/functional/feature_dersig.py b/test/functional/feature_dersig.py index 604a471c4da0c..4b840cf85df81 100755 --- a/test/functional/feature_dersig.py +++ b/test/functional/feature_dersig.py @@ -64,7 +64,7 @@ def create_tx(self, input_txid): return self.miniwallet.create_self_transfer(utxo_to_spend=utxo_to_spend)['tx'] def test_dersig_info(self, *, is_active): - assert_equal(self.nodes[0].getblockchaininfo()['softforks']['bip66'], + assert_equal(self.nodes[0].getdeploymentinfo()['deployments']['bip66'], { "active": is_active, "height": DERSIG_HEIGHT, diff --git a/test/functional/feature_governance.py b/test/functional/feature_governance.py index 82025d06d6cdc..ef2f5e7462d41 100755 --- a/test/functional/feature_governance.py +++ b/test/functional/feature_governance.py @@ -24,7 +24,7 @@ def set_test_params(self): self.delay_v20_and_mn_rr(height=160) def check_superblockbudget(self, v20_active): - v20_state = self.nodes[0].getblockchaininfo()["softforks"]["v20"] + v20_state = self.nodes[0].getdeploymentinfo()["deployments"]["v20"] assert_equal(v20_state["active"], v20_active) assert_equal(self.nodes[0].getsuperblockbudget(120), self.expected_old_budget) assert_equal(self.nodes[0].getsuperblockbudget(140), self.expected_old_budget) @@ -95,14 +95,14 @@ def run_test(self): self.bump_mocktime(3) self.generate(self.nodes[0], 3, sync_fun=self.sync_blocks()) assert_equal(self.nodes[0].getblockcount(), 137) - assert_equal(self.nodes[0].getblockchaininfo()["softforks"]["v20"]["active"], False) + assert_equal(self.nodes[0].getdeploymentinfo()["deployments"]["v20"]["active"], False) self.check_superblockbudget(False) self.log.info("Check 2nd superblock before v20") self.bump_mocktime(3) self.generate(self.nodes[0], 3, sync_fun=self.sync_blocks()) assert_equal(self.nodes[0].getblockcount(), 140) - assert_equal(self.nodes[0].getblockchaininfo()["softforks"]["v20"]["active"], False) + assert_equal(self.nodes[0].getdeploymentinfo()["deployments"]["v20"]["active"], False) self.check_superblockbudget(False) self.log.info("Prepare proposals") @@ -221,7 +221,7 @@ def run_test(self): self.bump_mocktime(1) self.generate(self.nodes[0], 1, sync_fun=self.no_op) assert_equal(self.nodes[0].getblockcount(), 150) - assert_equal(self.nodes[0].getblockchaininfo()["softforks"]["v20"]["active"], False) + assert_equal(self.nodes[0].getdeploymentinfo()["deployments"]["v20"]["active"], False) self.check_superblockbudget(False) self.log.info("The 'winner' should submit new trigger and vote for it, but it's isolated so no triggers should be found") @@ -361,7 +361,7 @@ def sync_gov(node): self.bump_mocktime(1) self.generate(self.nodes[0], 1, sync_fun=self.sync_blocks()) assert_equal(self.nodes[0].getblockcount(), 180) - assert_equal(self.nodes[0].getblockchaininfo()["softforks"]["v20"]["active"], True) + assert_equal(self.nodes[0].getdeploymentinfo()["deployments"]["v20"]["active"], True) self.log.info("Mine and check a couple more superblocks") for i in range(2): @@ -375,7 +375,7 @@ def sync_gov(node): self.bump_mocktime(1) self.generate(self.nodes[0], 1, sync_fun=self.sync_blocks()) assert_equal(self.nodes[0].getblockcount(), sb_block_height) - assert_equal(self.nodes[0].getblockchaininfo()["softforks"]["v20"]["active"], True) + assert_equal(self.nodes[0].getdeploymentinfo()["deployments"]["v20"]["active"], True) self.check_superblockbudget(True) self.check_superblock() diff --git a/test/functional/feature_mnehf.py b/test/functional/feature_mnehf.py index 836821d86c2f2..93bb80cdeefa9 100755 --- a/test/functional/feature_mnehf.py +++ b/test/functional/feature_mnehf.py @@ -162,11 +162,13 @@ def run_test(self): self.check_fork('defined') self.generate(node, 1) + self.generate(node, 1) for _ in range(4 // 2): self.check_fork('started') self.generate(node, 2) + self.generate(node, 1) for i in range(4 // 2): self.check_fork('locked_in') @@ -174,6 +176,7 @@ def run_test(self): if i == 1: self.restart_all_nodes() + self.generate(node, 1) self.check_fork('active') fork_active_blockhash = node.getbestblockhash() @@ -194,7 +197,7 @@ def run_test(self): assert tx_sent_2 in node.getblock(ehf_blockhash_2)['tx'] self.log.info(f"Generate some more block to jump to `started` status") - self.generate(node, 4) + self.generate(node, 5) self.check_fork('started') self.restart_node(0) self.check_fork('started') diff --git a/test/functional/feature_new_quorum_type_activation.py b/test/functional/feature_new_quorum_type_activation.py index 678d3bab66aba..fb1eca98b4e2b 100755 --- a/test/functional/feature_new_quorum_type_activation.py +++ b/test/functional/feature_new_quorum_type_activation.py @@ -22,7 +22,7 @@ def set_test_params(self): def run_test(self): self.log.info(get_bip9_details(self.nodes[0], 'testdummy')) assert_equal(get_bip9_details(self.nodes[0], 'testdummy')['status'], 'defined') - self.generate(self.nodes[0], 9, sync_fun=self.no_op) + self.generate(self.nodes[0], 10, sync_fun=self.no_op) assert_equal(get_bip9_details(self.nodes[0], 'testdummy')['status'], 'started') ql = self.nodes[0].quorum("list") assert_equal(len(ql), 3) diff --git a/test/functional/interface_rest.py b/test/functional/interface_rest.py index 5f63718c4bba2..a2ff3c94709b3 100755 --- a/test/functional/interface_rest.py +++ b/test/functional/interface_rest.py @@ -388,6 +388,17 @@ def run_test(self): assert_equal(self.test_rest_request(f"/headers/{bb_hash}", query_params={"count": 1}), self.test_rest_request(f"/headers/1/{bb_hash}")) assert_equal(self.test_rest_request(f"/blockfilterheaders/basic/{bb_hash}", query_params={"count": 1}), self.test_rest_request(f"/blockfilterheaders/basic/5/{bb_hash}")) + self.log.info("Test the /deploymentinfo URI") + + deployment_info = self.nodes[0].getdeploymentinfo() + assert_equal(deployment_info, self.test_rest_request('/deploymentinfo')) + + non_existing_blockhash = '42759cde25462784395a337460bde75f58e73d3f08bd31fdc3507cbac856a2c4' + resp = self.test_rest_request(f'/deploymentinfo/{non_existing_blockhash}', ret_type=RetType.OBJ, status=400) + assert_equal(resp.read().decode('utf-8').rstrip(), "Block not found") + + resp = self.test_rest_request(f"/deploymentinfo/{INVALID_PARAM}", ret_type=RetType.OBJ, status=400) + assert_equal(resp.read().decode('utf-8').rstrip(), f"Invalid hash: {INVALID_PARAM}") if __name__ == '__main__': RESTTest().main() diff --git a/test/functional/rpc_blockchain.py b/test/functional/rpc_blockchain.py index 6ffaaa75f0f90..d9ca3b0164632 100755 --- a/test/functional/rpc_blockchain.py +++ b/test/functional/rpc_blockchain.py @@ -6,6 +6,7 @@ Test the following RPCs: - getblockchaininfo + - getdeploymentinfo - getchaintxstats - gettxoutsetinfo - getblockheader @@ -92,6 +93,7 @@ def run_test(self): self._test_stopatheight() self._test_waitforblockheight() self._test_getblock() + self._test_getdeploymentinfo() self._test_y2106() assert self.nodes[0].verifychain(4, 0) @@ -130,7 +132,6 @@ def _test_getblockchaininfo(self): 'mediantime', 'pruned', 'size_on_disk', - 'softforks', 'time', 'verificationprogress', 'warnings', @@ -175,19 +176,6 @@ def _test_getblockchaininfo(self): '-stopatheight=207', '-prune=550', '-txindex=0', - '-testactivationheight=bip34@2', - '-testactivationheight=dersig@3', - '-testactivationheight=cltv@4', - '-testactivationheight=csv@5', - '-testactivationheight=bip147@6', - '-testactivationheight=dip0001@10', - '-dip3params=411:511', - '-testactivationheight=dip0008@12', - '-testactivationheight=dip0024@13', - '-testactivationheight=brr@14', - '-testactivationheight=v19@15', - '-testactivationheight=v20@412', # no earlier than DIP0003 - '-testactivationheight=mn_rr@413', ]) res = self.nodes[0].getblockchaininfo() @@ -200,7 +188,14 @@ def _test_getblockchaininfo(self): assert res['automatic_pruning'] assert_equal(res['prune_target_size'], 576716800) assert_greater_than(res['size_on_disk'], 0) - assert_equal(res['softforks'], { + + def check_signalling_deploymentinfo_result(self, gdi_result, height, blockhash, status_next): + assert height >= 144 and height <= 287 + + assert_equal(gdi_result, { + "hash": blockhash, + "height": height, + "deployments": { 'bip34': {'type': 'buried', 'active': True, 'height': 2}, 'bip66': {'type': 'buried', 'active': True, 'height': 3}, 'bip65': {'type': 'buried', 'active': True, 'height': 4}, @@ -219,35 +214,74 @@ def _test_getblockchaininfo(self): 'v24': { 'type': 'bip9', 'bip9': { - 'status': 'defined', 'start_time': 0, 'timeout': 9223372036854775807, # "v24" does not have a timeout so is set to the max int64 value - 'since': 0, 'min_activation_height': 0, - 'ehf': True + 'since': 0, + 'status': 'defined', + 'status_next': 'defined', + 'ehf': True, }, - 'active': False}, + 'active': False + }, 'testdummy': { 'type': 'bip9', 'bip9': { - 'status': 'started', 'bit': 28, 'start_time': 0, 'timeout': 9223372036854775807, # testdummy does not have a timeout so is set to the max int64 value + 'min_activation_height': 0, 'since': 144, + 'status': 'started', + 'status_next': status_next, 'statistics': { 'period': 144, 'threshold': 108, - 'elapsed': HEIGHT - 143, - 'count': HEIGHT - 143, + 'elapsed': height - 143, + 'count': height - 143, 'possible': True, }, - 'min_activation_height': 0, 'ehf': False, + 'signalling': '#'*(height-143), }, - 'active': False}, + 'active': False + } + } }) + def _test_getdeploymentinfo(self): + # Note: continues past -stopatheight height, so must be invoked + # after _test_stopatheight + + self.log.info("Test getdeploymentinfo") + self.stop_node(0) + self.start_node(0, extra_args=[ + '-testactivationheight=bip34@2', + '-testactivationheight=dersig@3', + '-testactivationheight=cltv@4', + '-testactivationheight=csv@5', + '-testactivationheight=bip147@6', + '-testactivationheight=dip0001@10', + '-dip3params=411:511', + '-testactivationheight=dip0008@12', + '-testactivationheight=dip0024@13', + '-testactivationheight=brr@14', + '-testactivationheight=v19@15', + '-testactivationheight=v20@412', # no earlier than DIP0003 + '-testactivationheight=mn_rr@413', + ]) + + gbci207 = self.nodes[0].getblockchaininfo() + self.check_signalling_deploymentinfo_result(self.nodes[0].getdeploymentinfo(), gbci207["blocks"], gbci207["bestblockhash"], "started") + + # block just prior to lock in + self.generate(self.wallet, 287 - gbci207["blocks"]) + gbci287 = self.nodes[0].getblockchaininfo() + self.check_signalling_deploymentinfo_result(self.nodes[0].getdeploymentinfo(), gbci287["blocks"], gbci287["bestblockhash"], "locked_in") + + # calling with an explicit hash works + self.check_signalling_deploymentinfo_result(self.nodes[0].getdeploymentinfo(gbci207["bestblockhash"]), gbci207["blocks"], gbci207["bestblockhash"], "started") + def _test_y2106(self): self.log.info("Check that block timestamps work until year 2106") self.generate(self.nodes[0], 8)[-1] diff --git a/test/functional/test_framework/blocktools.py b/test/functional/test_framework/blocktools.py index d451ab17359a3..a2d2f05cb1dbc 100644 --- a/test/functional/test_framework/blocktools.py +++ b/test/functional/test_framework/blocktools.py @@ -120,7 +120,7 @@ def create_block_with_mnpayments(mninfo, node, vtx=None, mn_payee=None, mn_amoun mn_operator_amount = 0 if mn_amount is None: - v20_info = node.getblockchaininfo()['softforks']['v20'] + v20_info = node.getdeploymentinfo()['deployments']['v20'] mn_amount_total = get_masternode_payment(height, coinbasevalue, v20_info['active']) mn_operator_amount = mn_amount_total * operator_reward // 100 mn_amount = mn_amount_total - mn_operator_amount diff --git a/test/functional/test_framework/util.py b/test/functional/test_framework/util.py index 107bd72cc6c3b..e44dbdf56eaf6 100644 --- a/test/functional/test_framework/util.py +++ b/test/functional/test_framework/util.py @@ -505,12 +505,12 @@ def get_chain_conf_names(chain): def get_bip9_details(node, key): """Return extra info about bip9 softfork""" - return node.getblockchaininfo()['softforks'][key]['bip9'] + return node.getdeploymentinfo()['deployments'][key]['bip9'] def softfork_active(node, key): """Return whether a softfork is active.""" - return node.getblockchaininfo()['softforks'][key]['active'] + return node.getdeploymentinfo()['deployments'][key]['active'] def set_node_times(nodes, t): diff --git a/test/functional/wallet_signrawtransactionwithwallet.py b/test/functional/wallet_signrawtransactionwithwallet.py index 3127a93a85342..b3f528a1c817d 100755 --- a/test/functional/wallet_signrawtransactionwithwallet.py +++ b/test/functional/wallet_signrawtransactionwithwallet.py @@ -130,7 +130,7 @@ def test_signing_with_csv(self): getcontext().prec = 8 # Make sure CSV is active - self.generate(self.nodes[0], 500) + assert self.nodes[0].getdeploymentinfo()['deployments']['csv']['active'] # Create a P2SH script with CSV script = CScript([1, OP_CHECKSEQUENCEVERIFY, OP_DROP, OP_TRUE]) @@ -159,11 +159,11 @@ def test_signing_with_cltv(self): self.nodes[0].walletpassphrase("password", 9999) getcontext().prec = 8 - # Make sure CSV is active - self.generate(self.nodes[0], 1500) + # Make sure CLTV is active + assert self.nodes[0].getdeploymentinfo()['deployments']['bip65']['active'] # Create a P2SH script with CLTV - script = CScript([1000, OP_CHECKLOCKTIMEVERIFY, OP_DROP, OP_TRUE]) + script = CScript([100, OP_CHECKLOCKTIMEVERIFY, OP_DROP, OP_TRUE]) address = script_to_p2sh(script) # Fund that address and make the spend