diff --git a/src/chain.h b/src/chain.h index b7e8a9176..5e5258339 100644 --- a/src/chain.h +++ b/src/chain.h @@ -16,6 +16,8 @@ #include +static const int SPROUT_VALUE_VERSION = 1001400; + struct CDiskBlockPos { int nFile; @@ -144,6 +146,15 @@ public: //! (memory only) The anchor for the tree state up to the end of this block uint256 hashAnchorEnd; + //! Change in value held by the Sprout circuit over this block. + //! Will be boost::none for older blocks on old nodes until a reindex has taken place. + boost::optional nSproutValue; + + //! (memory only) Total value held by the Sprout circuit up to and including this block. + //! Will be boost::none for on old nodes until a reindex has taken place. + //! Will be boost::none if nChainTx is zero. + boost::optional nChainSproutValue; + //! block header int nVersion; uint256 hashMerkleRoot; @@ -172,6 +183,8 @@ public: hashAnchor = uint256(); hashAnchorEnd = uint256(); nSequenceId = 0; + nSproutValue = boost::none; + nChainSproutValue = boost::none; nVersion = 0; hashMerkleRoot = uint256(); @@ -339,6 +352,14 @@ public: READWRITE(nBits); READWRITE(nNonce); READWRITE(nSolution); + + // Only read/write nSproutValue if the client version used to create + // this index was storing them. + // TODO: See if we can get away with not serializing the boost::optional + // one-byte header, without requiring users to reindex on upgrade. + if (nType & SER_DISK && nVersion >= SPROUT_VALUE_VERSION) { + READWRITE(nSproutValue); + } } uint256 GetBlockHash() const diff --git a/src/gtest/test_validation.cpp b/src/gtest/test_validation.cpp index 21ed20d84..6f603eaec 100644 --- a/src/gtest/test_validation.cpp +++ b/src/gtest/test_validation.cpp @@ -2,6 +2,18 @@ #include "consensus/validation.h" #include "main.h" +#include "utiltest.h" + +extern ZCJoinSplit* params; + +extern bool ReceivedBlockTransactions(const CBlock &block, CValidationState& state, CBlockIndex *pindexNew, const CDiskBlockPos& pos); + +void ExpectOptionalAmount(CAmount expected, boost::optional actual) { + EXPECT_TRUE((bool)actual); + if (actual) { + EXPECT_EQ(expected, *actual); + } +} // Fake an empty view class FakeCoinsViewDB : public CCoinsView { @@ -61,3 +73,77 @@ TEST(Validation, ContextualCheckInputsPassesWithCoinbase) { CValidationState state; EXPECT_TRUE(ContextualCheckInputs(tx, state, view, false, 0, false, Params(CBaseChainParams::MAIN).GetConsensus())); } + +TEST(Validation, ReceivedBlockTransactions) { + auto sk = libzcash::SpendingKey::random(); + + // Create a fake genesis block + CBlock block1; + block1.vtx.push_back(GetValidReceive(*params, sk, 5, true)); + block1.hashMerkleRoot = block1.BuildMerkleTree(); + CBlockIndex fakeIndex1 {block1}; + + // Create a fake child block + CBlock block2; + block2.hashPrevBlock = block1.GetHash(); + block2.vtx.push_back(GetValidReceive(*params, sk, 10, true)); + block2.hashMerkleRoot = block2.BuildMerkleTree(); + CBlockIndex fakeIndex2 {block2}; + fakeIndex2.pprev = &fakeIndex1; + + CDiskBlockPos pos1; + CDiskBlockPos pos2; + + // Set initial state of indices + ASSERT_TRUE(fakeIndex1.RaiseValidity(BLOCK_VALID_TREE)); + ASSERT_TRUE(fakeIndex2.RaiseValidity(BLOCK_VALID_TREE)); + EXPECT_TRUE(fakeIndex1.IsValid(BLOCK_VALID_TREE)); + EXPECT_TRUE(fakeIndex2.IsValid(BLOCK_VALID_TREE)); + EXPECT_FALSE(fakeIndex1.IsValid(BLOCK_VALID_TRANSACTIONS)); + EXPECT_FALSE(fakeIndex2.IsValid(BLOCK_VALID_TRANSACTIONS)); + + // Sprout pool values should not be set + EXPECT_FALSE((bool)fakeIndex1.nSproutValue); + EXPECT_FALSE((bool)fakeIndex1.nChainSproutValue); + EXPECT_FALSE((bool)fakeIndex2.nSproutValue); + EXPECT_FALSE((bool)fakeIndex2.nChainSproutValue); + + // Mark the second block's transactions as received first + CValidationState state; + EXPECT_TRUE(ReceivedBlockTransactions(block2, state, &fakeIndex2, pos2)); + EXPECT_FALSE(fakeIndex1.IsValid(BLOCK_VALID_TRANSACTIONS)); + EXPECT_TRUE(fakeIndex2.IsValid(BLOCK_VALID_TRANSACTIONS)); + + // Sprout pool value delta should now be set for the second block, + // but not any chain totals + EXPECT_FALSE((bool)fakeIndex1.nSproutValue); + EXPECT_FALSE((bool)fakeIndex1.nChainSproutValue); + { + SCOPED_TRACE("ExpectOptionalAmount call"); + ExpectOptionalAmount(20, fakeIndex2.nSproutValue); + } + EXPECT_FALSE((bool)fakeIndex2.nChainSproutValue); + + // Now mark the first block's transactions as received + EXPECT_TRUE(ReceivedBlockTransactions(block1, state, &fakeIndex1, pos1)); + EXPECT_TRUE(fakeIndex1.IsValid(BLOCK_VALID_TRANSACTIONS)); + EXPECT_TRUE(fakeIndex2.IsValid(BLOCK_VALID_TRANSACTIONS)); + + // Sprout pool values should now be set for both blocks + { + SCOPED_TRACE("ExpectOptionalAmount call"); + ExpectOptionalAmount(10, fakeIndex1.nSproutValue); + } + { + SCOPED_TRACE("ExpectOptionalAmount call"); + ExpectOptionalAmount(10, fakeIndex1.nChainSproutValue); + } + { + SCOPED_TRACE("ExpectOptionalAmount call"); + ExpectOptionalAmount(20, fakeIndex2.nSproutValue); + } + { + SCOPED_TRACE("ExpectOptionalAmount call"); + ExpectOptionalAmount(30, fakeIndex2.nChainSproutValue); + } +} diff --git a/src/main.cpp b/src/main.cpp index a8ec07403..d8e47caac 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2835,6 +2835,15 @@ bool ReceivedBlockTransactions(const CBlock &block, CValidationState& state, CBl { pindexNew->nTx = block.vtx.size(); pindexNew->nChainTx = 0; + CAmount sproutValue = 0; + for (auto tx : block.vtx) { + for (auto js : tx.vjoinsplit) { + sproutValue += js.vpub_old; + sproutValue -= js.vpub_new; + } + } + pindexNew->nSproutValue = sproutValue; + pindexNew->nChainSproutValue = boost::none; pindexNew->nFile = pos.nFile; pindexNew->nDataPos = pos.nPos; pindexNew->nUndoPos = 0; @@ -2852,6 +2861,15 @@ bool ReceivedBlockTransactions(const CBlock &block, CValidationState& state, CBl CBlockIndex *pindex = queue.front(); queue.pop_front(); pindex->nChainTx = (pindex->pprev ? pindex->pprev->nChainTx : 0) + pindex->nTx; + if (pindex->pprev) { + if (pindex->pprev->nChainSproutValue && pindex->nSproutValue) { + pindex->nChainSproutValue = *pindex->pprev->nChainSproutValue + *pindex->nSproutValue; + } else { + pindex->nChainSproutValue = boost::none; + } + } else { + pindex->nChainSproutValue = pindex->nSproutValue; + } { LOCK(cs_nBlockSequenceId); pindex->nSequenceId = nBlockSequenceId++; @@ -3522,12 +3540,19 @@ bool static LoadBlockIndexDB() if (pindex->pprev) { if (pindex->pprev->nChainTx) { pindex->nChainTx = pindex->pprev->nChainTx + pindex->nTx; + if (pindex->pprev->nChainSproutValue && pindex->nSproutValue) { + pindex->nChainSproutValue = *pindex->pprev->nChainSproutValue + *pindex->nSproutValue; + } else { + pindex->nChainSproutValue = boost::none; + } } else { pindex->nChainTx = 0; + pindex->nChainSproutValue = boost::none; mapBlocksUnlinked.insert(std::make_pair(pindex->pprev, pindex)); } } else { pindex->nChainTx = pindex->nTx; + pindex->nChainSproutValue = pindex->nSproutValue; } } if (pindex->IsValid(BLOCK_VALID_TRANSACTIONS) && (pindex->nChainTx || pindex->pprev == NULL)) diff --git a/src/txdb.cpp b/src/txdb.cpp index 004b0be2c..e1e29d9ac 100644 --- a/src/txdb.cpp +++ b/src/txdb.cpp @@ -310,6 +310,7 @@ bool CBlockTreeDB::LoadBlockIndexGuts() pindexNew->nSolution = diskindex.nSolution; pindexNew->nStatus = diskindex.nStatus; pindexNew->nTx = diskindex.nTx; + pindexNew->nSproutValue = diskindex.nSproutValue; if (!CheckProofOfWork(pindexNew->GetBlockHash(), pindexNew->nBits, Params().GetConsensus())) return error("LoadBlockIndex(): CheckProofOfWork failed: %s", pindexNew->ToString());