// Copyright (c) 2016 Jack Grigg // Copyright (c) 2016 The Zcash developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. // Implementation of the Equihash Proof-of-Work algorithm. // // Reference // ========= // Alex Biryukov and Dmitry Khovratovich // Equihash: Asymmetric Proof-of-Work Based on the Generalized Birthday Problem // NDSS ’16, 21-24 February 2016, San Diego, CA, USA // https://www.internetsociety.org/sites/default/files/blogs-media/equihash-asymmetric-proof-of-work-based-generalized-birthday-problem.pdf #include "crypto/equihash.h" #include "util.h" #include #include #include #include template int Equihash::InitialiseState(eh_HashState& base_state) { unsigned int n = N; unsigned int k = K; unsigned char personalization[crypto_generichash_blake2b_PERSONALBYTES] = {}; memcpy(personalization, "ZcashPOW", 8); memcpy(personalization+8, &n, 4); memcpy(personalization+12, &k, 4); return crypto_generichash_blake2b_init_salt_personal(&base_state, NULL, 0, // No key. N/8, NULL, // No salt. personalization); } // Big-endian so that array comparison is equivalent to integer comparison void EhIndexToArray(const eh_index i, unsigned char* array) { assert(sizeof(eh_index) == 4); array[0] = (i >> 24) & 0xFF; array[1] = (i >> 16) & 0xFF; array[2] = (i >> 8) & 0xFF; array[3] = i & 0xFF; } // Big-endian so that array comparison is equivalent to integer comparison eh_index ArrayToEhIndex(const unsigned char* array) { assert(sizeof(eh_index) == 4); eh_index ret {array[0]}; ret <<= 8; ret |= array[1]; ret <<= 8; ret |= array[2]; ret <<= 8; ret |= array[3]; return ret; } eh_trunc TruncateIndex(const eh_index i, const unsigned int ilen) { // Truncate to 8 bits assert(sizeof(eh_trunc) == 1); return (i >> (ilen - 8)) & 0xff; } eh_index UntruncateIndex(const eh_trunc t, const eh_index r, const unsigned int ilen) { eh_index i{t}; return (i << (ilen - 8)) | r; } template StepRow::StepRow(unsigned int n, const eh_HashState& base_state, eh_index i) { eh_HashState state; state = base_state; crypto_generichash_blake2b_update(&state, (unsigned char*) &i, sizeof(eh_index)); crypto_generichash_blake2b_final(&state, hash, n/8); } template template StepRow::StepRow(const StepRow& a) { assert(W <= WIDTH); std::copy(a.hash, a.hash+W, hash); } template FullStepRow::FullStepRow(unsigned int n, const eh_HashState& base_state, eh_index i) : StepRow {n, base_state, i} { EhIndexToArray(i, hash+(n/8)); } template template FullStepRow::FullStepRow(const FullStepRow& a, const FullStepRow& b, size_t len, size_t lenIndices, int trim) : StepRow {a} { assert(len+lenIndices <= W); assert(len-trim+(2*lenIndices) <= WIDTH); for (int i = trim; i < len; i++) hash[i-trim] = a.hash[i] ^ b.hash[i]; if (a.IndicesBefore(b, len, lenIndices)) { std::copy(a.hash+len, a.hash+len+lenIndices, hash+len-trim); std::copy(b.hash+len, b.hash+len+lenIndices, hash+len-trim+lenIndices); } else { std::copy(b.hash+len, b.hash+len+lenIndices, hash+len-trim); std::copy(a.hash+len, a.hash+len+lenIndices, hash+len-trim+lenIndices); } } template FullStepRow& FullStepRow::operator=(const FullStepRow& a) { std::copy(a.hash, a.hash+WIDTH, hash); return *this; } template bool StepRow::IsZero(size_t len) { // This doesn't need to be constant time. for (int i = 0; i < len; i++) { if (hash[i] != 0) return false; } return true; } template std::vector FullStepRow::GetIndices(size_t len, size_t lenIndices) const { std::vector ret; for (int i = 0; i < lenIndices; i += sizeof(eh_index)) { ret.push_back(ArrayToEhIndex(hash+len+i)); } return ret; } template bool HasCollision(StepRow& a, StepRow& b, int l) { // This doesn't need to be constant time. for (int j = 0; j < l; j++) { if (a.hash[j] != b.hash[j]) return false; } return true; } template TruncatedStepRow::TruncatedStepRow(unsigned int n, const eh_HashState& base_state, eh_index i, unsigned int ilen) : StepRow {n, base_state, i} { hash[n/8] = TruncateIndex(i, ilen); } template template TruncatedStepRow::TruncatedStepRow(const TruncatedStepRow& a, const TruncatedStepRow& b, size_t len, size_t lenIndices, int trim) : StepRow {a} { assert(len+lenIndices <= W); assert(len-trim+(2*lenIndices) <= WIDTH); for (int i = trim; i < len; i++) hash[i-trim] = a.hash[i] ^ b.hash[i]; if (a.IndicesBefore(b, len, lenIndices)) { std::copy(a.hash+len, a.hash+len+lenIndices, hash+len-trim); std::copy(b.hash+len, b.hash+len+lenIndices, hash+len-trim+lenIndices); } else { std::copy(b.hash+len, b.hash+len+lenIndices, hash+len-trim); std::copy(a.hash+len, a.hash+len+lenIndices, hash+len-trim+lenIndices); } } template TruncatedStepRow& TruncatedStepRow::operator=(const TruncatedStepRow& a) { std::copy(a.hash, a.hash+WIDTH, hash); return *this; } template eh_trunc* TruncatedStepRow::GetTruncatedIndices(size_t len, size_t lenIndices) const { eh_trunc* p = new eh_trunc[lenIndices]; std::copy(hash+len, hash+len+lenIndices, p); return p; } template std::set> Equihash::BasicSolve(const eh_HashState& base_state) { eh_index init_size { 1 << (CollisionBitLength + 1) }; // 1) Generate first list LogPrint("pow", "Generating first list\n"); size_t hashLen = N/8; size_t lenIndices = sizeof(eh_index); std::vector> X; X.reserve(init_size); for (eh_index i = 0; i < init_size; i++) { X.emplace_back(N, base_state, i); } // 3) Repeat step 2 until 2n/(k+1) bits remain for (int r = 1; r < K && X.size() > 0; r++) { LogPrint("pow", "Round %d:\n", r); // 2a) Sort the list LogPrint("pow", "- Sorting list\n"); std::sort(X.begin(), X.end(), CompareSR(hashLen)); LogPrint("pow", "- Finding collisions\n"); int i = 0; int posFree = 0; std::vector> Xc; while (i < X.size() - 1) { // 2b) Find next set of unordered pairs with collisions on the next n/(k+1) bits int j = 1; while (i+j < X.size() && HasCollision(X[i], X[i+j], CollisionByteLength)) { j++; } // 2c) Calculate tuples (X_i ^ X_j, (i, j)) for (int l = 0; l < j - 1; l++) { for (int m = l + 1; m < j; m++) { if (DistinctIndices(X[i+l], X[i+m], hashLen, lenIndices)) { Xc.emplace_back(X[i+l], X[i+m], hashLen, lenIndices, CollisionByteLength); } } } // 2d) Store tuples on the table in-place if possible while (posFree < i+j && Xc.size() > 0) { X[posFree++] = Xc.back(); Xc.pop_back(); } i += j; } // 2e) Handle edge case where final table entry has no collision while (posFree < X.size() && Xc.size() > 0) { X[posFree++] = Xc.back(); Xc.pop_back(); } if (Xc.size() > 0) { // 2f) Add overflow to end of table X.insert(X.end(), Xc.begin(), Xc.end()); } else if (posFree < X.size()) { // 2g) Remove empty space at the end X.erase(X.begin()+posFree, X.end()); X.shrink_to_fit(); } hashLen -= CollisionByteLength; lenIndices *= 2; } // k+1) Find a collision on last 2n(k+1) bits LogPrint("pow", "Final round:\n"); std::set> solns; if (X.size() > 1) { LogPrint("pow", "- Sorting list\n"); std::sort(X.begin(), X.end(), CompareSR(hashLen)); LogPrint("pow", "- Finding collisions\n"); for (int i = 0; i < X.size() - 1; i++) { FullStepRow res(X[i], X[i+1], hashLen, lenIndices, 0); if (res.IsZero(hashLen) && DistinctIndices(X[i], X[i+1], hashLen, lenIndices)) { solns.insert(res.GetIndices(hashLen, 2*lenIndices)); } } } else LogPrint("pow", "- List is empty\n"); return solns; } template void CollideBranches(std::vector>& X, const size_t hlen, const size_t lenIndices, const unsigned int clen, const unsigned int ilen, const eh_trunc lt, const eh_trunc rt) { int i = 0; int posFree = 0; std::vector> Xc; while (i < X.size() - 1) { // 2b) Find next set of unordered pairs with collisions on the next n/(k+1) bits int j = 1; while (i+j < X.size() && HasCollision(X[i], X[i+j], clen)) { j++; } // 2c) Calculate tuples (X_i ^ X_j, (i, j)) for (int l = 0; l < j - 1; l++) { for (int m = l + 1; m < j; m++) { if (DistinctIndices(X[i+l], X[i+m], hlen, lenIndices)) { if (IsValidBranch(X[i+l], hlen, ilen, lt) && IsValidBranch(X[i+m], hlen, ilen, rt)) { Xc.emplace_back(X[i+l], X[i+m], hlen, lenIndices, clen); } else if (IsValidBranch(X[i+m], hlen, ilen, lt) && IsValidBranch(X[i+l], hlen, ilen, rt)) { Xc.emplace_back(X[i+m], X[i+l], hlen, lenIndices, clen); } } } } // 2d) Store tuples on the table in-place if possible while (posFree < i+j && Xc.size() > 0) { X[posFree++] = Xc.back(); Xc.pop_back(); } i += j; } // 2e) Handle edge case where final table entry has no collision while (posFree < X.size() && Xc.size() > 0) { X[posFree++] = Xc.back(); Xc.pop_back(); } if (Xc.size() > 0) { // 2f) Add overflow to end of table X.insert(X.end(), Xc.begin(), Xc.end()); } else if (posFree < X.size()) { // 2g) Remove empty space at the end X.erase(X.begin()+posFree, X.end()); X.shrink_to_fit(); } } template std::set> Equihash::OptimisedSolve(const eh_HashState& base_state) { eh_index init_size { 1 << (CollisionBitLength + 1) }; // First run the algorithm with truncated indices eh_index soln_size { 1 << K }; // Each element of partialSolns is dynamically allocated in a call to // GetTruncatedIndices(), and freed at the end of this function. std::vector partialSolns; { // 1) Generate first list LogPrint("pow", "Generating first list\n"); size_t hashLen = N/8; size_t lenIndices = sizeof(eh_trunc); std::vector> Xt; Xt.reserve(init_size); for (eh_index i = 0; i < init_size; i++) { Xt.emplace_back(N, base_state, i, CollisionBitLength + 1); } // 3) Repeat step 2 until 2n/(k+1) bits remain for (int r = 1; r < K && Xt.size() > 0; r++) { LogPrint("pow", "Round %d:\n", r); // 2a) Sort the list LogPrint("pow", "- Sorting list\n"); std::sort(Xt.begin(), Xt.end(), CompareSR(hashLen)); LogPrint("pow", "- Finding collisions\n"); int i = 0; int posFree = 0; std::vector> Xc; while (i < Xt.size() - 1) { // 2b) Find next set of unordered pairs with collisions on the next n/(k+1) bits int j = 1; while (i+j < Xt.size() && HasCollision(Xt[i], Xt[i+j], CollisionByteLength)) { j++; } // 2c) Calculate tuples (X_i ^ X_j, (i, j)) for (int l = 0; l < j - 1; l++) { for (int m = l + 1; m < j; m++) { // We truncated, so don't check for distinct indices here Xc.emplace_back(Xt[i+l], Xt[i+m], hashLen, lenIndices, CollisionByteLength); } } // 2d) Store tuples on the table in-place if possible while (posFree < i+j && Xc.size() > 0) { Xt[posFree++] = Xc.back(); Xc.pop_back(); } i += j; } // 2e) Handle edge case where final table entry has no collision while (posFree < Xt.size() && Xc.size() > 0) { Xt[posFree++] = Xc.back(); Xc.pop_back(); } if (Xc.size() > 0) { // 2f) Add overflow to end of table Xt.insert(Xt.end(), Xc.begin(), Xc.end()); } else if (posFree < Xt.size()) { // 2g) Remove empty space at the end Xt.erase(Xt.begin()+posFree, Xt.end()); Xt.shrink_to_fit(); } hashLen -= CollisionByteLength; lenIndices *= 2; } // k+1) Find a collision on last 2n(k+1) bits LogPrint("pow", "Final round:\n"); if (Xt.size() > 1) { LogPrint("pow", "- Sorting list\n"); std::sort(Xt.begin(), Xt.end(), CompareSR(hashLen)); LogPrint("pow", "- Finding collisions\n"); for (int i = 0; i < Xt.size() - 1; i++) { TruncatedStepRow res(Xt[i], Xt[i+1], hashLen, lenIndices, 0); if (res.IsZero(hashLen)) { partialSolns.push_back(res.GetTruncatedIndices(hashLen, 2*lenIndices)); } } } else LogPrint("pow", "- List is empty\n"); } // Ensure Xt goes out of scope and is destroyed LogPrint("pow", "Found %d partial solutions\n", partialSolns.size()); // Now for each solution run the algorithm again to recreate the indices LogPrint("pow", "Culling solutions\n"); std::set> solns; eh_index recreate_size { UntruncateIndex(1, 0, CollisionBitLength + 1) }; int invalidCount = 0; for (eh_trunc* partialSoln : partialSolns) { size_t hashLen; size_t lenIndices; std::vector>>> X; X.reserve(K+1); // 3) Repeat steps 1 and 2 for each partial index for (eh_index i = 0; i < soln_size; i++) { // 1) Generate first list of possibilities std::vector> icv; icv.reserve(recreate_size); for (eh_index j = 0; j < recreate_size; j++) { eh_index newIndex { UntruncateIndex(partialSoln[i], j, CollisionBitLength + 1) }; icv.emplace_back(N, base_state, newIndex); } boost::optional>> ic = icv; // 2a) For each pair of lists: hashLen = N/8; lenIndices = sizeof(eh_index); size_t rti = i; for (size_t r = 0; r <= K; r++) { // 2b) Until we are at the top of a subtree: if (r < X.size()) { if (X[r]) { // 2c) Merge the lists ic->reserve(ic->size() + X[r]->size()); ic->insert(ic->end(), X[r]->begin(), X[r]->end()); std::sort(ic->begin(), ic->end(), CompareSR(hashLen)); size_t lti = rti-(1<size() == 0) goto invalidsolution; X[r] = boost::none; hashLen -= CollisionByteLength; lenIndices *= 2; rti = lti; } else { X[r] = *ic; break; } } else { X.push_back(ic); break; } } } // We are at the top of the tree assert(X.size() == K+1); for (FullStepRow row : *X[K]) { solns.insert(row.GetIndices(hashLen, lenIndices)); } goto deletesolution; invalidsolution: invalidCount++; deletesolution: delete[] partialSoln; } LogPrint("pow", "- Number of invalid solutions found: %d\n", invalidCount); return solns; } template bool Equihash::IsValidSolution(const eh_HashState& base_state, std::vector soln) { eh_index soln_size { 1u << K }; if (soln.size() != soln_size) { LogPrint("pow", "Invalid solution size: %d\n", soln.size()); return false; } std::vector> X; X.reserve(soln_size); for (eh_index i : soln) { X.emplace_back(N, base_state, i); } size_t hashLen = N/8; size_t lenIndices = sizeof(eh_index); while (X.size() > 1) { std::vector> Xc; for (int i = 0; i < X.size(); i += 2) { if (!HasCollision(X[i], X[i+1], CollisionByteLength)) { LogPrint("pow", "Invalid solution: invalid collision length between StepRows\n"); LogPrint("pow", "X[i] = %s\n", X[i].GetHex(hashLen)); LogPrint("pow", "X[i+1] = %s\n", X[i+1].GetHex(hashLen)); return false; } if (X[i+1].IndicesBefore(X[i], hashLen, lenIndices)) { return false; LogPrint("pow", "Invalid solution: Index tree incorrectly ordered\n"); } if (!DistinctIndices(X[i], X[i+1], hashLen, lenIndices)) { LogPrint("pow", "Invalid solution: duplicate indices\n"); return false; } Xc.emplace_back(X[i], X[i+1], hashLen, lenIndices, CollisionByteLength); } X = Xc; hashLen -= CollisionByteLength; lenIndices *= 2; } assert(X.size() == 1); return X[0].IsZero(hashLen); } // Explicit instantiations for Equihash<96,3> template int Equihash<96,3>::InitialiseState(eh_HashState& base_state); template std::set> Equihash<96,3>::BasicSolve(const eh_HashState& base_state); template std::set> Equihash<96,3>::OptimisedSolve(const eh_HashState& base_state); template bool Equihash<96,3>::IsValidSolution(const eh_HashState& base_state, std::vector soln); // Explicit instantiations for Equihash<96,5> template int Equihash<96,5>::InitialiseState(eh_HashState& base_state); template std::set> Equihash<96,5>::BasicSolve(const eh_HashState& base_state); template std::set> Equihash<96,5>::OptimisedSolve(const eh_HashState& base_state); template bool Equihash<96,5>::IsValidSolution(const eh_HashState& base_state, std::vector soln); // Explicit instantiations for Equihash<48,5> template int Equihash<48,5>::InitialiseState(eh_HashState& base_state); template std::set> Equihash<48,5>::BasicSolve(const eh_HashState& base_state); template std::set> Equihash<48,5>::OptimisedSolve(const eh_HashState& base_state); template bool Equihash<48,5>::IsValidSolution(const eh_HashState& base_state, std::vector soln);