diff --git a/src/Makefile.gtest.include b/src/Makefile.gtest.include index 7369f9431..ba07d2c43 100644 --- a/src/Makefile.gtest.include +++ b/src/Makefile.gtest.include @@ -6,6 +6,7 @@ zcash_gtest_SOURCES = \ gtest/main.cpp \ gtest/test_tautology.cpp \ gtest/test_checktransaction.cpp \ + gtest/test_equihash.cpp \ gtest/test_joinsplit.cpp \ gtest/test_noteencryption.cpp \ gtest/test_merkletree.cpp \ diff --git a/src/crypto/equihash.cpp b/src/crypto/equihash.cpp index e8a8fcb22..515ae3302 100644 --- a/src/crypto/equihash.cpp +++ b/src/crypto/equihash.cpp @@ -21,6 +21,8 @@ #include +EhSolverCancelledException solver_cancelled; + template int Equihash::InitialiseState(eh_HashState& base_state) { @@ -183,15 +185,15 @@ TruncatedStepRow& TruncatedStepRow::operator=(const TruncatedStepR } template -eh_trunc* TruncatedStepRow::GetTruncatedIndices(size_t len, size_t lenIndices) const +std::shared_ptr TruncatedStepRow::GetTruncatedIndices(size_t len, size_t lenIndices) const { - eh_trunc* p = new eh_trunc[lenIndices]; - std::copy(hash+len, hash+len+lenIndices, p); + std::shared_ptr p (new eh_trunc[lenIndices]); + std::copy(hash+len, hash+len+lenIndices, p.get()); return p; } template -std::set> Equihash::BasicSolve(const eh_HashState& base_state) +std::set> Equihash::BasicSolve(const eh_HashState& base_state, const std::function cancelled) { eh_index init_size { 1 << (CollisionBitLength + 1) }; @@ -203,6 +205,7 @@ std::set> Equihash::BasicSolve(const eh_HashState& ba X.reserve(init_size); for (eh_index i = 0; i < init_size; i++) { X.emplace_back(N, base_state, i); + if (cancelled(ListGeneration)) throw solver_cancelled; } // 3) Repeat step 2 until 2n/(k+1) bits remain @@ -211,6 +214,7 @@ std::set> Equihash::BasicSolve(const eh_HashState& ba // 2a) Sort the list LogPrint("pow", "- Sorting list\n"); std::sort(X.begin(), X.end(), CompareSR(CollisionByteLength)); + if (cancelled(ListSorting)) throw solver_cancelled; LogPrint("pow", "- Finding collisions\n"); int i = 0; @@ -240,6 +244,7 @@ std::set> Equihash::BasicSolve(const eh_HashState& ba } i += j; + if (cancelled(ListColliding)) throw solver_cancelled; } // 2e) Handle edge case where final table entry has no collision @@ -259,6 +264,7 @@ std::set> Equihash::BasicSolve(const eh_HashState& ba hashLen -= CollisionByteLength; lenIndices *= 2; + if (cancelled(RoundEnd)) throw solver_cancelled; } // k+1) Find a collision on last 2n(k+1) bits @@ -267,6 +273,7 @@ std::set> Equihash::BasicSolve(const eh_HashState& ba if (X.size() > 1) { LogPrint("pow", "- Sorting list\n"); std::sort(X.begin(), X.end(), CompareSR(hashLen)); + if (cancelled(FinalSorting)) throw solver_cancelled; LogPrint("pow", "- Finding collisions\n"); int i = 0; while (i < X.size() - 1) { @@ -286,6 +293,7 @@ std::set> Equihash::BasicSolve(const eh_HashState& ba } i += j; + if (cancelled(FinalColliding)) throw solver_cancelled; } } else LogPrint("pow", "- List is empty\n"); @@ -346,16 +354,17 @@ void CollideBranches(std::vector>& X, const size_t hlen, cons } template -std::set> Equihash::OptimisedSolve(const eh_HashState& base_state) +std::set> Equihash::OptimisedSolve(const eh_HashState& base_state, const std::function cancelled) { eh_index init_size { 1 << (CollisionBitLength + 1) }; + eh_index recreate_size { UntruncateIndex(1, 0, 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; + std::vector> partialSolns; + std::set> solns; + int invalidCount = 0; { // 1) Generate first list @@ -366,6 +375,7 @@ std::set> Equihash::OptimisedSolve(const eh_HashState Xt.reserve(init_size); for (eh_index i = 0; i < init_size; i++) { Xt.emplace_back(N, base_state, i, CollisionBitLength + 1); + if (cancelled(ListGeneration)) throw solver_cancelled; } // 3) Repeat step 2 until 2n/(k+1) bits remain @@ -374,6 +384,7 @@ std::set> Equihash::OptimisedSolve(const eh_HashState // 2a) Sort the list LogPrint("pow", "- Sorting list\n"); std::sort(Xt.begin(), Xt.end(), CompareSR(CollisionByteLength)); + if (cancelled(ListSorting)) throw solver_cancelled; LogPrint("pow", "- Finding collisions\n"); int i = 0; @@ -402,6 +413,7 @@ std::set> Equihash::OptimisedSolve(const eh_HashState } i += j; + if (cancelled(ListColliding)) throw solver_cancelled; } // 2e) Handle edge case where final table entry has no collision @@ -421,6 +433,7 @@ std::set> Equihash::OptimisedSolve(const eh_HashState hashLen -= CollisionByteLength; lenIndices *= 2; + if (cancelled(RoundEnd)) throw solver_cancelled; } // k+1) Find a collision on last 2n(k+1) bits @@ -428,6 +441,7 @@ std::set> Equihash::OptimisedSolve(const eh_HashState if (Xt.size() > 1) { LogPrint("pow", "- Sorting list\n"); std::sort(Xt.begin(), Xt.end(), CompareSR(hashLen)); + if (cancelled(FinalSorting)) throw solver_cancelled; LogPrint("pow", "- Finding collisions\n"); int i = 0; while (i < Xt.size() - 1) { @@ -445,6 +459,7 @@ std::set> Equihash::OptimisedSolve(const eh_HashState } i += j; + if (cancelled(FinalColliding)) throw solver_cancelled; } } else LogPrint("pow", "- List is empty\n"); @@ -455,10 +470,7 @@ std::set> Equihash::OptimisedSolve(const eh_HashState // 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) { + for (std::shared_ptr partialSoln : partialSolns) { size_t hashLen; size_t lenIndices; std::vector>>> X; @@ -470,8 +482,9 @@ std::set> Equihash::OptimisedSolve(const eh_HashState 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) }; + eh_index newIndex { UntruncateIndex(partialSoln.get()[i], j, CollisionBitLength + 1) }; icv.emplace_back(N, base_state, newIndex); + if (cancelled(PartialGeneration)) throw solver_cancelled; } boost::optional>> ic = icv; @@ -487,11 +500,12 @@ std::set> Equihash::OptimisedSolve(const eh_HashState 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)); + if (cancelled(PartialSorting)) throw solver_cancelled; size_t lti = rti-(1<size() == 0) @@ -509,7 +523,9 @@ std::set> Equihash::OptimisedSolve(const eh_HashState X.push_back(ic); break; } + if (cancelled(PartialSubtreeEnd)) throw solver_cancelled; } + if (cancelled(PartialIndexEnd)) throw solver_cancelled; } // We are at the top of the tree @@ -517,13 +533,11 @@ std::set> Equihash::OptimisedSolve(const eh_HashState for (FullStepRow row : *X[K]) { solns.insert(row.GetIndices(hashLen, lenIndices)); } - goto deletesolution; + if (cancelled(PartialEnd)) throw solver_cancelled; + continue; invalidsolution: invalidCount++; - -deletesolution: - delete[] partialSoln; } LogPrint("pow", "- Number of invalid solutions found: %d\n", invalidCount); @@ -577,18 +591,18 @@ bool Equihash::IsValidSolution(const eh_HashState& base_state, std::vector< // 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 std::set> Equihash<96,3>::BasicSolve(const eh_HashState& base_state, const std::function cancelled); +template std::set> Equihash<96,3>::OptimisedSolve(const eh_HashState& base_state, const std::function cancelled); 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 std::set> Equihash<96,5>::BasicSolve(const eh_HashState& base_state, const std::function cancelled); +template std::set> Equihash<96,5>::OptimisedSolve(const eh_HashState& base_state, const std::function cancelled); 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 std::set> Equihash<48,5>::BasicSolve(const eh_HashState& base_state, const std::function cancelled); +template std::set> Equihash<48,5>::OptimisedSolve(const eh_HashState& base_state, const std::function cancelled); template bool Equihash<48,5>::IsValidSolution(const eh_HashState& base_state, std::vector soln); diff --git a/src/crypto/equihash.h b/src/crypto/equihash.h index 49be6dfad..4ba0e1c00 100644 --- a/src/crypto/equihash.h +++ b/src/crypto/equihash.h @@ -12,6 +12,9 @@ #include "sodium.h" #include +#include +#include +#include #include #include @@ -105,7 +108,29 @@ public: TruncatedStepRow& operator=(const TruncatedStepRow& a); inline bool IndicesBefore(const TruncatedStepRow& a, size_t len, size_t lenIndices) const { return memcmp(hash+len, a.hash+len, lenIndices) < 0; } - eh_trunc* GetTruncatedIndices(size_t len, size_t lenIndices) const; + std::shared_ptr GetTruncatedIndices(size_t len, size_t lenIndices) const; +}; + +enum EhSolverCancelCheck +{ + ListGeneration, + ListSorting, + ListColliding, + RoundEnd, + FinalSorting, + FinalColliding, + PartialGeneration, + PartialSorting, + PartialSubtreeEnd, + PartialIndexEnd, + PartialEnd +}; + +class EhSolverCancelledException : public std::exception +{ + virtual const char* what() const throw() { + return "Equihash solver was cancelled"; + } }; inline constexpr const size_t max(const size_t A, const size_t B) { return A > B ? A : B; } @@ -130,8 +155,8 @@ public: Equihash() { } int InitialiseState(eh_HashState& base_state); - std::set> BasicSolve(const eh_HashState& base_state); - std::set> OptimisedSolve(const eh_HashState& base_state); + std::set> BasicSolve(const eh_HashState& base_state, const std::function cancelled); + std::set> OptimisedSolve(const eh_HashState& base_state, const std::function cancelled); bool IsValidSolution(const eh_HashState& base_state, std::vector soln); }; @@ -152,27 +177,31 @@ static Equihash<48,5> Eh48_5; throw std::invalid_argument("Unsupported Equihash parameters"); \ } -#define EhBasicSolve(n, k, base_state, solns) \ - if (n == 96 && k == 3) { \ - solns = Eh96_3.BasicSolve(base_state); \ - } else if (n == 96 && k == 5) { \ - solns = Eh96_5.BasicSolve(base_state); \ - } else if (n == 48 && k == 5) { \ - solns = Eh48_5.BasicSolve(base_state); \ - } else { \ +#define EhBasicSolve(n, k, base_state, solns, cancelled) \ + if (n == 96 && k == 3) { \ + solns = Eh96_3.BasicSolve(base_state, cancelled); \ + } else if (n == 96 && k == 5) { \ + solns = Eh96_5.BasicSolve(base_state, cancelled); \ + } else if (n == 48 && k == 5) { \ + solns = Eh48_5.BasicSolve(base_state, cancelled); \ + } else { \ throw std::invalid_argument("Unsupported Equihash parameters"); \ } +#define EhBasicSolveUncancellable(n, k, base_state, solns) \ + EhBasicSolve(n, k, base_state, solns, [](EhSolverCancelCheck pos) { return false; }) -#define EhOptimisedSolve(n, k, base_state, solns) \ - if (n == 96 && k == 3) { \ - solns = Eh96_3.OptimisedSolve(base_state); \ - } else if (n == 96 && k == 5) { \ - solns = Eh96_5.OptimisedSolve(base_state); \ - } else if (n == 48 && k == 5) { \ - solns = Eh48_5.OptimisedSolve(base_state); \ - } else { \ +#define EhOptimisedSolve(n, k, base_state, solns, cancelled) \ + if (n == 96 && k == 3) { \ + solns = Eh96_3.OptimisedSolve(base_state, cancelled); \ + } else if (n == 96 && k == 5) { \ + solns = Eh96_5.OptimisedSolve(base_state, cancelled); \ + } else if (n == 48 && k == 5) { \ + solns = Eh48_5.OptimisedSolve(base_state, cancelled); \ + } else { \ throw std::invalid_argument("Unsupported Equihash parameters"); \ } +#define EhOptimisedSolveUncancellable(n, k, base_state, solns) \ + EhOptimisedSolve(n, k, base_state, solns, [](EhSolverCancelCheck pos) { return false; }) #define EhIsValidSolution(n, k, base_state, soln, ret) \ if (n == 96 && k == 3) { \ diff --git a/src/gtest/test_equihash.cpp b/src/gtest/test_equihash.cpp new file mode 100644 index 000000000..7e281990a --- /dev/null +++ b/src/gtest/test_equihash.cpp @@ -0,0 +1,162 @@ +#include +#include + +#include "crypto/equihash.h" + +TEST(equihash_tests, check_basic_solver_cancelled) { + Equihash<48,5> Eh48_5; + crypto_generichash_blake2b_state state; + Eh48_5.InitialiseState(state); + std::set> solns; + + { + ASSERT_NO_THROW(Eh48_5.BasicSolve(state, [](EhSolverCancelCheck pos) { + return false; + })); + } + + { + ASSERT_THROW(Eh48_5.BasicSolve(state, [](EhSolverCancelCheck pos) { + return pos == ListGeneration; + }), EhSolverCancelledException); + } + + { + ASSERT_THROW(Eh48_5.BasicSolve(state, [](EhSolverCancelCheck pos) { + return pos == ListSorting; + }), EhSolverCancelledException); + } + + { + ASSERT_THROW(Eh48_5.BasicSolve(state, [](EhSolverCancelCheck pos) { + return pos == ListColliding; + }), EhSolverCancelledException); + } + + { + ASSERT_THROW(Eh48_5.BasicSolve(state, [](EhSolverCancelCheck pos) { + return pos == RoundEnd; + }), EhSolverCancelledException); + } + + { + ASSERT_THROW(Eh48_5.BasicSolve(state, [](EhSolverCancelCheck pos) { + return pos == FinalSorting; + }), EhSolverCancelledException); + } + + { + ASSERT_THROW(Eh48_5.BasicSolve(state, [](EhSolverCancelCheck pos) { + return pos == FinalColliding; + }), EhSolverCancelledException); + } + + { + ASSERT_NO_THROW(Eh48_5.BasicSolve(state, [](EhSolverCancelCheck pos) { + return pos == PartialGeneration; + })); + } + + { + ASSERT_NO_THROW(Eh48_5.BasicSolve(state, [](EhSolverCancelCheck pos) { + return pos == PartialSorting; + })); + } + + { + ASSERT_NO_THROW(Eh48_5.BasicSolve(state, [](EhSolverCancelCheck pos) { + return pos == PartialSubtreeEnd; + })); + } + + { + ASSERT_NO_THROW(Eh48_5.BasicSolve(state, [](EhSolverCancelCheck pos) { + return pos == PartialIndexEnd; + })); + } + + { + ASSERT_NO_THROW(Eh48_5.BasicSolve(state, [](EhSolverCancelCheck pos) { + return pos == PartialEnd; + })); + } +} + +TEST(equihash_tests, check_optimised_solver_cancelled) { + Equihash<48,5> Eh48_5; + crypto_generichash_blake2b_state state; + Eh48_5.InitialiseState(state); + std::set> solns; + + { + ASSERT_NO_THROW(Eh48_5.OptimisedSolve(state, [](EhSolverCancelCheck pos) { + return false; + })); + } + + { + ASSERT_THROW(Eh48_5.OptimisedSolve(state, [](EhSolverCancelCheck pos) { + return pos == ListGeneration; + }), EhSolverCancelledException); + } + + { + ASSERT_THROW(Eh48_5.OptimisedSolve(state, [](EhSolverCancelCheck pos) { + return pos == ListSorting; + }), EhSolverCancelledException); + } + + { + ASSERT_THROW(Eh48_5.OptimisedSolve(state, [](EhSolverCancelCheck pos) { + return pos == ListColliding; + }), EhSolverCancelledException); + } + + { + ASSERT_THROW(Eh48_5.OptimisedSolve(state, [](EhSolverCancelCheck pos) { + return pos == RoundEnd; + }), EhSolverCancelledException); + } + + { + ASSERT_THROW(Eh48_5.OptimisedSolve(state, [](EhSolverCancelCheck pos) { + return pos == FinalSorting; + }), EhSolverCancelledException); + } + + { + ASSERT_THROW(Eh48_5.OptimisedSolve(state, [](EhSolverCancelCheck pos) { + return pos == FinalColliding; + }), EhSolverCancelledException); + } + + { + ASSERT_THROW(Eh48_5.OptimisedSolve(state, [](EhSolverCancelCheck pos) { + return pos == PartialGeneration; + }), EhSolverCancelledException); + } + + { + ASSERT_THROW(Eh48_5.OptimisedSolve(state, [](EhSolverCancelCheck pos) { + return pos == PartialSorting; + }), EhSolverCancelledException); + } + + { + ASSERT_THROW(Eh48_5.OptimisedSolve(state, [](EhSolverCancelCheck pos) { + return pos == PartialSubtreeEnd; + }), EhSolverCancelledException); + } + + { + ASSERT_THROW(Eh48_5.OptimisedSolve(state, [](EhSolverCancelCheck pos) { + return pos == PartialIndexEnd; + }), EhSolverCancelledException); + } + + { + ASSERT_THROW(Eh48_5.OptimisedSolve(state, [](EhSolverCancelCheck pos) { + return pos == PartialEnd; + }), EhSolverCancelledException); + } +} diff --git a/src/miner.cpp b/src/miner.cpp index 95ec6df85..d110bb425 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -21,12 +21,14 @@ #ifdef ENABLE_WALLET #include "crypto/equihash.h" #include "wallet/wallet.h" +#include #endif #include "sodium.h" #include #include +#include using namespace std; @@ -454,6 +456,15 @@ void static BitcoinMiner(CWallet *pwallet) unsigned int n = chainparams.EquihashN(); unsigned int k = chainparams.EquihashK(); + std::mutex m_cs; + bool cancelSolver = false; + boost::signals2::connection c = uiInterface.NotifyBlockTip.connect( + [&m_cs, &cancelSolver](const uint256& hashNewTip) mutable { + std::lock_guard lock{m_cs}; + cancelSolver = true; + } + ); + try { while (true) { if (chainparams.MiningRequiresPeers()) { @@ -519,7 +530,17 @@ void static BitcoinMiner(CWallet *pwallet) LogPrint("pow", "Running Equihash solver with nNonce = %s\n", pblock->nNonce.ToString()); std::set> solns; - EhOptimisedSolve(n, k, curr_state, solns); + try { + std::function cancelled = [&m_cs, &cancelSolver](EhSolverCancelCheck pos) { + std::lock_guard lock{m_cs}; + return cancelSolver; + }; + EhOptimisedSolve(n, k, curr_state, solns, cancelled); + } catch (EhSolverCancelledException&) { + LogPrint("pow", "Equihash solver cancelled\n"); + std::lock_guard lock{m_cs}; + cancelSolver = false; + } LogPrint("pow", "Solutions: %d\n", solns.size()); // Write the solution to the hash and compute the result. @@ -534,7 +555,11 @@ void static BitcoinMiner(CWallet *pwallet) SetThreadPriority(THREAD_PRIORITY_NORMAL); LogPrintf("ZcashMiner:\n"); LogPrintf("proof-of-work found \n hash: %s \ntarget: %s\n", pblock->GetHash().GetHex(), hashTarget.GetHex()); - ProcessBlockFound(pblock, *pwallet, reservekey); + if (ProcessBlockFound(pblock, *pwallet, reservekey)) { + // Ignore chain updates caused by us + std::lock_guard lock{m_cs}; + cancelSolver = false; + } SetThreadPriority(THREAD_PRIORITY_LOWEST); // In regression test mode, stop mining after a block is found. @@ -577,6 +602,7 @@ void static BitcoinMiner(CWallet *pwallet) LogPrintf("ZcashMiner runtime error: %s\n", e.what()); return; } + c.disconnect(); } void GenerateBitcoins(bool fGenerate, CWallet* pwallet, int nThreads) diff --git a/src/rpcmining.cpp b/src/rpcmining.cpp index 82bbe0c12..95b32df96 100644 --- a/src/rpcmining.cpp +++ b/src/rpcmining.cpp @@ -189,7 +189,7 @@ Value generate(const Array& params, bool fHelp) // (x_1, x_2, ...) = A(I, V, n, k) std::set> solns; - EhBasicSolve(n, k, curr_state, solns); + EhBasicSolveUncancellable(n, k, curr_state, solns); for (auto soln : solns) { bool isValid; diff --git a/src/test/equihash_tests.cpp b/src/test/equihash_tests.cpp index dbe757a7a..cf4d98c31 100644 --- a/src/test/equihash_tests.cpp +++ b/src/test/equihash_tests.cpp @@ -50,7 +50,7 @@ void TestEquihashSolvers(unsigned int n, unsigned int k, const std::string &I, c // First test the basic solver std::set> ret; - EhBasicSolve(n, k, state, ret); + EhBasicSolveUncancellable(n, k, state, ret); BOOST_TEST_MESSAGE("[Basic] Number of solutions: " << ret.size()); std::stringstream strm; PrintSolutions(strm, ret); @@ -59,7 +59,7 @@ void TestEquihashSolvers(unsigned int n, unsigned int k, const std::string &I, c // The optimised solver should have the exact same result std::set> retOpt; - EhOptimisedSolve(n, k, state, retOpt); + EhOptimisedSolveUncancellable(n, k, state, retOpt); BOOST_TEST_MESSAGE("[Optimised] Number of solutions: " << retOpt.size()); strm.str(""); PrintSolutions(strm, retOpt); diff --git a/src/test/miner_tests.cpp b/src/test/miner_tests.cpp index b0a0bead4..adc606aa7 100644 --- a/src/test/miner_tests.cpp +++ b/src/test/miner_tests.cpp @@ -206,7 +206,7 @@ BOOST_AUTO_TEST_CASE(CreateNewBlock_validity) // (x_1, x_2, ...) = A(I, V, n, k) std::set> solns; - EhOptimisedSolve(n, k, curr_state, solns); + EhOptimisedSolveUncancellable(n, k, curr_state, solns); bool ret; for (auto soln : solns) { diff --git a/src/zcbenchmarks.cpp b/src/zcbenchmarks.cpp index abe631f8b..5e90c55b5 100644 --- a/src/zcbenchmarks.cpp +++ b/src/zcbenchmarks.cpp @@ -118,7 +118,7 @@ double benchmark_solve_equihash() timer_start(); std::set> solns; - EhOptimisedSolve(n, k, eh_state, solns); + EhOptimisedSolveUncancellable(n, k, eh_state, solns); return timer_stop(); }