Skip to content

Conversation

cvengler
Copy link
Contributor

@cvengler cvengler commented Jul 5, 2019

As such a feature get requested a lot (#16317, #14858 and #8457), I've implemented it into a new RPC call called getblockbyheight as it was discussed in #16317
In the past this feature got requested for the getblock command but it isn't a good idea to interpret a string as a number because it breaks API consistency.

The function is basically just a duplicate of getblock with the exception that it gets the hash by the height (first parameter) and not by the hash as a parameter.

It also checks if the block exists by comparing the height with the total height or if the block is negative.

EDIT: I improved the code with the help of fqlx so it no longer contains some of the getblock legacy.

CDataStream ssBlock(SER_NETWORK, PROTOCOL_VERSION | RPCSerializationFlags());
ssBlock << block;
std::string strHex = HexStr(ssBlock.begin(), ssBlock.end());
return strHex;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will std::string be converted to the univalue object? Do we need to cast to a JSON object?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::string is a valid UniValue object and is used in many other RPCs.

block = GetBlockChecked(pblockindex);
}

if (verbosity <= 0)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no documentation for when verbosity is less than 0. I don't think it can ever be? Should verbosity be an unsigned int or enum?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope verbosity cannot be less than zero

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove this check for less than zero and change verbosity to an unsigned short or preferably an enum of constants 0-3?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC it is undefined behavior to cast an int to an enum that is out of range of the enum. Because any int can be entered in the RPC, it could be out of range of the enum and cause undesirable things. So I think it is actually better to just handle it as an int. Also, UniValue does not have a get_short() function so there would be no way to fetch a short from the request other than casting which I think is undesirable especially when it's easier to just handle it as an int.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate the response @achow101. I would then suggest we add a validate and transform for the RPC call but that is out of the scope of the PR. Validation from the client is necessary in these cases so we don't get undesirable things and it would keep the code clean.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validation from the client is not possible. The client is not necessarily bitcoin-cli. It could be any JSON-RPC client. Validation of this must happen server side.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validation from the client is necessary in these cases so we don't get undesirable things and it would keep the code clean.

No.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fqlx from the server point of view, never trust the client.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would test the argument for valid range and throw an rpc error if out of range. Better to accept just the valid values IMO.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed on filtering for valid range

int verbosity = 1;
if (!request.params[1].isNull()) {
if(request.params[1].isNum())
verbosity = request.params[1].get_int();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't tell if this returns negative or >3

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_int can return any valid int.

@cvengler
Copy link
Contributor Author

cvengler commented Jul 5, 2019

@fqlx Actually I do not think it is a good idea to move int verbosity to something else.
This could break the API backwards compatibility, maybe some 3rd party APIs are working with negative numbers for the verbosity. If we would use an unsigned variable it would cause an overflow and then the output would be the opposite

@cvengler
Copy link
Contributor Author

cvengler commented Jul 5, 2019

@fqlx I made some changes you've suggested.

@fqlx
Copy link

fqlx commented Jul 5, 2019

Can you add tests?

@cvengler
Copy link
Contributor Author

cvengler commented Jul 5, 2019

I don't see the need for a test for such a function.

@promag
Copy link
Contributor

promag commented Jul 5, 2019

@emilengler every change should have a test, especially a new RPC. Also in this case you should a release note too.

@fqlx
Copy link

fqlx commented Jul 5, 2019

The tests are good not only to increase confidence in our codebase but to also show a working example how to use it. The code will used by 1000s of people

@achow101
Copy link
Member

achow101 commented Jul 6, 2019

I don't see the need for a test for such a function.

There should be at least a basic test that checks getblockbyheight(height) is consistent with getblock(getblockhash(height))

@achow101
Copy link
Member

achow101 commented Jul 6, 2019

Concept ACK

failing a linter:

This diff appears to have added new lines with trailing whitespace.
The following changes were suspected:
diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp
@@ -942,0 +943,97 @@ static UniValue getblock(const JSONRPCRequest& request)
+
+
^---- failure generated from test/lint/lint-whitespace.sh

@cvengler
Copy link
Contributor Author

cvengler commented Jul 6, 2019

@fqlx @promag @achow101
Test added, can someone take a look at it?

Copy link
Contributor

@promag promag left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An alternative to overloading hash in getblock is to add an extra parameter to getblock, like:

getblock hash ( height )

Then:

  • if hash is null then use height
  • if both are set then they must match.

This would look good if called with named parameter getblock(height=20).

@promag
Copy link
Contributor

promag commented Jul 6, 2019

I can't unresolve #16345 (comment), but please address my comment there.

@cvengler
Copy link
Contributor Author

cvengler commented Jul 6, 2019

@promag I pushed a new commit which fix this and unresolved the conversation.

Copy link
Member

@fanquake fanquake left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@emilengler Thanks for contributing. Before this can be merged, or reviewed, this PR needs a few changes.

The macOS build, and others, are currently failing on Travis:

rpc/blockchain.cpp:1019:19: error: calling function 'LookupBlockIndex' requires holding mutex 'cs_main' exclusively [-Werror,-Wthread-safety-analysis]
    pblockindex = LookupBlockIndex(hash);
                  ^
1 error generated.

Once you've fixed any problems, and you believe your code is ready for review, can you please squash your commits. You could either squash everything into a single commit, or two commits. One that adds the new RPC command and a second commit that adds the test. When you do so, make sure you use clear and meaningful commit messages. The general convention is to prefix with the part of the code you are modifying.

I've also updated the PR title and body. We don't use @ mentions in commits or PR text, because they just lead to spam when downstream projects pull our changes.

@fanquake fanquake changed the title RPC: Add getblockbyheight rpc: Add getblockbyheight method Jul 8, 2019
}

// Check if block exists or is negative
if(::ChainActive().Height() < request.params[0].get_int() || request.params[0].get_int() <= -1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: having a blockheight local will make this more legible imo

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is only used 2 times, I think it is not necessary.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 times (see below)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, add {}

@cvengler
Copy link
Contributor Author

cvengler commented Jul 9, 2019

@fanquake Thank you for writing such an informative comment!
I have fixed to compilation errors and squashed to commits.

@cvengler
Copy link
Contributor Author

cvengler commented Jul 9, 2019

I've also updated the commit messages with the rpc prefix

@instagibbs
Copy link
Member

I don't see the need for a test for such a function.

Is how we get RPC bugs into releases :D

concept ACK


// Check if block exists or is negative
if(::ChainActive().Height() < request.params[0].get_int() || request.params[0].get_int() <= -1)
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block " + std::to_string(request.params[0].get_int()) + " not found");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feel-free-to-ignore-nit: error could be RPC_INVALID_PARAMETER

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok

node = self.nodes[0]
node.add_p2p_connection(P2PInterface())
assert_equal(node.getblock(node.getblockhash(20)), node.getblockbyheight(20))

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good follow-up testing would be to make sure -1, 0, getblockcount(), and getblockcount()+1 args respond properly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, exercising the 2nd arg once in the test would be 👍

block = GetBlockChecked(pblockindex);
}

if (verbosity <= 0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed on filtering for valid range

}

// Check if block exists or is negative
if(::ChainActive().Height() < request.params[0].get_int() || request.params[0].get_int() <= -1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, add {}

@cvengler
Copy link
Contributor Author

@promag First there was a type sorry
I meant:
I think it should be added due to lots of requests.

To the second point: I don't have numbers to prove it but I'm very sure that 2 separate calls are slower.

@promag
Copy link
Contributor

promag commented Jul 17, 2019

I know it's slower, but how much? How much this saves?

@sipa
Copy link
Member

sipa commented Jul 17, 2019

I don't think speed is a good rationale for this PR. If speed was really a concern, you wouldn't be using Bitcoin Core RPCs in the first place.

The only real reason to want this is convenience. I'm sure that's the real reason this is often requested, and I'm mildly in favor of it because of that.

@maflcko
Copy link
Member

maflcko commented Jul 17, 2019

See also " GUI: Add generate method #16000 ".

Basically it would be nice to have a way to set aliases like generate=generatetoaddress(getnewaddress()) or getblockbyheight=getblock(getblockhash($1)). This is really easy if you call the rpc from bash or python, but not when calling from the gui.

@ajtowns
Copy link
Contributor

ajtowns commented Jul 18, 2019

The only real reason to want this is convenience.

How about just treating a blockhash "@123" as a request of the block at height 123, rather than an error for not being 64 hex digits? That's pretty convenient for manual use from the cli and gui, and pretty easy to code (and thus to do for other RPCs that want a block hash):

$ bitcoin-cli -regtest getblock $(bitcoin-cli -regtest getblockhash 0) | grep ^...hash
  "hash": "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206",
$ bitcoin-cli -regtest getblock @0 | grep ^...hash
  "hash":  "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206",
+inline uint256 GetBlockHashFromParam(const UniValue& param, const std::string& name) EXCLUSIVE_LOCKS_REQUIRED(cs_main)
+{
+    const std::string& v = param.get_str();
+    if (v.size() > 1 && v[0] == '@') {
+        // treat as height, and lookup
+        int32_t height;
+        if (!ParseInt32(v.substr(1), &height)) {
+            throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Target block height %s must be @ followed by a number", v));
+        }
+        if (height < 0) {
+            throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Target block height %d is negative", height));
+        }
+        const int current_tip = ::ChainActive().Height();
+        if (height > current_tip) {
+            throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Target block height %d after current tip %d", height, current_tip));
+        }
+        return *(::ChainActive()[height]->phashBlock);
+    } else {
+        return ParseHashV(param, name);
+    }
+}
+
 static UniValue getblock(const JSONRPCRequest& request)
 {
     const RPCHelpMan help{"getblock",
@@ -904,8 +926,6 @@ static UniValue getblock(const JSONRPCRequest& request)
         throw std::runtime_error(help.ToString());
     }
 
-    uint256 hash(ParseHashV(request.params[0], "blockhash"));
-
     int verbosity = 1;
     if (!request.params[1].isNull()) {
         if(request.params[1].isNum())
@@ -919,6 +939,7 @@ static UniValue getblock(const JSONRPCRequest& request)
     const CBlockIndex* tip;
     {
         LOCK(cs_main);
+        uint256 hash(GetBlockHashFromParam(request.params[0], "blockhash"));
         pblockindex = LookupBlockIndex(hash);
         tip = ::ChainActive().Tip();

@cvengler
Copy link
Contributor Author

@ajtowns I think it is better to have a seperate command for this. This could cause confusion

@promag
Copy link
Contributor

promag commented Jul 18, 2019

Concept NAK from me sorry, please provide benchmarks to support the new RPC - which IMO is the only valid reason to add redundancy to the RPC interface.

@cvengler
Copy link
Contributor Author

This would make it even faster to get a block just by its height. Currently you need to execute two separate commands which is slower and more complicated in two ways: For the machine and for the user and such a feature get requested a lot

@promag
Copy link
Contributor

promag commented Jul 18, 2019

I went ahead and made a quick profile:

count = 5000
node.generate(count)

for i in range(count):
    node.getblock(self.nodes[0].getblockhash(i))

# takes ~ 3742ms

for i in range(count):
    node.getblockbyheight(i)

# takes ~ 2193ms

Note that it fetches 5000 blocks but these are really small blocks and you should try with bigger blocks - I think it will make a difference in the comparison - I mean getblockbyheight would just be slightly faster.

@cvengler
Copy link
Contributor Author

@promag Thanks for doing the test but it is even faster for the user to type the command

@cvengler
Copy link
Contributor Author

Currently there are:
3 ACKs
1 NACK

rpc: Fix whitespace in rpc/blockchain.cpp

rpc: getblockbyheight: Remove hash
@ajtowns
Copy link
Contributor

ajtowns commented Jul 23, 2019

Note that it fetches 5000 blocks but these are really small blocks and you should try with bigger blocks - I think it will make a difference in the comparison - I mean getblockbyheight would just be slightly faster.

It looks like even with bigger blocks avoiding the two RPCs is a fair bit faster:

$ time for a in `seq 400000 400500`; do bitcoin-cli getblock $(bitcoin-cli getblockhash $a); done | grep -v '"confirmations":' | sha256sum
28620dea517c27debd958345197dc911f2fe95934742293548e79cd673bfae40  -

real	0m15.763s
$ time for a in `seq 400000 400500`; do ./bitcoin-cli getblockbyheight $a; done | grep -v '"confirmations":' | sha256sum
28620dea517c27debd958345197dc911f2fe95934742293548e79cd673bfae40  -

real	0m11.514s
$ time for a in `seq 400000 400500`; do bitcoin-cli getblock $(bitcoin-cli getblockhash $a); done | grep -v '"confirmations":' | sha256sum
28620dea517c27debd958345197dc911f2fe95934742293548e79cd673bfae40  -

real	0m15.408s

@ajtowns
Copy link
Contributor

ajtowns commented Jul 23, 2019

@emilengler note that this call can't be cached whereas getblock by hash can.

Strictly speaking, "getblock(hash)" can't be cached either, because it includes a "confirmations" key which changes every time a new block gets mined

@jnewbery
Copy link
Contributor

I'm a mild concept NACK and an approach NACK on this.

  • mild concept NACK: unless there's a very compelling reason, I don't think the RPC interface should offer redundant ways to do the same thing. Increasing the surface area of the RPC interface increases the maintenance burden, so we should try to keep it minimal wherever possible.
  • approach NACK: this approach duplicates a lot of code, both in the implementation and in the RPC help text. That could be improved by factoring both the logic and help text out, and calling it from both getblock and getblockatheight.

Overall, I prefer #16439. I'm not a fan of magic incantations like @height in general, but that PR seems like a pretty clean implementation and there's absolutely no ambiguity between getblock @height and getblock hash.

@ryanofsky
Copy link
Contributor

@emilengler re: your question in IRC

[01:04:54] <emilengler> How the CTRL+L shortcut is being handled in bitcoin-qt? Over a QAction or a QShortcut?

It's just a property of the clear button

<property name="shortcut">
<string notr="true">Ctrl+L</string>
</property>

@cvengler
Copy link
Contributor Author

@ryanofsky Thank you, already got this :)
But I don't think this is the right place to discuss this.
An IRC direct message had done the same

@luke-jr
Copy link
Member

luke-jr commented Aug 19, 2019

Concept NACK. getblock(getblockhash(1)) is simple enough.

@cvengler
Copy link
Contributor Author

@luke-jr It probably is but in my opinion it isn't really shown that using RPC commands as parameters is possible (at least I didn't saw something like this)

@DrahtBot
Copy link
Contributor

Needs rebase

@laanwj laanwj added the Feature label Sep 30, 2019
@cvengler
Copy link
Contributor Author

Closed because of lack of interest and merging conflicts

@cvengler cvengler closed this Oct 14, 2019
@cvengler cvengler deleted the getblockbyheight branch October 14, 2019 15:27
@bitcoin bitcoin locked as resolved and limited conversation to collaborators Dec 16, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.