From f4fe77ad1ecb67a6934c3a7de18225944c1cccd2 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Wed, 15 Apr 2020 11:31:10 +1200 Subject: [PATCH 1/3] test: Run Equihash test vectors on both C++ and Rust validators --- src/test/equihash_tests.cpp | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/test/equihash_tests.cpp b/src/test/equihash_tests.cpp index e573f8e29..8e202fc68 100644 --- a/src/test/equihash_tests.cpp +++ b/src/test/equihash_tests.cpp @@ -15,6 +15,8 @@ #include "sodium.h" +#include "librustzcash.h" + #include #include #include @@ -87,6 +89,9 @@ void TestEquihashSolvers(unsigned int n, unsigned int k, const std::string &I, c void TestEquihashValidator(unsigned int n, unsigned int k, const std::string &I, const arith_uint256 &nonce, std::vector soln, bool expected) { size_t cBitLen { n/(k+1) }; + auto minimal = GetMinimalFromIndices(soln, cBitLen); + + // First test the C++ validator crypto_generichash_blake2b_state state; EhInitialiseState(n, k, state); uint256 V = ArithToUint256(nonce); @@ -97,7 +102,15 @@ void TestEquihashValidator(unsigned int n, unsigned int k, const std::string &I, PrintSolution(strm, soln); BOOST_TEST_MESSAGE(strm.str()); bool isValid; - EhIsValidSolution(n, k, state, GetMinimalFromIndices(soln, cBitLen), isValid); + EhIsValidSolution(n, k, state, minimal, isValid); + BOOST_CHECK(isValid == expected); + + // The Rust validator should have the exact same result + isValid = librustzcash_eh_isvalid( + n, k, + (unsigned char*)&I[0], I.size(), + V.begin(), V.size(), + minimal.data(), minimal.size()); BOOST_CHECK(isValid == expected); } @@ -219,6 +232,11 @@ BOOST_AUTO_TEST_CASE(validator_allbitsmatter) { bool isValid; EhIsValidSolution(n, k, state, sol_char, isValid); BOOST_CHECK(isValid == true); + BOOST_CHECK(librustzcash_eh_isvalid( + n, k, + (unsigned char*)&I[0], I.size(), + V.begin(), V.size(), + sol_char.data(), sol_char.size())); // Changing any single bit of the encoded solution should make it invalid. for (size_t i = 0; i < sol_char.size() * 8; i++) { @@ -226,6 +244,11 @@ BOOST_AUTO_TEST_CASE(validator_allbitsmatter) { mutated.at(i/8) ^= (1 << (i % 8)); EhIsValidSolution(n, k, state, mutated, isValid); BOOST_CHECK(isValid == false); + BOOST_CHECK(!librustzcash_eh_isvalid( + n, k, + (unsigned char*)&I[0], I.size(), + V.begin(), V.size(), + mutated.data(), mutated.size())); } } From 49f9584613e2a5506508281b16427fa08c4ad11d Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Wed, 15 Apr 2020 11:51:21 +1200 Subject: [PATCH 2/3] Pass the block height through to CheckEquihashSolution() This requires moving CheckEquihashSolution() to ContextualCheckBlockHeader() for all but the genesis block, which has no effect on consensus; it just means that an invalid Equihash solution is rejected slightly later in the block validation process. --- src/main.cpp | 28 ++++++++++++++++++++-------- src/main.h | 5 +++-- src/pow.cpp | 2 +- src/pow.h | 2 +- src/zcbenchmarks.cpp | 2 +- 5 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index b12f3a037..c593fbd93 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1740,7 +1740,7 @@ bool WriteBlockToDisk(const CBlock& block, CDiskBlockPos& pos, const CMessageHea return true; } -bool ReadBlockFromDisk(CBlock& block, const CDiskBlockPos& pos, const Consensus::Params& consensusParams) +bool ReadBlockFromDisk(CBlock& block, const CDiskBlockPos& pos, int nHeight, const Consensus::Params& consensusParams) { block.SetNull(); @@ -1758,7 +1758,7 @@ bool ReadBlockFromDisk(CBlock& block, const CDiskBlockPos& pos, const Consensus: } // Check the header - if (!(CheckEquihashSolution(&block, consensusParams) && + if (!(CheckEquihashSolution(&block, nHeight, consensusParams) && CheckProofOfWork(block.GetHash(), block.nBits, consensusParams))) return error("ReadBlockFromDisk: Errors in block header at %s", pos.ToString()); @@ -1767,7 +1767,7 @@ bool ReadBlockFromDisk(CBlock& block, const CDiskBlockPos& pos, const Consensus: bool ReadBlockFromDisk(CBlock& block, const CBlockIndex* pindex, const Consensus::Params& consensusParams) { - if (!ReadBlockFromDisk(block, pindex->GetBlockPos(), consensusParams)) + if (!ReadBlockFromDisk(block, pindex->GetBlockPos(), pindex->nHeight, consensusParams)) return false; if (block.GetHash() != pindex->GetBlockHash()) return error("ReadBlockFromDisk(CBlock&, CBlockIndex*): GetHash() doesn't match index for %s at %s", @@ -3855,13 +3855,19 @@ bool CheckBlockHeader( const CChainParams& chainparams, bool fCheckPOW) { + auto consensusParams = chainparams.GetConsensus(); + // Check block version if (block.nVersion < MIN_BLOCK_VERSION) return state.DoS(100, error("CheckBlockHeader(): block version too low"), REJECT_INVALID, "version-too-low"); - // Check Equihash solution is valid - if (fCheckPOW && !CheckEquihashSolution(&block, chainparams.GetConsensus())) + // Check Equihash solution is valid. The main check is in ContextualCheckBlockHeader, + // because we currently need to know the block height. That function skips the genesis + // block because it has no previous block, so we check it specifically here. + if (fCheckPOW && + block.GetHash() == consensusParams.hashGenesisBlock && + !CheckEquihashSolution(&block, 0, consensusParams)) return state.DoS(100, error("CheckBlockHeader(): Equihash solution invalid"), REJECT_INVALID, "invalid-solution"); @@ -3938,7 +3944,8 @@ bool CheckBlock(const CBlock& block, CValidationState& state, bool ContextualCheckBlockHeader( const CBlockHeader& block, CValidationState& state, - const CChainParams& chainParams, CBlockIndex * const pindexPrev) + const CChainParams& chainParams, CBlockIndex * const pindexPrev, + bool fCheckPOW) { const Consensus::Params& consensusParams = chainParams.GetConsensus(); uint256 hash = block.GetHash(); @@ -3950,6 +3957,11 @@ bool ContextualCheckBlockHeader( int nHeight = pindexPrev->nHeight+1; + // Check Equihash solution is valid + if (fCheckPOW && !CheckEquihashSolution(&block, nHeight, consensusParams)) + return state.DoS(100, error("CheckBlockHeader(): Equihash solution invalid"), + REJECT_INVALID, "invalid-solution"); + // Check proof of work if (block.nBits != GetNextWorkRequired(pindexPrev, &block, consensusParams)) { return state.DoS(100, error("%s: incorrect proof of work", __func__), @@ -4239,7 +4251,7 @@ bool TestBlockValidity(CValidationState& state, const CChainParams& chainparams, auto verifier = libzcash::ProofVerifier::Disabled(); // NOTE: CheckBlockHeader is called by CheckBlock - if (!ContextualCheckBlockHeader(block, state, chainparams, pindexPrev)) + if (!ContextualCheckBlockHeader(block, state, chainparams, pindexPrev, fCheckPOW)) return false; if (!CheckBlock(block, state, chainparams, verifier, fCheckPOW, fCheckMerkleRoot)) return false; @@ -5031,7 +5043,7 @@ bool LoadExternalBlockFile(const CChainParams& chainparams, FILE* fileIn, CDiskB std::pair::iterator, std::multimap::iterator> range = mapBlocksUnknownParent.equal_range(head); while (range.first != range.second) { std::multimap::iterator it = range.first; - if (ReadBlockFromDisk(block, it->second, chainparams.GetConsensus())) + if (ReadBlockFromDisk(block, it->second, mapBlockIndex[head]->nHeight, chainparams.GetConsensus())) { LogPrintf("%s: Processing out of order child %s of %s\n", __func__, block.GetHash().ToString(), head.ToString()); diff --git a/src/main.h b/src/main.h index 421f74ffb..193d06c56 100644 --- a/src/main.h +++ b/src/main.h @@ -436,7 +436,7 @@ bool GetTimestampIndex(unsigned int high, unsigned int low, bool fActiveOnly, /** Functions for disk access for blocks */ bool WriteBlockToDisk(const CBlock& block, CDiskBlockPos& pos, const CMessageHeader::MessageStartChars& messageStart); -bool ReadBlockFromDisk(CBlock& block, const CDiskBlockPos& pos, const Consensus::Params& consensusParams); +bool ReadBlockFromDisk(CBlock& block, const CDiskBlockPos& pos, int nHeight, const Consensus::Params& consensusParams); bool ReadBlockFromDisk(CBlock& block, const CBlockIndex* pindex, const Consensus::Params& consensusParams); /** Functions for validating blocks and updating the block tree */ @@ -454,7 +454,8 @@ bool CheckBlock(const CBlock& block, CValidationState& state, * By "context", we mean only the previous block headers, but not the UTXO * set; UTXO-related validity checks are done in ConnectBlock(). */ bool ContextualCheckBlockHeader(const CBlockHeader& block, CValidationState& state, - const CChainParams& chainparams, CBlockIndex *pindexPrev); + const CChainParams& chainparams, CBlockIndex *pindexPrev, + bool fCheckPOW = true); bool ContextualCheckBlock(const CBlock& block, CValidationState& state, const CChainParams& chainparams, CBlockIndex *pindexPrev); diff --git a/src/pow.cpp b/src/pow.cpp index 7e95df8e0..6c128a04f 100644 --- a/src/pow.cpp +++ b/src/pow.cpp @@ -92,7 +92,7 @@ unsigned int CalculateNextWorkRequired(arith_uint256 bnAvg, return bnNew.GetCompact(); } -bool CheckEquihashSolution(const CBlockHeader *pblock, const Consensus::Params& params) +bool CheckEquihashSolution(const CBlockHeader *pblock, int nHeight, const Consensus::Params& params) { unsigned int n = params.nEquihashN; unsigned int k = params.nEquihashK; diff --git a/src/pow.h b/src/pow.h index eb05c6e34..4a6f86b48 100644 --- a/src/pow.h +++ b/src/pow.h @@ -23,7 +23,7 @@ unsigned int CalculateNextWorkRequired(arith_uint256 bnAvg, int nextHeight); /** Check whether the Equihash solution in a block header is valid */ -bool CheckEquihashSolution(const CBlockHeader *pblock, const Consensus::Params&); +bool CheckEquihashSolution(const CBlockHeader *pblock, int nHeight, const Consensus::Params&); /** Check whether a block hash satisfies the proof-of-work requirement specified by nBits */ bool CheckProofOfWork(uint256 hash, unsigned int nBits, const Consensus::Params&); diff --git a/src/zcbenchmarks.cpp b/src/zcbenchmarks.cpp index 5db123594..cd728eceb 100644 --- a/src/zcbenchmarks.cpp +++ b/src/zcbenchmarks.cpp @@ -204,7 +204,7 @@ double benchmark_verify_equihash() CBlockHeader genesis_header = genesis.GetBlockHeader(); struct timeval tv_start; timer_start(tv_start); - CheckEquihashSolution(&genesis_header, params.GetConsensus()); + assert(CheckEquihashSolution(&genesis_header, 1, params.GetConsensus())); return timer_stop(tv_start); } From 655f0c9802e91eaf6cfbb9f6d8587bb88ec94fac Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Wed, 15 Apr 2020 11:53:28 +1200 Subject: [PATCH 3/3] consensus: From Heartwood activation, use Rust Equihash validator The C++ and Rust Equihash validators are intended to have an identical set of valid Equihash solutions, so this should merely be an implementation detail. However, deploying the Rust validator at the same time as a network upgrade reduces the risk of an unintentional consensus divergence due to undocumented behaviour in either implementation. Once Heartwood has activated on mainnet, we can verify that all pre-Heartwood blocks satisfy the Rust validator, and then remove the C++ validator and make Equihash-checking non-contextual again. --- src/pow.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/pow.cpp b/src/pow.cpp index 6c128a04f..0477a59cb 100644 --- a/src/pow.cpp +++ b/src/pow.cpp @@ -13,6 +13,7 @@ #include "streams.h" #include "uint256.h" +#include #include "sodium.h" unsigned int GetNextWorkRequired(const CBlockIndex* pindexLast, const CBlockHeader *pblock, const Consensus::Params& params) @@ -106,6 +107,17 @@ bool CheckEquihashSolution(const CBlockHeader *pblock, int nHeight, const Consen // I||V CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); ss << I; + + // From Heartwood activation, check with the Rust validator + if (params.NetworkUpgradeActive(nHeight, Consensus::UPGRADE_HEARTWOOD)) { + return librustzcash_eh_isvalid( + n, k, + (unsigned char*)&ss[0], ss.size(), + pblock->nNonce.begin(), pblock->nNonce.size(), + pblock->nSolution.data(), pblock->nSolution.size()); + } + + // Before Heartwood activation, check with the C++ validator ss << pblock->nNonce; // H(I||V||...