Skip to content

Commit

Permalink
Auto merge of #4744 - LarryRuane:4721-treestate, r=daira
Browse files Browse the repository at this point in the history
add z_gettreestate RPC

Closes #4721.
  • Loading branch information
zkbot committed Oct 27, 2020
2 parents b9a4f29 + a91a7d3 commit 1d5ed8f
Show file tree
Hide file tree
Showing 4 changed files with 241 additions and 33 deletions.
58 changes: 58 additions & 0 deletions qa/rpc-tests/finalsaplingroot.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from decimal import Decimal

SPROUT_TREE_EMPTY_ROOT = "59d2cde5e65c1414c32ba54f0fe4bdb3d67618125286e6a191317917c812c6d7"
SAPLING_TREE_EMPTY_ROOT = "3e49b5f954aa9d3545bc6c37744661eea48d7c34e3000d82b7f0010c30f4c2fb"
NULL_FIELD = "0000000000000000000000000000000000000000000000000000000000000000"

Expand Down Expand Up @@ -45,13 +46,38 @@ def run_test(self):
# 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()
for height in range(1, blockcount + 1):
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')
Expand All @@ -74,6 +100,17 @@ def run_test(self):
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)
Expand Down Expand Up @@ -107,6 +144,15 @@ def run_test(self):
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 = []
Expand All @@ -126,6 +172,14 @@ def run_test(self):
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 = []
Expand All @@ -144,6 +198,10 @@ def run_test(self):
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()
38 changes: 38 additions & 0 deletions src/gtest/test_rpc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
175 changes: 142 additions & 33 deletions src/rpc/blockchain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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();
}

Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 },
Expand Down
3 changes: 3 additions & 0 deletions src/rpc/server.h
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 1d5ed8f

Please sign in to comment.