diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index ca677c69a3bc0b..81615b8151944d 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -1245,6 +1245,24 @@ static void SoftForkDescPushBack(const CBlockIndex* blockindex, UniValue& softfo case ThresholdState::LOCKED_IN: case ThresholdState::ACTIVE: bip9.pushKV("signal_abandon", strprintf("%08x", chainman.GetConsensus().vDeployments[id].signal_abandon)); + + // Report observed signalling + { + UniValue signals(UniValue::VARR); + for (const auto& signal_info : chainman.m_versionbitscache.GetSignalInfo(blockindex, chainman.GetConsensus(), id)) { + UniValue s(UniValue::VOBJ); + if (signal_info.revision == -1) { + // don't report self-activation signals if already active + if (signal_info.activate && current_state == ThresholdState::ACTIVE) continue; + } else { + s.pushKV("revision", signal_info.revision); + } + s.pushKV("height", signal_info.height); + s.pushKV("action", (signal_info.activate ? "activate" : "abandon")); + signals.push_back(s); + } + bip9.pushKV("signals", signals); + } break; case ThresholdState::DEACTIVATING: case ThresholdState::ABANDONED: @@ -1355,6 +1373,13 @@ const std::vector RPCHelpForDeployment{ {RPCResult::Type::STR, "status_next", "status of deployment at the next block"}, {RPCResult::Type::STR_HEX, "signal_activate", /*optional=*/true, "version number to trigger deployment activation"}, {RPCResult::Type::STR_HEX, "signal_abandon", /*optional=*/true, "version number to trigger deployment abandonment"}, + {RPCResult::Type::ARR, "signals", /*optional=*/true, "indicates blocks that signalled in the last period", + {{RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::NUM, "revision", /*optional=*/true, "revision being signalled"}, + {RPCResult::Type::NUM, "height", "height of the signalling block"}, + {RPCResult::Type::STR, "action", "action signalled (activate or abandon)"}, + }}}}, }}, }; diff --git a/src/test/fuzz/versionbits.cpp b/src/test/fuzz/versionbits.cpp index fdcbe568ca4a5d..cb925b875ace68 100644 --- a/src/test/fuzz/versionbits.cpp +++ b/src/test/fuzz/versionbits.cpp @@ -13,11 +13,17 @@ #include #include +#include #include #include #include #include +bool operator==(const SignalInfo& a, const SignalInfo& b) +{ + return a.height == b.height && a.revision == b.revision && a.activate == b.activate; +} + namespace { class TestConditionChecker : public AbstractThresholdConditionChecker { @@ -165,6 +171,18 @@ FUZZ_TARGET(versionbits, .init = initialize) // Now that we have chosen time and versions, setup to mine blocks Blocks blocks(block_start_time, interval); + const auto siginfo_nosignal = [&]() -> std::optional { + int year, number, revision; + if (checker.BINANA(year, number, revision)) { + if ((ver_nosignal & 0xFFFFFF00l) == (ver_activate & 0xFFFFFF00l)) { + return SignalInfo{.height = 0, .revision = static_cast(ver_nosignal & 0xFF), .activate = true}; + } else if ((ver_nosignal & 0xFFFFFF00l) == (ver_abandon & 0xFFFFFF00l)) { + return SignalInfo{.height = 0, .revision = static_cast(ver_nosignal & 0xFF), .activate = false}; + } + } + return std::nullopt; + }(); + /* Strategy: * * we mine n*period blocks, with zero/one of * those blocks signalling activation, and zero/one of @@ -207,8 +225,13 @@ FUZZ_TARGET(versionbits, .init = initialize) bool sig_abandon = false; int next_active = 0; int next_abandon = 0; + + std::vector exp_siginfo = checker.GetSignalInfo(nullptr); // dummy + assert(exp_siginfo.empty()); + for (int b = 1; b <= period; ++b) { CBlockIndex* current_block = blocks.tip(); + const int next_height = (current_block != nullptr ? current_block->nHeight + 1 : 0); if (current_block != nullptr) { // state and since don't change within the period @@ -221,14 +244,24 @@ FUZZ_TARGET(versionbits, .init = initialize) while (b > next_abandon) next_abandon += 1 + fuzzed_data_provider.ConsumeIntegral(); while (b > next_active) next_active += 1 + fuzzed_data_provider.ConsumeIntegral(); if (b == next_abandon) { + exp_siginfo.push_back({.height = next_height, .revision = -1, .activate = false}); blocks.mine_block(ver_abandon); sig_abandon = true; } else if (b == next_active) { + exp_siginfo.push_back({.height = next_height, .revision = -1, .activate = true}); blocks.mine_block(ver_activate); sig_active = true; } else { + if (siginfo_nosignal) { + exp_siginfo.push_back(*siginfo_nosignal); + exp_siginfo.back().height = next_height; + } blocks.mine_block(ver_nosignal); } + + const std::vector siginfo = checker.GetSignalInfo(blocks.tip()); + assert(siginfo.size() == exp_siginfo.size()); + assert(std::equal(siginfo.begin(), siginfo.end(), exp_siginfo.rbegin(), exp_siginfo.rend())); } // grab the final block and the state it's left us in diff --git a/src/versionbits.cpp b/src/versionbits.cpp index b0708f290419b6..6f28a3039c9bc1 100644 --- a/src/versionbits.cpp +++ b/src/versionbits.cpp @@ -177,6 +177,42 @@ int AbstractThresholdConditionChecker::GetStateSinceHeightFor(const CBlockIndex* return pindexPrev->nHeight + 1; } +std::vector AbstractThresholdConditionChecker::GetSignalInfo(const CBlockIndex* pindex) const +{ + std::vector result; + + int year = 0, number = 0, revision = 0; + const bool check_other_versions = BINANA(year, number, revision); + + const int32_t activate = ActivateVersion(); + const int32_t abandon = AbandonVersion(); + const int period = Period(); + + while (pindex != nullptr) { + if (pindex->nVersion == activate) { + result.push_back({ .height = pindex->nHeight, .revision = -1, .activate = true }); + } else if (pindex->nVersion == abandon) { + result.push_back({ .height = pindex->nHeight, .revision = -1, .activate = false }); + } else if (check_other_versions) { + if ((pindex->nVersion & 0x07FFFF00l) == (activate & 0x07FFFF00l)) { + SignalInfo s; + s.height = pindex->nHeight; + s.revision = static_cast(pindex->nVersion & 0xFF); + if ((pindex->nVersion & 0xF8000000l) == VERSIONBITS_TOP_ACTIVE) { + s.activate = true; + result.push_back(s); + } else if ((pindex->nVersion & 0xF8000000l) == VERSIONBITS_TOP_ABANDON) { + s.activate = false; + result.push_back(s); + } + } + } + if (pindex->nHeight % period == 0) break; + pindex = pindex->pprev; + } + return result; +} + namespace { /** @@ -212,6 +248,11 @@ int VersionBitsCache::StateSinceHeight(const CBlockIndex* pindexPrev, const Cons return VersionBitsConditionChecker(params, pos).GetStateSinceHeightFor(pindexPrev, m_caches[pos]); } +std::vector VersionBitsCache::GetSignalInfo(const CBlockIndex* pindex, const Consensus::Params& params, Consensus::DeploymentPos pos) const +{ + return VersionBitsConditionChecker(params, pos).GetSignalInfo(pindex); +} + int32_t VersionBitsCache::ComputeBlockVersion(const CBlockIndex* pindexPrev, const Consensus::Params& params) { int32_t nVersion = VERSIONBITS_TOP_BITS; diff --git a/src/versionbits.h b/src/versionbits.h index 4465ebb7e860b0..de887038d61909 100644 --- a/src/versionbits.h +++ b/src/versionbits.h @@ -36,6 +36,13 @@ enum class ThresholdState { // will either be nullptr or a block with (height + 1) % Period() == 0. typedef std::map ThresholdConditionCache; +struct SignalInfo +{ + int height; + int revision; // -1 = this revision + bool activate; // true = activate, false = abandon +}; + /** * Abstract class that implements BIP9-style threshold logic, and caches results. */ @@ -56,6 +63,9 @@ class AbstractThresholdConditionChecker { /** Report BINANA id, based on nVersion signalling standard */ bool BINANA(int& year, int& number, int& revision) const; + + /** Returns signalling information */ + std::vector GetSignalInfo(const CBlockIndex* pindex) const; }; /** BIP 9 allows multiple softforks to be deployed in parallel. We cache @@ -80,6 +90,9 @@ class VersionBitsCache /** Report BINANA details, based on nVersion signalling standard */ bool BINANA(int& year, int& number, int& revision, const Consensus::Params& params, Consensus::DeploymentPos pos) const; + /** Returns signalling information */ + std::vector GetSignalInfo(const CBlockIndex* pindex, const Consensus::Params& params, Consensus::DeploymentPos pos) const; + void Clear() EXCLUSIVE_LOCKS_REQUIRED(!m_mutex); }; diff --git a/test/functional/rpc_blockchain.py b/test/functional/rpc_blockchain.py index b8066721c04bc1..07527e79909f2f 100755 --- a/test/functional/rpc_blockchain.py +++ b/test/functional/rpc_blockchain.py @@ -216,6 +216,7 @@ def check_signalling_deploymentinfo_result(self, gdi_result, height, blockhash): 'since': 144, 'signal_activate': "30000000", 'signal_abandon': "50000000", + 'signals': [], }, 'active': False },