Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

Already on GitHub? Sign in to your account

RPC: getblockchaininfo returns BIP signaling statistics #9571

Merged
merged 1 commit into from May 23, 2017

Conversation

Contributor

pinheadmz commented Jan 18, 2017

Adds a new array to getblockchaininfo that returns the number of blocks in the current period signaling a BIP9 condition.

Example:

"segwit": {
      "status": "started",
      "bit": 1,
      "startTime": 0,
      "timeout": 999999999999,
      "since": 144,
      "statistics": {
        "period length": 144,
        "threshold": 108,
        "elapsed": 143,
        "count": 143,
        "possible": true
      }
}

and the explanation:

"statistics": {        (array) numeric statistics about BIP9 signalling for a softfork
   "period length": xx, (numeric) the length in blocks of the BIP9 signalling period 
   "threshold": xx,    (numeric) the number of blocks with the version bit set required to activate the feature 
   "elapsed": xx,      (numeric) the number of blocks elapsed since the beginning of the current period 
   "count": xx         (numeric) the number of blocks with the version bit set in the current period 
   "possible": xx      (boolean) returns false if there are not enough blocks left in this period to pass activation threshold 
}

[related question]

Member

gmaxwell commented Jan 18, 2017

NAK. The BIP9 quorum sensing is not a vote.

Contributor

pinheadmz commented Jan 18, 2017

Signaling statistics for soft forks pre-BIP9 used to be reported by getblockchaininfo.

I think it's helpful to see the adoption rate to better predict when a soft fork will engage. Currently there are several 3rd party websites that report these statistics, I would rather get them from my own node.

Owner

sipa commented Jan 18, 2017

Contributor

pinheadmz commented Jan 18, 2017

@sipa no V-word in the RPC help. I rephrased my commit description.

Member

gmaxwell commented Jan 18, 2017

yea, okay. Anyone interested in writing a test for one off on the possible boundary.

Contributor

pinheadmz commented Jan 18, 2017

@gmaxwell Added tests, look ok?

src/validation.h
@@ -32,6 +32,8 @@
#include <boost/unordered_map.hpp>
#include <boost/filesystem/path.hpp>
+#include <univalue.h>
@TheBlueMatt

TheBlueMatt Jan 18, 2017

Contributor

Preferably dont add a UniValue dependancy here...maybe just define a struct which carries the info you want?

@sipa

sipa Jan 19, 2017

Owner

Agree.

Contributor

TheBlueMatt commented Jan 18, 2017

Looks cool, I've previously written python crap to calculate similar stats from rpc data.

.gitignore
@@ -8,6 +8,7 @@ src/bitcoin-tx
src/test/test_bitcoin
src/test/test_bitcoin_fuzzy
src/qt/test/test_bitcoin-qt
+src/datadir
@@ -187,7 +187,7 @@ void RPCNotifyBlockChange(bool ibd, const CBlockIndex * pindex)
latestblock.hash = pindex->GetBlockHash();
latestblock.height = pindex->nHeight;
}
- cond_blockchange.notify_all();
+ cond_blockchange.notify_all();
@instagibbs

instagibbs Jan 18, 2017

Member

try to avoid changing whitespace randomly

@TheBlueMatt

TheBlueMatt Jan 18, 2017

Contributor

That wasnt random, that was replacing an errant tab, I say leave it.

src/versionbits.cpp
+
+ // Walk back from current block to beginning of period
+ std::vector<const CBlockIndex*> vToCompute;
+ while (pindexPrev->nHeight != currentIndex->nHeight){
@robmcl4

robmcl4 Jan 18, 2017

Contributor

What if pindexPrev == currentIndex == NULL? Also, why not keep a rolling count rather than using an accessory vector?

Contributor

pinheadmz commented Jan 18, 2017

@TheBlueMatt UniValue replaced with struct (although not sure I returned the values the most elegant way)

@robmcl4 Thanks, took out vector and just counted. If pindexPrev == currentIndex == NULL we're on the genesis block and count just stays at 0 anyway.

@instagibbs Removed my extra ignore line but left tab fix

src/validation.h
@@ -32,6 +32,8 @@
#include <boost/unordered_map.hpp>
#include <boost/filesystem/path.hpp>
+#include <univalue.h>
@sipa

sipa Jan 19, 2017

Owner

Agree.

src/versionbits.cpp
#include "consensus/params.h"
+#include <boost/foreach.hpp>
@sipa

sipa Jan 19, 2017

Owner

Please don't add new boost foreach uses, use c++11 range-based for instead.

@pinheadmz

pinheadmz Jan 19, 2017

Contributor

already gone :)

src/versionbits.cpp
@@ -105,6 +106,47 @@ ThresholdState AbstractThresholdConditionChecker::GetStateFor(const CBlockIndex*
return state;
}
+// return the numerical statistics of blocks signalling the specified BIP9 condition in this current period
+UniValue AbstractThresholdConditionChecker::GetStateStatisticsFor(const CBlockIndex* pindexPrev, const Consensus::Params& params, ThresholdConditionCache& cache) const
@sipa

sipa Jan 19, 2017

Owner

Does this code need to be in the condition checker implementation? It seems like it could be written generically, as it doesn't need access to the cache or internal state machine.

@pinheadmz

pinheadmz Jan 19, 2017

Contributor

Should I take it out? VersionBitsConditionChecker already has Condition() function for the bitmask for the corresponding soft fork.

Contributor

pinheadmz commented Jan 20, 2017

squashed

Concept ACK, and functional ACK. I've tested this and I'm fairly confident it's calculating the values correctly. Thanks for adding the additional tests to bip9-softforks.py

I have a few code style and variable naming nits.

src/rpc/blockchain.cpp
@@ -1102,7 +1114,14 @@ UniValue getblockchaininfo(const JSONRPCRequest& request)
" \"bit\": xx, (numeric) the bit (0-28) in the block version field used to signal this softfork (only for \"started\" status)\n"
" \"startTime\": xx, (numeric) the minimum median time past of a block at which the bit gains its meaning\n"
" \"timeout\": xx, (numeric) the median time past of a block at which the deployment is considered failed if not yet locked in\n"
- " \"since\": xx (numeric) height of the first block to which the status applies\n"
+ " \"since\": xx, (numeric) height of the first block to which the status applies\n"
+ " \"statistics\": { (object) numeric statistics about BIP9 signalling for a softfork\n"
@jnewbery

jnewbery Jan 23, 2017

Member

nit: perhaps add (only for \"started\" status)\n (like for the bit field above)

src/rpc/blockchain.cpp
+ " \"period length\": xx, (numeric) the length in blocks of the BIP9 signalling period \n"
+ " \"threshold\": xx, (numeric) the number of blocks with the version bit set required to activate the feature \n"
+ " \"elapsed\": xx, (numeric) the number of blocks elapsed since the beginning of the current period \n"
+ " \"count\": xx (numeric) the number of blocks with the version bit set in the current period \n"
@jnewbery

jnewbery Jan 23, 2017

Member

nanonit: comma after \"count\": xx please

src/versionbits.cpp
+// return the numerical statistics of blocks signalling the specified BIP9 condition in this current period
+BIP9Stats AbstractThresholdConditionChecker::GetStateStatisticsFor(const CBlockIndex* pindexPrev, const Consensus::Params& params) const
+{
+ BIP9Stats obj;
@jnewbery

jnewbery Jan 23, 2017

Member

nit: I suspect the name 'obj' is from when you were passing the object back as a univalue. I'd prefer for this variable to be named a little more descriptively, perhaps 'stats' or 'bip9Stats'.

src/versionbits.cpp
+
+ int elapsedBlocks;
+ int nPeriod = Period(params);
+ int nThreshold = Threshold(params);
@jnewbery

jnewbery Jan 23, 2017

Member

You're declaring local variables, setting them once, and then setting the obj members to those values. Would it be cleaner to just set the obj members directly?

@pinheadmz

pinheadmz Jan 23, 2017

Contributor

nPeriod and nThreshold are used again in line 137 (now line 135) to calculate the possible value. But you are right about elapsedBlocks and the actual bool possible, so I took those declarations out and simplified the assignments.

@jnewbery

jnewbery Jan 23, 2017

Member

yep, sounds fine to me.

@pinheadmz

pinheadmz Jan 23, 2017

Contributor

Actually just took out declarations for all those variables, assigned to return object directly and refer to those assignments downstream. (see 6eecfe0)

src/versionbits.cpp
+ obj.threshold = nThreshold;
+
+ // Find beginning of period
+ if (pindexPrev != NULL) {
@jnewbery

jnewbery Jan 23, 2017

Member

You're checking that pindexPrev isn't NULL, but then dereferencing further down (in line 130) even if this check fails.

Perhaps move this check to the top and return from the function if pindexPrev is NULL?

@pinheadmz

pinheadmz Jan 23, 2017

Contributor

Moved the check up so the return struct is partially initialized. However, this function shouldn't ever be called unless THRESHOLD_STARTED == thresholdState anyway, which will never be the case at block 0.

@jnewbery

jnewbery Jan 23, 2017

Member

I agree that this should always be non-NULL, but it's better to be conservative, and it's certainly better to be consistently conservative than conservative here and not conservative later :)

src/versionbits.cpp
+
+ // Find beginning of period
+ if (pindexPrev != NULL) {
+ pindexPrev = pindexPrev->GetAncestor(pindexPrev->nHeight - ((pindexPrev->nHeight + 1) % nPeriod));
@jnewbery

jnewbery Jan 23, 2017

Member

I don't much like this reassignment of pindexPrev to the first block in the current difficulty window. I know that the same thing happens in AbstractThresholdConditionChecker() above, but I think it'd be clearer to have a local variable called something like pindexFirstInWindow. Reasoning about whether a function is doing what you want is easier if the variables are named what they're actually used for.

@pinheadmz

pinheadmz Jan 23, 2017

Contributor

Yes I was mostly copying the format of AbstractThresholdConditionChecker::GetStateFor() but I agree with you, changed variable name.

src/versionbits.cpp
+ if (pindexPrev != NULL) {
+ pindexPrev = pindexPrev->GetAncestor(pindexPrev->nHeight - ((pindexPrev->nHeight + 1) % nPeriod));
+ elapsedBlocks = currentIndex->nHeight - pindexPrev->nHeight;
+ obj.elapsed = elapsedBlocks;
@jnewbery

jnewbery Jan 23, 2017

Member

Is it tidier to calculate elapsed directly from (pindexPrev % nPeriod) + 1?

@pinheadmz

pinheadmz Jan 23, 2017

Contributor

Not sure about this. After making this one line, it seems the same to me. Still need to get nHeight from each. Simpler to me to see "Current - Start = Elapsed"

src/versionbits.cpp
+
+ obj.count = count;
+ bool possible = (nPeriod - nThreshold) > (elapsedBlocks - count);
+ obj.possible = possible;
@jnewbery

jnewbery Jan 23, 2017

Member

combine these lines?

src/versionbits.h
@@ -53,6 +61,7 @@ class AbstractThresholdConditionChecker {
public:
// Note that the functions below take a pindexPrev as input: they compute information for block B based on its parent.
@jnewbery

jnewbery Jan 23, 2017

Member

This comment is very misleading, and so is naming the variable pindexPrev.

When GetStateFor() was written, the meaning of the function was: "I have block B in hand, and I'm trying to connect it to block A in the tree. What are the BIP9 softfork consensus rules that I should enforce on block B based on block A?" In that context, pindexPrev makes sense - we're judging block B's consensus rules based on its pindexPrev pointer to block A.

GetStateStatisticsFor() and GetStateSinceHeightFor() (introduced in #7948) are being used for a different purpose. The meaning of these functions is "Block A is our view of the current tip. What is our view of current network consensus rules?" In that context, pindexPrev doesn't make sense at all. There's no block B with a pindexPointer back to block A.

I'd prefer for the variable pindexPrev to be renamed in GetStateStatisticsFor() and GetStateSinceHeightFor(). Perhaps change it to pindex and update the comment to describe what the functions are actually being used for.

@pinheadmz

pinheadmz Jan 23, 2017

Contributor

Yeah ok this makes sense to me. I'll take my function out of the "functions below" list and rename variable to pindex. Should correcting GetStateSinceHeightFor() happen in a different commit though?

@jnewbery

jnewbery Jan 23, 2017

Member

yeah, I'd suggest changing GetStateSinceHeightFor() in a different commit. I can open a PR for that or leave it to you if you prefer.

src/rpc/blockchain.cpp
+ {
+ UniValue statsUV(UniValue::VOBJ);
+ BIP9Stats statsStruct = VersionBitsTipStatistics(consensusParams, id);
+ statsUV.push_back(Pair("period length", statsStruct.period));
@luke-jr

luke-jr Feb 2, 2017

Member

Do we really want spaces in keys?

@pinheadmz

pinheadmz Feb 2, 2017

Contributor

Whoops, thanks. Fixed and squashed

Owner

sipa commented Feb 10, 2017

utACK 83538dd

@laanwj laanwj added this to the 0.15.0 milestone Feb 10, 2017

src/rpc/blockchain.cpp
+ statsUV.push_back(Pair("elapsed", statsStruct.elapsed));
+ statsUV.push_back(Pair("count", statsStruct.count));
+ statsUV.push_back(Pair("possible", statsStruct.possible));
+
@TheBlueMatt

TheBlueMatt Feb 10, 2017

Contributor

Please be careful to not introduce lines with whitespace at the end. Looks like this patch has it in 5 places (here and 4 lines in AbstractThreasholdConditionCHecker::GetStateStatisticsFor).

@pinheadmz

pinheadmz Feb 10, 2017

Contributor

thanks

src/versionbits.cpp
+ return stats;
+
+ // Find beginning of period
+ const CBlockIndex* pindexFirstInPeriod = pindex->GetAncestor(pindex->nHeight - ((pindex->nHeight + 1) % stats.period));
@TheBlueMatt

TheBlueMatt Feb 10, 2017

Contributor

Can we get a more descriptive name? This is actually the last block in the previous period, not the first in this period (though it looks like all the later math is correct).

@pinheadmz

pinheadmz Feb 10, 2017

Contributor

Ah I think you're right. I copied this line from AbstractThresholdConditionChecker::GetStateFor

https://github.com/pinheadmz/bitcoin/blob/83538dd86dc426dc252714d4ca1ae91ad2c5ac24/src/versionbits.cpp#L30-L33

We'll call it pindexEndOfPrevPeriod ?

src/versionbits.cpp
+ }
+
+ stats.count = count;
+ stats.possible = (stats.period - stats.threshold ) > (stats.elapsed - count);
@TheBlueMatt

TheBlueMatt Feb 10, 2017

Contributor

I believe this should be >=?

@pinheadmz

pinheadmz Feb 10, 2017

Contributor

good catch thanks

@pinheadmz

pinheadmz Feb 10, 2017

Contributor

I'll update my test to hit the edge too

src/validation.h
@@ -330,6 +330,9 @@ std::string FormatStateMessage(const CValidationState &state);
/** Get the BIP9 state for a given deployment at the current tip. */
ThresholdState VersionBitsTipState(const Consensus::Params& params, Consensus::DeploymentPos pos);
+/** Get the numerical staistics for the BIP9 state for a given deployment at the current tip. */
@laanwj

laanwj Feb 20, 2017

Owner

s/staistics/statistics/

@pinheadmz

pinheadmz Feb 21, 2017

Contributor

thanks, fixed

qa/rpc-tests/bip9-softforks.py
- test_blocks = self.generate_blocks(20, 4, test_blocks) # 0x00000004 (signalling not)
- test_blocks = self.generate_blocks(50, activated_version, test_blocks) # 0x20000101 (signalling ready)
- test_blocks = self.generate_blocks(24, 4, test_blocks) # 0x20010000 (signalling not)
+ test_blocks = self.generate_blocks(40, activated_version) # 0x20000001 (signalling ready)
@TheBlueMatt

TheBlueMatt Feb 20, 2017

Contributor

I'd kinda prefer if you didnt change Test 2 here...just add more tests as "Test 1" with a 144-block-total. Also, though I /think/ the new code is right, it'd be really nice if you could add two tests: one which fails to active by 1 block (and checks the "possible" flag is false after the first N), and one which activates with one more signaling block, to make sure the possible flat is set right.

@pinheadmz

pinheadmz Feb 20, 2017

Contributor

I think I see what you mean, test the flag at the end of the period to make sure the flag matches the actual outcome in the end? That's cool for the negative test, but since it can only activate once, the positive test must borrow some blocks from Test 2?

@pinheadmz

pinheadmz Feb 21, 2017

Contributor

@TheBlueMatt I rearranged the tests to hit the thresholds. I put Test 2 back to where it was, but still had to modify Test 3 to cover everything (just borrowed one block and tested the possible flag right before period completion). LMK what you think, thanks

Contributor

TheBlueMatt commented Feb 22, 2017

utACK 9b9b685 thanks so much for sticking with this :).

@@ -330,6 +330,9 @@ std::string FormatStateMessage(const CValidationState &state);
/** Get the BIP9 state for a given deployment at the current tip. */
ThresholdState VersionBitsTipState(const Consensus::Params& params, Consensus::DeploymentPos pos);
+/** Get the numerical statistics for the BIP9 state for a given deployment at the current tip. */
+BIP9Stats VersionBitsTipStatistics(const Consensus::Params& params, Consensus::DeploymentPos pos);
@kallewoof

kallewoof Mar 1, 2017

Member

Nit: I keep getting this and VersionBitsStatistics mixed up when I read through the changes. I think it would help readability to name this e.g. CurrentVersionBitsStatistics or ActiveVersionBitsStatistics or something.

RPC: getblockchaininfo: BIP9 stats
add RPC tests for BIP9 counting stats
Contributor

pinheadmz commented Mar 25, 2017

rebased on master a0b1e57

Member

gmaxwell commented Mar 28, 2017

Concept ACK

@laanwj laanwj merged commit 557c9a6 into bitcoin:master May 23, 2017

1 check passed

continuous-integration/travis-ci/pr The Travis CI build passed
Details

laanwj added a commit that referenced this pull request May 23, 2017

Merge #9571: RPC: getblockchaininfo returns BIP signaling statistics
557c9a6 RPC: getblockchaininfo: BIP9 stats (Matthew Zipkin)

Tree-SHA512: ecf0bf47f04f92becc77acc649fdfa270e768939acce42df39d30069398d40d9a30539862f7c307e08239f78d5c58c470ca5f6e717d2ab8e24db9be0dd7bec0c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment