rpc: getdeploymentinfo: change stats to always refer to current period

On a period boundary, getdeploymentinfo (and previously getblockchaininfo)
would report the status and statistics for the next block rather than
the current block. Change this to always report the status/statistics
of the current block, but add status-next to report the status for the
next block.
This commit is contained in:
Anthony Towns 2021-11-14 07:55:34 +10:00
parent 7f15c1841b
commit a7469bcd35
5 changed files with 98 additions and 59 deletions

View File

@ -1412,45 +1412,60 @@ static void SoftForkDescPushBack(const CBlockIndex* active_chain_tip, UniValue&
// For BIP9 deployments.
if (!DeploymentEnabled(consensusParams, id)) return;
if (active_chain_tip == 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(active_chain_tip, consensusParams, id);
const ThresholdState current_state = g_versionbitscache.State(active_chain_tip->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);
}
bip9.pushKV("start_time", consensusParams.vDeployments[id].nStartTime);
bip9.pushKV("timeout", consensusParams.vDeployments[id].nTimeout);
int64_t since_height = g_versionbitscache.StateSinceHeight(active_chain_tip, consensusParams, id);
bip9.pushKV("since", since_height);
bip9.pushKV("min_activation_height", consensusParams.vDeployments[id].min_activation_height);
// BIP9 status
bip9.pushKV("status", get_state_name(current_state));
bip9.pushKV("since", g_versionbitscache.StateSinceHeight(active_chain_tip->pprev, consensusParams, id));
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);
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);
}
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(active_chain_tip, consensusParams, id));
}
rv.pushKV("active", ThresholdState::ACTIVE == thresholdState);
rv.pushKV("active", ThresholdState::ACTIVE == next_state);
rv.pushKV("bip9", bip9);
softforks.pushKV(DeploymentName(id), rv);
}
@ -1551,14 +1566,17 @@ RPCHelpMan getblockchaininfo()
namespace {
const std::vector<RPCResult> 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::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::NUM, "since", "height of the first block to which the status applies"},
{RPCResult::Type::NUM, "min_activation_height", "minimum height of blocks for which the rules may be enforced"},
{RPCResult::Type::STR, "status", "bip9 status of 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", "bip9 status of 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"},
@ -1568,8 +1586,6 @@ const std::vector<RPCResult> RPCHelpForDeployment{
{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"},
};
UniValue DeploymentInfo(const CBlockIndex* tip, const Consensus::Params& consensusParams)

View File

@ -51,7 +51,7 @@ public:
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); }
BIP9Stats GetStateStatisticsFor(const CBlockIndex* pindex) const { return AbstractThresholdConditionChecker::GetStateStatisticsFor(pindex, dummy_params); }
bool Condition(int32_t version) const
{
@ -220,7 +220,13 @@ FUZZ_TARGET_INIT(versionbits, initialize)
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);
int prev_next_height = (prev == nullptr ? 0 : prev->nHeight + 1);
assert(exp_since <= prev_next_height);
@ -241,9 +247,6 @@ FUZZ_TARGET_INIT(versionbits, initialize)
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);
assert(stats.period == period);
@ -265,14 +268,12 @@ FUZZ_TARGET_INIT(versionbits, initialize)
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);

View File

@ -105,22 +105,23 @@ BIP9Stats AbstractThresholdConditionChecker::GetStateStatisticsFor(const CBlockI
stats.period = Period(params);
stats.threshold = Threshold(params);
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;
int start_height = pindex->nHeight - (pindex->nHeight % stats.period);
// 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++;
for(;;) {
++elapsed;
if (Condition(currentIndex, params)) ++count;
if (currentIndex->nHeight <= start_height) break;
currentIndex = currentIndex->pprev;
}
stats.elapsed = elapsed;
stats.count = count;
stats.possible = (stats.period - stats.threshold ) >= (stats.elapsed - count);
@ -196,9 +197,9 @@ 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)
{
return VersionBitsConditionChecker(pos).GetStateStatisticsFor(pindexPrev, params);
return VersionBitsConditionChecker(pos).GetStateStatisticsFor(pindex, params);
}
int VersionBitsCache::StateSinceHeight(const CBlockIndex* pindexPrev, const Consensus::Params& params, Consensus::DeploymentPos pos)

View File

@ -64,7 +64,7 @@ protected:
virtual int Threshold(const Consensus::Params& params) const =0;
public:
/** Returns the numerical statistics of an in-progress BIP9 softfork in the current period */
/** Returns the numerical statistics of an in-progress BIP9 softfork in the period including pindex */
BIP9Stats GetStateStatisticsFor(const CBlockIndex* pindex, const Consensus::Params& params) 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. */
@ -82,8 +82,8 @@ private:
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. */
static BIP9Stats Statistics(const CBlockIndex* pindexPrev, const Consensus::Params& params, Consensus::DeploymentPos pos);
/** Get the numerical statistics for a given deployment for the signalling period that includes pindex. */
static BIP9Stats Statistics(const CBlockIndex* pindex, const Consensus::Params& params, Consensus::DeploymentPos pos);
static uint32_t Mask(const Consensus::Params& params, Consensus::DeploymentPos pos);

View File

@ -72,7 +72,6 @@ class BlockchainTest(BitcoinTestFramework):
self.restart_node(0, extra_args=['-stopatheight=207', '-prune=1']) # Set extra args with pruning after rescan is complete
self._test_getblockchaininfo()
self._test_getdeploymentinfo()
self._test_getchaintxstats()
self._test_gettxoutsetinfo()
self._test_getblockheader()
@ -81,6 +80,7 @@ class BlockchainTest(BitcoinTestFramework):
self._test_stopatheight()
self._test_waitforblockheight()
self._test_getblock()
self._test_getdeploymentinfo()
assert self.nodes[0].verifychain(4, 0)
def mine_chain(self):
@ -160,11 +160,6 @@ class BlockchainTest(BitcoinTestFramework):
self.start_node(0, extra_args=[
'-stopatheight=207',
'-prune=550',
'-testactivationheight=bip34@2',
'-testactivationheight=dersig@3',
'-testactivationheight=cltv@4',
'-testactivationheight=csv@5',
'-testactivationheight=segwit@6',
])
res = self.nodes[0].getblockchaininfo()
@ -178,11 +173,10 @@ class BlockchainTest(BitcoinTestFramework):
assert_equal(res['prune_target_size'], 576716800)
assert_greater_than(res['size_on_disk'], 0)
def _test_getdeploymentinfo(self):
self.log.info("Test getdeploymentinfo")
def check_signalling_deploymentinfo_result(self, gdi_result, height, blockhash, status_next):
assert height >= 144 and height <= 287
res = self.nodes[0].getdeploymentinfo()
assert_equal(res, {
assert_equal(gdi_result, {
"deployments": {
'bip34': {'type': 'buried', 'active': True, 'height': 2},
'bip66': {'type': 'buried', 'active': True, 'height': 3},
@ -192,30 +186,32 @@ class BlockchainTest(BitcoinTestFramework):
'testdummy': {
'type': 'bip9',
'bip9': {
'status': 'started',
'bit': 28,
'start_time': 0,
'timeout': 0x7fffffffffffffff, # testdummy does not have a timeout so is set to the max int64 value
'min_activation_height': 0,
'status': 'started',
'status-next': status_next,
'since': 144,
'statistics': {
'period': 144,
'threshold': 108,
'elapsed': HEIGHT - 143,
'count': HEIGHT - 143,
'elapsed': height - 143,
'count': height - 143,
'possible': True,
},
'min_activation_height': 0,
},
'active': False
},
'taproot': {
'type': 'bip9',
'bip9': {
'status': 'active',
'start_time': -1,
'timeout': 9223372036854775807,
'since': 0,
'min_activation_height': 0,
'status': 'active',
'status-next': 'active',
'since': 0,
},
'height': 0,
'active': True
@ -223,6 +219,31 @@ class BlockchainTest(BitcoinTestFramework):
}
})
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=segwit@6',
])
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_getchaintxstats(self):
self.log.info("Test getchaintxstats")