Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions doc/REST-interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<BLOCKHASH>.json`

Returns an object containing various state info regarding deployments of
consensus changes at the current chain tip, or at <BLOCKHASH> if provided.
Only supports JSON as output format.
Refer to the `getdeploymentinfo` RPC help for details.

#### Query UTXO set
- `GET /rest/getutxos/<TXID>-<N>/<TXID>-<N>/.../<TXID>-<N>.<bin|hex|json>`
- `GET /rest/getutxos/checkmempool/<TXID>-<N>/<TXID>-<N>/.../<TXID>-<N>.<bin|hex|json>`
Expand Down
17 changes: 17 additions & 0 deletions doc/release-notes-6888.md
Original file line number Diff line number Diff line change
@@ -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.
44 changes: 44 additions & 0 deletions src/rest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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},
};

Expand Down
212 changes: 155 additions & 57 deletions src/rpc/blockchain.cpp

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/test/fuzz/rpc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ const std::vector<std::string> RPC_COMMANDS_SAFE_FOR_FUZZING{
"getchaintips",
"getchaintxstats",
"getconnectioncount",
"getdeploymentinfo",
"getdescriptorinfo",
"getdifficulty",
"getindexinfo",
Expand Down
35 changes: 24 additions & 11 deletions src/test/fuzz/versionbits.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool>* signals=nullptr) const { return AbstractThresholdConditionChecker::GetStateStatisticsFor(pindex, dummy_params, m_cache, signals); }

bool Condition(int32_t version) const
{
Expand Down Expand Up @@ -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<bool> last_signals{};

int prev_next_height = (prev == nullptr ? 0 : prev->nHeight + 1);
assert(exp_since <= prev_next_height);
Expand All @@ -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<bool> 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) {
Expand All @@ -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);
Expand Down
34 changes: 22 additions & 12 deletions src/versionbits.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool>* 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) {
Expand All @@ -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);

Expand Down Expand Up @@ -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<bool>* 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)
Expand Down
13 changes: 9 additions & 4 deletions src/versionbits.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <sync.h>

#include <map>
#include <vector>

/** What block version to use for new blocks (pre versionbits) */
static const int32_t VERSIONBITS_LAST_OLD_BLOCK_VERSION = 4;
Expand Down Expand Up @@ -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<bool>* 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;
Expand All @@ -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<bool>* signalling_blocks = nullptr) EXCLUSIVE_LOCKS_REQUIRED(!m_mutex);

static uint32_t Mask(const Consensus::Params& params, Consensus::DeploymentPos pos);

Expand Down
2 changes: 1 addition & 1 deletion test/functional/feature_asset_locks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion test/functional/feature_cltv.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion test/functional/feature_dersig.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
12 changes: 6 additions & 6 deletions test/functional/feature_governance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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):
Expand All @@ -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()

Expand Down
5 changes: 4 additions & 1 deletion test/functional/feature_mnehf.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,18 +162,21 @@ 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')
self.generate(node, 2)
if i == 1:
self.restart_all_nodes()

self.generate(node, 1)
self.check_fork('active')

fork_active_blockhash = node.getbestblockhash()
Expand All @@ -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')
Expand Down
2 changes: 1 addition & 1 deletion test/functional/feature_new_quorum_type_activation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading
Loading