Auto merge of #4744 - LarryRuane:4721-treestate, r=daira

add z_gettreestate RPC

Closes #4721.
This commit is contained in:
Homu 2020-10-27 09:46:56 +00:00
commit 1d5ed8fa2f
4 changed files with 241 additions and 33 deletions

View File

@ -16,6 +16,7 @@ from test_framework.util import (
from decimal import Decimal
SPROUT_TREE_EMPTY_ROOT = "59d2cde5e65c1414c32ba54f0fe4bdb3d67618125286e6a191317917c812c6d7"
SAPLING_TREE_EMPTY_ROOT = "3e49b5f954aa9d3545bc6c37744661eea48d7c34e3000d82b7f0010c30f4c2fb"
NULL_FIELD = "0000000000000000000000000000000000000000000000000000000000000000"
@ -45,6 +46,19 @@ class FinalSaplingRootTest(BitcoinTestFramework):
# Verfify genesis block contains null field for what is now called the final sapling root field.
blk = self.nodes[0].getblock("0")
assert_equal(blk["finalsaplingroot"], NULL_FIELD)
treestate = self.nodes[0].z_gettreestate("0")
assert_equal(treestate["height"], 0)
assert_equal(treestate["hash"], self.nodes[0].getblockhash(0))
assert_equal(treestate["sprout"]["commitments"]["finalRoot"], SPROUT_TREE_EMPTY_ROOT)
assert_equal(treestate["sprout"]["commitments"]["finalState"], "000000")
assert("skipHash" not in treestate["sprout"])
assert_equal(treestate["sapling"]["commitments"]["finalRoot"], NULL_FIELD)
# There is no sapling state tree yet, and trying to find it in an earlier
# block won't succeed (we're at genesis block), so skipHash is absent.
assert("finalState" not in treestate["sapling"])
assert("skipHash" not in treestate["sapling"])
# Verify all generated blocks contain the empty root of the Sapling tree.
blockcount = self.nodes[0].getblockcount()
@ -52,6 +66,18 @@ class FinalSaplingRootTest(BitcoinTestFramework):
blk = self.nodes[0].getblock(str(height))
assert_equal(blk["finalsaplingroot"], SAPLING_TREE_EMPTY_ROOT)
treestate = self.nodes[0].z_gettreestate(str(height))
assert_equal(treestate["height"], height)
assert_equal(treestate["hash"], self.nodes[0].getblockhash(height))
assert("skipHash" not in treestate["sprout"])
assert_equal(treestate["sprout"]["commitments"]["finalRoot"], SPROUT_TREE_EMPTY_ROOT)
assert_equal(treestate["sprout"]["commitments"]["finalState"], "000000")
assert("skipHash" not in treestate["sapling"])
assert_equal(treestate["sapling"]["commitments"]["finalRoot"], SAPLING_TREE_EMPTY_ROOT)
assert_equal(treestate["sapling"]["commitments"]["finalState"], "000000")
# Node 0 shields some funds
taddr0 = get_coinbase_address(self.nodes[0])
saplingAddr0 = self.nodes[0].z_getnewaddress('sapling')
@ -74,6 +100,17 @@ class FinalSaplingRootTest(BitcoinTestFramework):
result = self.nodes[0].getrawtransaction(mytxid, 1)
assert_equal(len(result["vShieldedOutput"]), 1)
# Since there is a now sapling shielded input in the blockchain,
# the sapling values should have changed
new_treestate = self.nodes[0].z_gettreestate(str(-1))
assert_equal(new_treestate["sapling"]["commitments"]["finalRoot"], root)
assert_equal(new_treestate["sprout"], treestate["sprout"])
assert(new_treestate["sapling"]["commitments"]["finalRoot"] != treestate["sapling"]["commitments"]["finalRoot"])
assert(new_treestate["sapling"]["commitments"]["finalState"] != treestate["sapling"]["commitments"]["finalState"])
assert_equal(len(new_treestate["sapling"]["commitments"]["finalRoot"]), 64)
assert_equal(len(new_treestate["sapling"]["commitments"]["finalState"]), 70)
treestate = new_treestate
# Mine an empty block and verify the final Sapling root does not change
self.sync_all()
self.nodes[0].generate(1)
@ -107,6 +144,15 @@ class FinalSaplingRootTest(BitcoinTestFramework):
assert_equal(self.nodes[1].z_getbalance(zaddr1), Decimal("10"))
assert_equal(root, self.nodes[0].getblock("204")["finalsaplingroot"])
new_treestate = self.nodes[0].z_gettreestate(str(-1))
assert_equal(new_treestate["sapling"]["commitments"]["finalRoot"], root)
assert_equal(new_treestate["sapling"], treestate["sapling"])
assert(new_treestate["sprout"]["commitments"]["finalRoot"] != treestate["sprout"]["commitments"]["finalRoot"])
assert(new_treestate["sprout"]["commitments"]["finalState"] != treestate["sprout"]["commitments"]["finalState"])
assert_equal(len(new_treestate["sprout"]["commitments"]["finalRoot"]), 64)
assert_equal(len(new_treestate["sprout"]["commitments"]["finalState"]), 134)
treestate = new_treestate
# Mine a block with a Sapling shielded recipient and verify the final Sapling root changes
saplingAddr1 = self.nodes[1].z_getnewaddress("sapling")
recipients = []
@ -126,6 +172,14 @@ class FinalSaplingRootTest(BitcoinTestFramework):
result = self.nodes[0].getrawtransaction(mytxid, 1)
assert_equal(len(result["vShieldedOutput"]), 2) # there is Sapling shielded change
new_treestate = self.nodes[0].z_gettreestate(str(-1))
assert_equal(new_treestate["sprout"], treestate["sprout"])
assert(new_treestate["sapling"]["commitments"]["finalRoot"] != treestate["sapling"]["commitments"]["finalRoot"])
assert(new_treestate["sapling"]["commitments"]["finalState"] != treestate["sapling"]["commitments"]["finalState"])
assert_equal(len(new_treestate["sapling"]["commitments"]["finalRoot"]), 64)
assert_equal(len(new_treestate["sapling"]["commitments"]["finalState"]), 136)
treestate = new_treestate
# Mine a block with a Sapling shielded sender and transparent recipient and verify the final Sapling root doesn't change
taddr2 = self.nodes[0].getnewaddress()
recipients = []
@ -144,6 +198,10 @@ class FinalSaplingRootTest(BitcoinTestFramework):
root = blk["finalsaplingroot"]
assert_equal(root, self.nodes[0].getblock("205")["finalsaplingroot"])
new_treestate = self.nodes[0].z_gettreestate(str(-1))
assert_equal(new_treestate["sprout"], treestate["sprout"])
assert_equal(new_treestate["sapling"], treestate["sapling"])
if __name__ == '__main__':
FinalSaplingRootTest().main()

View File

@ -67,3 +67,41 @@ TEST(rpc, CheckExperimentalDisabledHelpMsg) {
"experimentalfeatures=1\n"
"athirdvalue=1\n");
}
TEST(rpc, ParseHeightArg) {
EXPECT_EQ(parseHeightArg("15", 21), 15);
EXPECT_EQ(parseHeightArg("21", 21), 21);
ASSERT_THROW(parseHeightArg("22", 21), UniValue);
EXPECT_EQ(parseHeightArg("0", 21), 0);
EXPECT_EQ(parseHeightArg("011", 21), 11); // allowed and parsed as decimal, not octal
// negative values count back from current height
EXPECT_EQ(parseHeightArg("-1", 21), 21);
EXPECT_EQ(parseHeightArg("-2", 21), 20);
EXPECT_EQ(parseHeightArg("-22", 21), 0);
ASSERT_THROW(parseHeightArg("-23", 21), UniValue);
ASSERT_THROW(parseHeightArg("-0", 21), UniValue);
// currentHeight zero
EXPECT_EQ(parseHeightArg("0", 0), 0);
EXPECT_EQ(parseHeightArg("-1", 0), 0);
// maximum possible height, just beyond, far beyond
EXPECT_EQ(parseHeightArg("2147483647", 2147483647), 2147483647);
ASSERT_THROW(parseHeightArg("2147483648", 2147483647), UniValue);
ASSERT_THROW(parseHeightArg("999999999999999999999999999999999999999", 21), UniValue);
// disallowed characters and formats
ASSERT_THROW(parseHeightArg("5.21", 21), UniValue);
ASSERT_THROW(parseHeightArg("5.0", 21), UniValue);
ASSERT_THROW(parseHeightArg("a21", 21), UniValue);
ASSERT_THROW(parseHeightArg(" 21", 21), UniValue);
ASSERT_THROW(parseHeightArg("21 ", 21), UniValue);
ASSERT_THROW(parseHeightArg("21x", 21), UniValue);
ASSERT_THROW(parseHeightArg("+21", 21), UniValue);
ASSERT_THROW(parseHeightArg("0x15", 21), UniValue);
ASSERT_THROW(parseHeightArg("-0", 21), UniValue);
ASSERT_THROW(parseHeightArg("-01", 21), UniValue);
ASSERT_THROW(parseHeightArg("-0x15", 21), UniValue);
ASSERT_THROW(parseHeightArg("", 21), UniValue);
}

View File

@ -570,6 +570,37 @@ UniValue getblockhashes(const UniValue& params, bool fHelp)
return result;
}
//! Sanity-check a height argument and interpret negative values.
int interpretHeightArg(int nHeight, int currentHeight)
{
if (nHeight < 0) {
nHeight += currentHeight + 1;
}
if (nHeight < 0 || nHeight > currentHeight) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Block height out of range");
}
return nHeight;
}
//! Parse and sanity-check a height argument, return its integer representation.
int parseHeightArg(const std::string& strHeight, int currentHeight)
{
// std::stoi allows (locale-dependent) whitespace and optional '+' sign,
// whereas we want to be strict.
regex r("(?:(-?)[1-9][0-9]*|[0-9]+)");
if (!regex_match(strHeight, r)) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid block height parameter");
}
int nHeight;
try {
nHeight = std::stoi(strHeight);
}
catch (const std::exception &e) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid block height parameter");
}
return interpretHeightArg(nHeight, currentHeight);
}
UniValue getblockhash(const UniValue& params, bool fHelp)
{
if (fHelp || params.size() != 1)
@ -587,16 +618,7 @@ UniValue getblockhash(const UniValue& params, bool fHelp)
LOCK(cs_main);
int nHeight = params[0].get_int();
if (nHeight < 0) {
nHeight += chainActive.Height() + 1;
}
if (nHeight < 0 || nHeight > chainActive.Height())
throw JSONRPCError(RPC_INVALID_PARAMETER, "Block height out of range");
CBlockIndex* pblockindex = chainActive[nHeight];
const CBlockIndex* pblockindex = chainActive[interpretHeightArg(params[0].get_int(), chainActive.Height())];
return pblockindex->GetBlockHash().GetHex();
}
@ -711,29 +733,7 @@ UniValue getblock(const UniValue& params, bool fHelp)
// If height is supplied, find the hash
if (strHash.size() < (2 * sizeof(uint256))) {
// std::stoi allows characters, whereas we want to be strict
regex r("(?:(-?)[1-9][0-9]*|[0-9]+)");
if (!regex_match(strHash, r)) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid block height parameter");
}
int nHeight = -1;
try {
nHeight = std::stoi(strHash);
}
catch (const std::exception &e) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid block height parameter");
}
if (nHeight < 0) {
nHeight += chainActive.Height() + 1;
}
if (nHeight < 0 || nHeight > chainActive.Height()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Block height out of range");
}
strHash = chainActive[nHeight]->GetBlockHash().GetHex();
strHash = chainActive[parseHeightArg(strHash, chainActive.Height())]->GetBlockHash().GetHex();
}
uint256 hash(uint256S(strHash));
@ -1202,6 +1202,114 @@ UniValue getchaintips(const UniValue& params, bool fHelp)
return res;
}
UniValue z_gettreestate(const UniValue& params, bool fHelp)
{
if (fHelp || params.size() != 1)
throw runtime_error(
"z_gettreestate \"hash|height\"\n"
"Return information about the given block's tree state.\n"
"\nArguments:\n"
"1. \"hash|height\" (string, required) The block hash or height. Height can be negative where -1 is the last known valid block\n"
"\nResult:\n"
"{\n"
" \"hash\": \"hash\", (string) hex block hash\n"
" \"height\": n, (numeric) block height\n"
" \"sprout\": {\n"
" \"skipHash\": \"hash\", (string) hash of most recent block with more information\n"
" \"commitments\": {\n"
" \"finalRoot\": \"hex\", (string)\n"
" \"finalState\": \"hex\" (string)\n"
" }\n"
" },\n"
" \"sapling\": {\n"
" \"skipHash\": \"hash\", (string) hash of most recent block with more information\n"
" \"commitments\": {\n"
" \"finalRoot\": \"hex\", (string)\n"
" \"finalState\": \"hex\" (string)\n"
" }\n"
" }\n"
"}\n"
"\nExamples:\n"
+ HelpExampleCli("z_gettreestate", "\"00000000febc373a1da2bd9f887b105ad79ddc26ac26c2b28652d64e5207c5b5\"")
+ HelpExampleRpc("z_gettreestate", "\"00000000febc373a1da2bd9f887b105ad79ddc26ac26c2b28652d64e5207c5b5\"")
+ HelpExampleCli("z_gettreestate", "12800")
+ HelpExampleRpc("z_gettreestate", "12800")
);
LOCK(cs_main);
std::string strHash = params[0].get_str();
// If height is supplied, find the hash
if (strHash.size() < (2 * sizeof(uint256))) {
strHash = chainActive[parseHeightArg(strHash, chainActive.Height())]->GetBlockHash().GetHex();
}
uint256 hash(uint256S(strHash));
if (mapBlockIndex.count(hash) == 0)
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found");
const CBlockIndex* const pindex = mapBlockIndex[hash];
if (!chainActive.Contains(pindex)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Requested block is not part of the main chain");
}
UniValue res(UniValue::VOBJ);
res.pushKV("hash", pindex->GetBlockHash().GetHex());
res.pushKV("height", pindex->nHeight);
res.pushKV("time", int64_t(pindex->nTime));
// sprout
{
UniValue sprout_result(UniValue::VOBJ);
UniValue sprout_commitments(UniValue::VOBJ);
sprout_commitments.pushKV("finalRoot", pindex->hashFinalSproutRoot.GetHex());
SproutMerkleTree tree;
if (pcoinsTip->GetSproutAnchorAt(pindex->hashFinalSproutRoot, tree)) {
CDataStream s(SER_NETWORK, PROTOCOL_VERSION);
s << tree;
sprout_commitments.pushKV("finalState", HexStr(s.begin(), s.end()));
} else {
// Set skipHash to the most recent block that has a finalState.
const CBlockIndex* pindex_skip = pindex->pprev;
while (pindex_skip && !pcoinsTip->GetSproutAnchorAt(pindex_skip->hashFinalSproutRoot, tree)) {
pindex_skip = pindex_skip->pprev;
}
if (pindex_skip) {
sprout_result.pushKV("skipHash", pindex_skip->GetBlockHash().GetHex());
}
}
sprout_result.pushKV("commitments", sprout_commitments);
res.pushKV("sprout", sprout_result);
}
// sapling
{
UniValue sapling_result(UniValue::VOBJ);
UniValue sapling_commitments(UniValue::VOBJ);
sapling_commitments.pushKV("finalRoot", pindex->hashFinalSaplingRoot.GetHex());
bool need_skiphash = false;
SaplingMerkleTree tree;
if (pcoinsTip->GetSaplingAnchorAt(pindex->hashFinalSaplingRoot, tree)) {
CDataStream s(SER_NETWORK, PROTOCOL_VERSION);
s << tree;
sapling_commitments.pushKV("finalState", HexStr(s.begin(), s.end()));
} else {
// Set skipHash to the most recent block that has a finalState.
const CBlockIndex* pindex_skip = pindex->pprev;
while (pindex_skip && !pcoinsTip->GetSaplingAnchorAt(pindex_skip->hashFinalSaplingRoot, tree)) {
pindex_skip = pindex_skip->pprev;
}
if (pindex_skip) {
sapling_result.pushKV("skipHash", pindex_skip->GetBlockHash().GetHex());
}
}
sapling_result.pushKV("commitments", sapling_commitments);
res.pushKV("sapling", sapling_result);
}
return res;
}
UniValue mempoolInfoToJSON()
{
UniValue ret(UniValue::VOBJ);
@ -1323,6 +1431,7 @@ static const CRPCCommand commands[] =
{ "blockchain", "getblockhash", &getblockhash, true },
{ "blockchain", "getblockheader", &getblockheader, true },
{ "blockchain", "getchaintips", &getchaintips, true },
{ "blockchain", "z_gettreestate", &z_gettreestate, true },
{ "blockchain", "getdifficulty", &getdifficulty, true },
{ "blockchain", "getmempoolinfo", &getmempoolinfo, true },
{ "blockchain", "getrawmempool", &getrawmempool, true },

View File

@ -185,4 +185,7 @@ std::string JSONRPCExecBatch(const UniValue& vReq);
extern std::string experimentalDisabledHelpMsg(const std::string& rpc, const std::vector<std::string>& enableArgs);
extern int interpretHeightArg(int nHeight, int currentHeight);
extern int parseHeightArg(const std::string& strHeight, int currentHeight);
#endif // BITCOIN_RPCSERVER_H