Auto merge of #4448 - str4d:equihash-validator-rust-nu, r=ebfull

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.

Replaces zcash/zcash#2851.
This commit is contained in:
Homu 2020-04-15 02:03:06 +00:00
commit b5dda4ecc5
6 changed files with 62 additions and 14 deletions

View File

@ -1740,7 +1740,7 @@ bool WriteBlockToDisk(const CBlock& block, CDiskBlockPos& pos, const CMessageHea
return true; 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(); block.SetNull();
@ -1758,7 +1758,7 @@ bool ReadBlockFromDisk(CBlock& block, const CDiskBlockPos& pos, const Consensus:
} }
// Check the header // Check the header
if (!(CheckEquihashSolution(&block, consensusParams) && if (!(CheckEquihashSolution(&block, nHeight, consensusParams) &&
CheckProofOfWork(block.GetHash(), block.nBits, consensusParams))) CheckProofOfWork(block.GetHash(), block.nBits, consensusParams)))
return error("ReadBlockFromDisk: Errors in block header at %s", pos.ToString()); 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) 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; return false;
if (block.GetHash() != pindex->GetBlockHash()) if (block.GetHash() != pindex->GetBlockHash())
return error("ReadBlockFromDisk(CBlock&, CBlockIndex*): GetHash() doesn't match index for %s at %s", return error("ReadBlockFromDisk(CBlock&, CBlockIndex*): GetHash() doesn't match index for %s at %s",
@ -3855,13 +3855,19 @@ bool CheckBlockHeader(
const CChainParams& chainparams, const CChainParams& chainparams,
bool fCheckPOW) bool fCheckPOW)
{ {
auto consensusParams = chainparams.GetConsensus();
// Check block version // Check block version
if (block.nVersion < MIN_BLOCK_VERSION) if (block.nVersion < MIN_BLOCK_VERSION)
return state.DoS(100, error("CheckBlockHeader(): block version too low"), return state.DoS(100, error("CheckBlockHeader(): block version too low"),
REJECT_INVALID, "version-too-low"); REJECT_INVALID, "version-too-low");
// Check Equihash solution is valid // Check Equihash solution is valid. The main check is in ContextualCheckBlockHeader,
if (fCheckPOW && !CheckEquihashSolution(&block, chainparams.GetConsensus())) // 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"), return state.DoS(100, error("CheckBlockHeader(): Equihash solution invalid"),
REJECT_INVALID, "invalid-solution"); REJECT_INVALID, "invalid-solution");
@ -3938,7 +3944,8 @@ bool CheckBlock(const CBlock& block, CValidationState& state,
bool ContextualCheckBlockHeader( bool ContextualCheckBlockHeader(
const CBlockHeader& block, CValidationState& state, 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(); const Consensus::Params& consensusParams = chainParams.GetConsensus();
uint256 hash = block.GetHash(); uint256 hash = block.GetHash();
@ -3950,6 +3957,11 @@ bool ContextualCheckBlockHeader(
int nHeight = pindexPrev->nHeight+1; 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 // Check proof of work
if (block.nBits != GetNextWorkRequired(pindexPrev, &block, consensusParams)) { if (block.nBits != GetNextWorkRequired(pindexPrev, &block, consensusParams)) {
return state.DoS(100, error("%s: incorrect proof of work", __func__), 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(); auto verifier = libzcash::ProofVerifier::Disabled();
// NOTE: CheckBlockHeader is called by CheckBlock // NOTE: CheckBlockHeader is called by CheckBlock
if (!ContextualCheckBlockHeader(block, state, chainparams, pindexPrev)) if (!ContextualCheckBlockHeader(block, state, chainparams, pindexPrev, fCheckPOW))
return false; return false;
if (!CheckBlock(block, state, chainparams, verifier, fCheckPOW, fCheckMerkleRoot)) if (!CheckBlock(block, state, chainparams, verifier, fCheckPOW, fCheckMerkleRoot))
return false; return false;
@ -5031,7 +5043,7 @@ bool LoadExternalBlockFile(const CChainParams& chainparams, FILE* fileIn, CDiskB
std::pair<std::multimap<uint256, CDiskBlockPos>::iterator, std::multimap<uint256, CDiskBlockPos>::iterator> range = mapBlocksUnknownParent.equal_range(head); std::pair<std::multimap<uint256, CDiskBlockPos>::iterator, std::multimap<uint256, CDiskBlockPos>::iterator> range = mapBlocksUnknownParent.equal_range(head);
while (range.first != range.second) { while (range.first != range.second) {
std::multimap<uint256, CDiskBlockPos>::iterator it = range.first; std::multimap<uint256, CDiskBlockPos>::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(), LogPrintf("%s: Processing out of order child %s of %s\n", __func__, block.GetHash().ToString(),
head.ToString()); head.ToString());

View File

@ -436,7 +436,7 @@ bool GetTimestampIndex(unsigned int high, unsigned int low, bool fActiveOnly,
/** Functions for disk access for blocks */ /** Functions for disk access for blocks */
bool WriteBlockToDisk(const CBlock& block, CDiskBlockPos& pos, const CMessageHeader::MessageStartChars& messageStart); 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); bool ReadBlockFromDisk(CBlock& block, const CBlockIndex* pindex, const Consensus::Params& consensusParams);
/** Functions for validating blocks and updating the block tree */ /** 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 * By "context", we mean only the previous block headers, but not the UTXO
* set; UTXO-related validity checks are done in ConnectBlock(). */ * set; UTXO-related validity checks are done in ConnectBlock(). */
bool ContextualCheckBlockHeader(const CBlockHeader& block, CValidationState& state, 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, bool ContextualCheckBlock(const CBlock& block, CValidationState& state,
const CChainParams& chainparams, CBlockIndex *pindexPrev); const CChainParams& chainparams, CBlockIndex *pindexPrev);

View File

@ -13,6 +13,7 @@
#include "streams.h" #include "streams.h"
#include "uint256.h" #include "uint256.h"
#include <librustzcash.h>
#include "sodium.h" #include "sodium.h"
unsigned int GetNextWorkRequired(const CBlockIndex* pindexLast, const CBlockHeader *pblock, const Consensus::Params& params) unsigned int GetNextWorkRequired(const CBlockIndex* pindexLast, const CBlockHeader *pblock, const Consensus::Params& params)
@ -92,7 +93,7 @@ unsigned int CalculateNextWorkRequired(arith_uint256 bnAvg,
return bnNew.GetCompact(); 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 n = params.nEquihashN;
unsigned int k = params.nEquihashK; unsigned int k = params.nEquihashK;
@ -106,6 +107,17 @@ bool CheckEquihashSolution(const CBlockHeader *pblock, const Consensus::Params&
// I||V // I||V
CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
ss << I; 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; ss << pblock->nNonce;
// H(I||V||... // H(I||V||...

View File

@ -23,7 +23,7 @@ unsigned int CalculateNextWorkRequired(arith_uint256 bnAvg,
int nextHeight); int nextHeight);
/** Check whether the Equihash solution in a block header is valid */ /** 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 */ /** Check whether a block hash satisfies the proof-of-work requirement specified by nBits */
bool CheckProofOfWork(uint256 hash, unsigned int nBits, const Consensus::Params&); bool CheckProofOfWork(uint256 hash, unsigned int nBits, const Consensus::Params&);

View File

@ -15,6 +15,8 @@
#include "sodium.h" #include "sodium.h"
#include "librustzcash.h"
#include <sstream> #include <sstream>
#include <set> #include <set>
#include <vector> #include <vector>
@ -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<uint32_t> soln, bool expected) { void TestEquihashValidator(unsigned int n, unsigned int k, const std::string &I, const arith_uint256 &nonce, std::vector<uint32_t> soln, bool expected) {
size_t cBitLen { n/(k+1) }; size_t cBitLen { n/(k+1) };
auto minimal = GetMinimalFromIndices(soln, cBitLen);
// First test the C++ validator
crypto_generichash_blake2b_state state; crypto_generichash_blake2b_state state;
EhInitialiseState(n, k, state); EhInitialiseState(n, k, state);
uint256 V = ArithToUint256(nonce); uint256 V = ArithToUint256(nonce);
@ -97,7 +102,15 @@ void TestEquihashValidator(unsigned int n, unsigned int k, const std::string &I,
PrintSolution(strm, soln); PrintSolution(strm, soln);
BOOST_TEST_MESSAGE(strm.str()); BOOST_TEST_MESSAGE(strm.str());
bool isValid; 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); BOOST_CHECK(isValid == expected);
} }
@ -219,6 +232,11 @@ BOOST_AUTO_TEST_CASE(validator_allbitsmatter) {
bool isValid; bool isValid;
EhIsValidSolution(n, k, state, sol_char, isValid); EhIsValidSolution(n, k, state, sol_char, isValid);
BOOST_CHECK(isValid == true); 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. // Changing any single bit of the encoded solution should make it invalid.
for (size_t i = 0; i < sol_char.size() * 8; i++) { 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)); mutated.at(i/8) ^= (1 << (i % 8));
EhIsValidSolution(n, k, state, mutated, isValid); EhIsValidSolution(n, k, state, mutated, isValid);
BOOST_CHECK(isValid == false); 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()));
} }
} }

View File

@ -204,7 +204,7 @@ double benchmark_verify_equihash()
CBlockHeader genesis_header = genesis.GetBlockHeader(); CBlockHeader genesis_header = genesis.GetBlockHeader();
struct timeval tv_start; struct timeval tv_start;
timer_start(tv_start); timer_start(tv_start);
CheckEquihashSolution(&genesis_header, params.GetConsensus()); assert(CheckEquihashSolution(&genesis_header, 1, params.GetConsensus()));
return timer_stop(tv_start); return timer_stop(tv_start);
} }