From f0feb36ce380eca7792d7d58cea57f689f8b07c8 Mon Sep 17 00:00:00 2001 From: PastaPastaPasta <6443210+PastaPastaPasta@users.noreply.github.com> Date: Mon, 11 Dec 2023 15:43:57 -0600 Subject: [PATCH 1/8] Merge pull request #5759 from UdjinM6/bp26532 backport: partial merge bitcoin#26532: wallet: bugfix, invalid crypted key "checksum_valid" set --- src/wallet/walletdb.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp index 9ea4bba0cc..71e26c5199 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -388,7 +388,7 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue, if (!ssValue.eof()) { uint256 checksum; ssValue >> checksum; - if ((checksum_valid = Hash(vchPrivKey) != checksum)) { + if (!(checksum_valid = Hash(vchPrivKey) == checksum)) { strErr = "Error reading wallet database: Crypted key corrupt"; return false; } From ee4c27d0b99d0d2c5fd50e89aa8fb69414a173a8 Mon Sep 17 00:00:00 2001 From: UdjinM6 Date: Wed, 20 Dec 2023 18:54:00 +0300 Subject: [PATCH 2/8] fix: Improve quorum caching (again) (#5761) ## Issue being fixed or feature implemented 1. `scanQuorumsCache` is a special one and we use it incorrectly. 2. Platform doesn't really use anything that calls `ScanQuorums()` directly, they specify the exact quorum hash in RPCs so it's `GetQuorum()` that is used instead. The only place `ScanQuorums()` is used for Platform related stuff is `StartCleanupOldQuorumDataThread()` because we want to preserve quorum data used by `GetQuorum()`. But this can be optimised with its own (much more compact) cache. 3. RPCs that use `ScanQuorums()` should in most cases be ok with smaller cache, for other use cases there is a note in help text now. ## What was done? pls see individual commits ## How Has This Been Tested? run tests, run a node (~in progress~ looks stable) ## Breaking Changes n/a ## Checklist: - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have added or updated relevant unit/integration/functional/e2e tests - [ ] I have made corresponding changes to the documentation - [x] I have assigned this pull request to a milestone --- src/llmq/dkgsessionmgr.cpp | 6 +-- src/llmq/quorums.cpp | 75 +++++++++++++++++++++++++++++++------- src/llmq/quorums.h | 2 + src/llmq/utils.cpp | 1 + src/llmq/utils.h | 11 ++++++ src/rpc/quorums.cpp | 11 ++++-- 6 files changed, 85 insertions(+), 21 deletions(-) diff --git a/src/llmq/dkgsessionmgr.cpp b/src/llmq/dkgsessionmgr.cpp index 5f8f5ccee0..f058341b71 100644 --- a/src/llmq/dkgsessionmgr.cpp +++ b/src/llmq/dkgsessionmgr.cpp @@ -465,10 +465,6 @@ void CDKGSessionManager::CleanupOldContributions() const const auto prefixes = {DB_VVEC, DB_SKCONTRIB, DB_ENC_CONTRIB}; for (const auto& params : Params().GetConsensus().llmqs) { - // For how many blocks recent DKG info should be kept - const int MAX_CYCLES = params.useRotation ? params.keepOldKeys / params.signingActiveQuorumCount : params.keepOldKeys; - const int MAX_STORE_DEPTH = MAX_CYCLES * params.dkgInterval; - LogPrint(BCLog::LLMQ, "CDKGSessionManager::%s -- looking for old entries for llmq type %d\n", __func__, ToUnderlying(params.type)); CDBBatch batch(*db); @@ -486,7 +482,7 @@ void CDKGSessionManager::CleanupOldContributions() const } cnt_all++; const CBlockIndex* pindexQuorum = m_chainstate.m_blockman.LookupBlockIndex(std::get<2>(k)); - if (pindexQuorum == nullptr || m_chainstate.m_chain.Tip()->nHeight - pindexQuorum->nHeight > MAX_STORE_DEPTH) { + if (pindexQuorum == nullptr || m_chainstate.m_chain.Tip()->nHeight - pindexQuorum->nHeight > utils::max_store_depth(params)) { // not found or too old batch.Erase(k); cnt_old++; diff --git a/src/llmq/quorums.cpp b/src/llmq/quorums.cpp index 49c6fcd215..0d3969cdd3 100644 --- a/src/llmq/quorums.cpp +++ b/src/llmq/quorums.cpp @@ -201,8 +201,6 @@ CQuorumManager::CQuorumManager(CBLSWorker& _blsWorker, CChainState& chainstate, m_peerman(peerman) { utils::InitQuorumsCache(mapQuorumsCache, false); - utils::InitQuorumsCache(scanQuorumsCache, false); - quorumThreadInterrupt.reset(); } @@ -503,14 +501,45 @@ std::vector CQuorumManager::ScanQuorums(Consensus::LLMQType llmqTyp return {}; } - const CBlockIndex* pIndexScanCommitments{pindexStart}; + gsl::not_null pindexStore{pindexStart}; + const auto& llmq_params_opt = GetLLMQParams(llmqType); + assert(llmq_params_opt.has_value()); + + // Quorum sets can only change during the mining phase of DKG. + // Find the closest known block index. + const int quorumCycleStartHeight = pindexStart->nHeight - (pindexStart->nHeight % llmq_params_opt->dkgInterval); + const int quorumCycleMiningStartHeight = quorumCycleStartHeight + llmq_params_opt->dkgMiningWindowStart; + const int quorumCycleMiningEndHeight = quorumCycleStartHeight + llmq_params_opt->dkgMiningWindowEnd; + + if (pindexStart->nHeight < quorumCycleMiningStartHeight) { + // too early for this cycle, use the previous one + // bail out if it's below genesis block + if (quorumCycleMiningEndHeight < llmq_params_opt->dkgInterval) return {}; + pindexStore = pindexStart->GetAncestor(quorumCycleMiningEndHeight - llmq_params_opt->dkgInterval); + } else if (pindexStart->nHeight > quorumCycleMiningEndHeight) { + // we are past the mining phase of this cycle, use it + pindexStore = pindexStart->GetAncestor(quorumCycleMiningEndHeight); + } + // everything else is inside the mining phase of this cycle, no pindexStore adjustment needed + + gsl::not_null pIndexScanCommitments{pindexStore}; size_t nScanCommitments{nCountRequested}; std::vector vecResultQuorums; { LOCK(cs_scan_quorums); + if (scanQuorumsCache.empty()) { + for (const auto& llmq : Params().GetConsensus().llmqs) { + // NOTE: We store it for each block hash in the DKG mining phase here + // and not for a single quorum hash per quorum like we do for other caches. + // And we only do this for max_cycles() of the most recent quorums + // because signing by old quorums requires the exact quorum hash to be specified + // and quorum scanning isn't needed there. + scanQuorumsCache.try_emplace(llmq.type, utils::max_cycles(llmq, llmq.keepOldConnections) * (llmq.dkgMiningWindowEnd - llmq.dkgMiningWindowStart)); + } + } auto& cache = scanQuorumsCache[llmqType]; - bool fCacheExists = cache.get(pindexStart->GetBlockHash(), vecResultQuorums); + bool fCacheExists = cache.get(pindexStore->GetBlockHash(), vecResultQuorums); if (fCacheExists) { // We have exactly what requested so just return it if (vecResultQuorums.size() == nCountRequested) { @@ -524,17 +553,17 @@ std::vector CQuorumManager::ScanQuorums(Consensus::LLMQType llmqTyp // scanning for the rests if (!vecResultQuorums.empty()) { nScanCommitments -= vecResultQuorums.size(); + // bail out if it's below genesis block + if (vecResultQuorums.back()->m_quorum_base_block_index->pprev == nullptr) return {}; pIndexScanCommitments = vecResultQuorums.back()->m_quorum_base_block_index->pprev; } } else { - // If there is nothing in cache request at least cache.max_size() because this gets cached then later - nScanCommitments = std::max(nCountRequested, cache.max_size()); + // If there is nothing in cache request at least keepOldConnections because this gets cached then later + nScanCommitments = std::max(nCountRequested, static_cast(llmq_params_opt->keepOldConnections)); } } // Get the block indexes of the mined commitments to build the required quorums from - const auto& llmq_params_opt = GetLLMQParams(llmqType); - assert(llmq_params_opt.has_value()); std::vector pQuorumBaseBlockIndexes{ llmq_params_opt->useRotation ? quorumBlockProcessor.GetMinedCommitmentsIndexedUntilBlock(llmqType, pIndexScanCommitments, nScanCommitments) : quorumBlockProcessor.GetMinedCommitmentsUntilBlock(llmqType, pIndexScanCommitments, nScanCommitments) @@ -551,10 +580,12 @@ std::vector CQuorumManager::ScanQuorums(Consensus::LLMQType llmqTyp const size_t nCountResult{vecResultQuorums.size()}; if (nCountResult > 0) { LOCK(cs_scan_quorums); - // Don't cache more than cache.max_size() elements + // Don't cache more than keepOldConnections elements + // because signing by old quorums requires the exact quorum hash + // to be specified and quorum scanning isn't needed there. auto& cache = scanQuorumsCache[llmqType]; - const size_t nCacheEndIndex = std::min(nCountResult, cache.max_size()); - cache.emplace(pindexStart->GetBlockHash(), {vecResultQuorums.begin(), vecResultQuorums.begin() + nCacheEndIndex}); + const size_t nCacheEndIndex = std::min(nCountResult, static_cast(llmq_params_opt->keepOldConnections)); + cache.emplace(pindexStore->GetBlockHash(), {vecResultQuorums.begin(), vecResultQuorums.begin() + nCacheEndIndex}); } // Don't return more than nCountRequested elements const size_t nResultEndIndex = std::min(nCountResult, nCountRequested); @@ -1023,13 +1054,31 @@ void CQuorumManager::StartCleanupOldQuorumDataThread(const CBlockIndex* pIndex) workerPool.push([pIndex, t, this](int threadId) { std::set dbKeysToSkip; + if (LOCK(cs_cleanup); cleanupQuorumsCache.empty()) { + utils::InitQuorumsCache(cleanupQuorumsCache, false); + } for (const auto& params : Params().GetConsensus().llmqs) { if (quorumThreadInterrupt) { break; } - for (const auto& pQuorum : ScanQuorums(params.type, pIndex, params.keepOldKeys)) { - dbKeysToSkip.insert(MakeQuorumKey(*pQuorum)); + LOCK(cs_cleanup); + auto& cache = cleanupQuorumsCache[params.type]; + const CBlockIndex* pindex_loop{pIndex}; + std::set quorum_keys; + while (pindex_loop != nullptr && pIndex->nHeight - pindex_loop->nHeight < utils::max_store_depth(params)) { + uint256 quorum_key; + if (cache.get(pindex_loop->GetBlockHash(), quorum_key)) { + quorum_keys.insert(quorum_key); + if (quorum_keys.size() >= params.keepOldKeys) break; // extra safety belt + } + pindex_loop = pindex_loop->pprev; } + for (const auto& pQuorum : ScanQuorums(params.type, pIndex, params.keepOldKeys - quorum_keys.size())) { + const uint256 quorum_key = MakeQuorumKey(*pQuorum); + quorum_keys.insert(quorum_key); + cache.insert(pQuorum->m_quorum_base_block_index->GetBlockHash(), quorum_key); + } + dbKeysToSkip.merge(quorum_keys); } if (!quorumThreadInterrupt) { diff --git a/src/llmq/quorums.h b/src/llmq/quorums.h index 976ba0f300..afb515a056 100644 --- a/src/llmq/quorums.h +++ b/src/llmq/quorums.h @@ -231,6 +231,8 @@ private: mutable std::map> mapQuorumsCache GUARDED_BY(cs_map_quorums); mutable RecursiveMutex cs_scan_quorums; mutable std::map, StaticSaltedHasher>> scanQuorumsCache GUARDED_BY(cs_scan_quorums); + mutable Mutex cs_cleanup; + mutable std::map> cleanupQuorumsCache GUARDED_BY(cs_cleanup); mutable ctpl::thread_pool workerPool; mutable CThreadInterrupt quorumThreadInterrupt; diff --git a/src/llmq/utils.cpp b/src/llmq/utils.cpp index 8105ac5520..14ffd23ffe 100644 --- a/src/llmq/utils.cpp +++ b/src/llmq/utils.cpp @@ -1115,6 +1115,7 @@ template void InitQuorumsCache, StaticSaltedHasher>>>(std::map, StaticSaltedHasher>>& cache, bool limit_by_connections); template void InitQuorumsCache, StaticSaltedHasher, 0ul, 0ul>, std::less, std::allocator, StaticSaltedHasher, 0ul, 0ul>>>>>(std::map, StaticSaltedHasher, 0ul, 0ul>, std::less, std::allocator, StaticSaltedHasher, 0ul, 0ul>>>>&cache, bool limit_by_connections); template void InitQuorumsCache>>(std::map>& cache, bool limit_by_connections); +template void InitQuorumsCache>>(std::map>& cache, bool limit_by_connections); } // namespace utils diff --git a/src/llmq/utils.h b/src/llmq/utils.h index 2db8da2725..ff60e3ec67 100644 --- a/src/llmq/utils.h +++ b/src/llmq/utils.h @@ -122,6 +122,17 @@ void IterateNodesRandom(NodesContainer& nodeStates, Continue&& cont, Callback&& template void InitQuorumsCache(CacheType& cache, bool limit_by_connections = true); +[[ nodiscard ]] static constexpr int max_cycles(const Consensus::LLMQParams& llmqParams, int quorums_count) +{ + return llmqParams.useRotation ? quorums_count / llmqParams.signingActiveQuorumCount : quorums_count; +} + +[[ nodiscard ]] static constexpr int max_store_depth(const Consensus::LLMQParams& llmqParams) +{ + // For how many blocks recent DKG info should be kept + return max_cycles(llmqParams, llmqParams.keepOldKeys) * llmqParams.dkgInterval; +} + } // namespace utils [[ nodiscard ]] const std::optional GetLLMQParams(Consensus::LLMQType llmqType); diff --git a/src/rpc/quorums.cpp b/src/rpc/quorums.cpp index 3d46fb21ad..282ff3f768 100644 --- a/src/rpc/quorums.cpp +++ b/src/rpc/quorums.cpp @@ -36,7 +36,10 @@ static void quorum_list_help(const JSONRPCRequest& request) RPCHelpMan{"quorum list", "List of on-chain quorums\n", { - {"count", RPCArg::Type::NUM, /* default */ "", "Number of quorums to list. Will list active quorums if \"count\" is not specified."}, + {"count", RPCArg::Type::NUM, /* default */ "", + "Number of quorums to list. Will list active quorums if \"count\" is not specified.\n" + "Can be CPU/disk heavy when the value is larger than the number of active quorums." + }, }, RPCResult{ RPCResult::Type::OBJ, "", "", @@ -365,8 +368,10 @@ static void quorum_memberof_help(const JSONRPCRequest& request) { {"proTxHash", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "ProTxHash of the masternode."}, {"scanQuorumsCount", RPCArg::Type::NUM, /* default */ "", - "Number of quorums to scan for. If not specified,\n" - "the active quorum count for each specific quorum type is used."}, + "Number of quorums to scan for.\n" + "If not specified, the active quorum count for each specific quorum type is used.\n" + "Can be CPU/disk heavy when the value is larger than the number of active quorums." + }, }, RPCResults{}, RPCExamples{""}, From 3e5a6ef649c368be417b9d461cfa78c8a7e2564b Mon Sep 17 00:00:00 2001 From: UdjinM6 Date: Tue, 19 Dec 2023 16:43:36 +0300 Subject: [PATCH 3/8] fix(rpc): pass blockhash into `TxToJSON` so that `getspecialtxes` could show correct `instantlock`/`chainlock` values (#5774) ## Issue being fixed or feature implemented `instantlock` and `chainlock` are broken in `getspecialtxes` kudos to @thephez for finding the issue ## What was done? pass the hash and also rename the variable to self-describing ## How Has This Been Tested? run `getspecialtxes` on a node with and without the patch ## Breaking Changes `instantlock` and `chainlock` will show actual values and not just `false` all the time now (not sure if that qualifies for "breaking" though) ## Checklist: - [x] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have added or updated relevant unit/integration/functional/e2e tests - [ ] I have made corresponding changes to the documentation - [x] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_ --- doc/release-notes-5774.md | 4 ++++ src/rpc/blockchain.cpp | 6 +++--- test/functional/feature_llmq_chainlocks.py | 13 ++++++++++++- 3 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 doc/release-notes-5774.md diff --git a/doc/release-notes-5774.md b/doc/release-notes-5774.md new file mode 100644 index 0000000000..4ceafc25ff --- /dev/null +++ b/doc/release-notes-5774.md @@ -0,0 +1,4 @@ +RPC changes +----------- + +In `getspecialtxes` `instantlock` and `chainlock` fields were always `false`. They should show actual values now. diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index 0f3c3a2caa..2d6281c31b 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -2462,7 +2462,7 @@ static UniValue getspecialtxes(const JSONRPCRequest& request) CTxMemPool& mempool = EnsureMemPool(node); LLMQContext& llmq_ctx = EnsureLLMQContext(node); - uint256 hash(ParseHashV(request.params[0], "blockhash")); + uint256 blockhash(ParseHashV(request.params[0], "blockhash")); int nTxType = -1; if (!request.params[1].isNull()) { @@ -2491,7 +2491,7 @@ static UniValue getspecialtxes(const JSONRPCRequest& request) } } - const CBlockIndex* pblockindex = chainman.m_blockman.LookupBlockIndex(hash); + const CBlockIndex* pblockindex = chainman.m_blockman.LookupBlockIndex(blockhash); if (!pblockindex) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found"); } @@ -2519,7 +2519,7 @@ static UniValue getspecialtxes(const JSONRPCRequest& request) case 2 : { UniValue objTx(UniValue::VOBJ); - TxToJSON(*tx, uint256(), mempool, chainman.ActiveChainstate(), *llmq_ctx.clhandler, *llmq_ctx.isman, objTx); + TxToJSON(*tx, blockhash, mempool, chainman.ActiveChainstate(), *llmq_ctx.clhandler, *llmq_ctx.isman, objTx); result.push_back(objTx); break; } diff --git a/test/functional/feature_llmq_chainlocks.py b/test/functional/feature_llmq_chainlocks.py index 93959bc154..f4981a61d0 100755 --- a/test/functional/feature_llmq_chainlocks.py +++ b/test/functional/feature_llmq_chainlocks.py @@ -40,8 +40,13 @@ class LLMQChainLocksTest(DashTestFramework): self.test_coinbase_best_cl(self.nodes[0], expected_cl_in_cb=False) # v20 is active, no quorums, no CLs - null CL in CbTx - self.nodes[0].generate(1) + nocl_block_hash = self.nodes[0].generate(1)[0] self.test_coinbase_best_cl(self.nodes[0], expected_cl_in_cb=True, expected_null_cl=True) + cbtx = self.nodes[0].getspecialtxes(nocl_block_hash, 5, 1, 0, 2)[0] + assert_equal(cbtx["instantlock"], False) + assert_equal(cbtx["instantlock_internal"], False) + assert_equal(cbtx["chainlock"], False) + self.nodes[0].sporkupdate("SPORK_17_QUORUM_DKG_ENABLED", 0) self.wait_for_sporks_same() @@ -55,6 +60,12 @@ class LLMQChainLocksTest(DashTestFramework): self.wait_for_chainlocked_block_all_nodes(self.nodes[0].getbestblockhash()) self.test_coinbase_best_cl(self.nodes[0]) + # ChainLock locks all the blocks below it so nocl_block_hash should be locked too + cbtx = self.nodes[0].getspecialtxes(nocl_block_hash, 5, 1, 0, 2)[0] + assert_equal(cbtx["instantlock"], True) + assert_equal(cbtx["instantlock_internal"], False) + assert_equal(cbtx["chainlock"], True) + self.log.info("Mine many blocks, wait for chainlock") self.nodes[0].generate(20) # We need more time here due to 20 blocks being generated at once From 234d0900455f1b7419ff709f559ad0004e2fbe48 Mon Sep 17 00:00:00 2001 From: PastaPastaPasta <6443210+PastaPastaPasta@users.noreply.github.com> Date: Tue, 5 Dec 2023 12:48:51 -0600 Subject: [PATCH 4/8] Merge pull request #5152 from vijaydasmp/bp21_14 backport: Merge bitcoin#18814, 18781 --- src/random.cpp | 16 ++-------------- src/random.h | 16 ++++++++++++++-- src/rpc/server.cpp | 6 ++++-- src/test/random_tests.cpp | 7 +++++-- src/wallet/rpcwallet.cpp | 9 +++++++-- src/wallet/wallet.h | 4 +++- 6 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/random.cpp b/src/random.cpp index 68f4d5bf02..d7d869024e 100644 --- a/src/random.cpp +++ b/src/random.cpp @@ -14,16 +14,14 @@ #include #endif #include // for LogPrintf() +#include +#include #include // for Mutex #include // for GetTimeMicros() #include #include -#include - -#include - #ifndef WIN32 #include #endif @@ -582,16 +580,6 @@ static void ProcRand(unsigned char* out, int num, RNGLevel level) noexcept } } -std::chrono::microseconds GetRandMicros(std::chrono::microseconds duration_max) noexcept -{ - return std::chrono::microseconds{GetRand(duration_max.count())}; -} - -std::chrono::milliseconds GetRandMillis(std::chrono::milliseconds duration_max) noexcept -{ - return std::chrono::milliseconds{GetRand(duration_max.count())}; -} - void GetRandBytes(unsigned char* buf, int num) noexcept { ProcRand(buf, num, RNGLevel::FAST); } void GetStrongRandBytes(unsigned char* buf, int num) noexcept { ProcRand(buf, num, RNGLevel::SLOW); } void RandAddPeriodic() noexcept { ProcRand(nullptr, 0, RNGLevel::PERIODIC); } diff --git a/src/random.h b/src/random.h index 8bbf1d4d0c..2250e608dd 100644 --- a/src/random.h +++ b/src/random.h @@ -69,9 +69,21 @@ * Thread-safe. */ void GetRandBytes(unsigned char* buf, int num) noexcept; +/** Generate a uniform random integer in the range [0..range). Precondition: range > 0 */ uint64_t GetRand(uint64_t nMax) noexcept; -std::chrono::microseconds GetRandMicros(std::chrono::microseconds duration_max) noexcept; -std::chrono::milliseconds GetRandMillis(std::chrono::milliseconds duration_max) noexcept; +/** Generate a uniform random duration in the range [0..max). Precondition: max.count() > 0 */ +template +D GetRandomDuration(typename std::common_type::type max) noexcept +// Having the compiler infer the template argument from the function argument +// is dangerous, because the desired return value generally has a different +// type than the function argument. So std::common_type is used to force the +// call site to specify the type of the return value. +{ + assert(max.count() > 0); + return D{GetRand(max.count())}; +}; +constexpr auto GetRandMicros = GetRandomDuration; +constexpr auto GetRandMillis = GetRandomDuration; int GetRandInt(int nMax) noexcept; uint256 GetRandHash() noexcept; diff --git a/src/rpc/server.cpp b/src/rpc/server.cpp index a87c7e2263..6ccd7c278f 100644 --- a/src/rpc/server.cpp +++ b/src/rpc/server.cpp @@ -28,7 +28,8 @@ static std::string rpcWarmupStatus GUARDED_BY(g_rpc_warmup_mutex) = "RPC server /* Timer-creating functions */ static RPCTimerInterface* timerInterface = nullptr; /* Map of name to timer. */ -static std::map > deadlineTimers; +static Mutex g_deadline_timers_mutex; +static std::map > deadlineTimers GUARDED_BY(g_deadline_timers_mutex); static bool ExecuteCommand(const CRPCCommand& command, const JSONRPCRequest& request, UniValue& result, bool last_handler, std::multimap> mapPlatformRestrictions); // Any commands submitted by this user will have their commands filtered based on the mapPlatformRestrictions @@ -330,7 +331,7 @@ void InterruptRPC() void StopRPC() { LogPrint(BCLog::RPC, "Stopping RPC\n"); - deadlineTimers.clear(); + WITH_LOCK(g_deadline_timers_mutex, deadlineTimers.clear()); DeleteAuthCookie(); g_rpcSignals.Stopped(); } @@ -609,6 +610,7 @@ void RPCRunLater(const std::string& name, std::function func, int64_t nS { if (!timerInterface) throw JSONRPCError(RPC_INTERNAL_ERROR, "No timer handler registered for RPC"); + LOCK(g_deadline_timers_mutex); deadlineTimers.erase(name); LogPrint(BCLog::RPC, "queue run of timer %s in %i seconds (using %s)\n", name, nSeconds, timerInterface->Name()); deadlineTimers.emplace(name, std::unique_ptr(timerInterface->NewTimer(func, nSeconds*1000))); diff --git a/src/test/random_tests.cpp b/src/test/random_tests.cpp index d8cc0a564c..d94009ebfb 100644 --- a/src/test/random_tests.cpp +++ b/src/test/random_tests.cpp @@ -27,6 +27,8 @@ BOOST_AUTO_TEST_CASE(fastrandom_tests) for (int i = 10; i > 0; --i) { BOOST_CHECK_EQUAL(GetRand(std::numeric_limits::max()), uint64_t{10393729187455219830U}); BOOST_CHECK_EQUAL(GetRandInt(std::numeric_limits::max()), int{769702006}); + BOOST_CHECK_EQUAL(GetRandMicros(std::chrono::hours{1}).count(), 2917185654); + BOOST_CHECK_EQUAL(GetRandMillis(std::chrono::hours{1}).count(), 2144374); } { constexpr SteadySeconds time_point{1s}; @@ -66,6 +68,8 @@ BOOST_AUTO_TEST_CASE(fastrandom_tests) for (int i = 10; i > 0; --i) { BOOST_CHECK(GetRand(std::numeric_limits::max()) != uint64_t{10393729187455219830U}); BOOST_CHECK(GetRandInt(std::numeric_limits::max()) != int{769702006}); + BOOST_CHECK(GetRandMicros(std::chrono::hours{1}) != std::chrono::microseconds{2917185654}); + BOOST_CHECK(GetRandMillis(std::chrono::hours{1}) != std::chrono::milliseconds{2144374}); } { @@ -107,7 +111,7 @@ BOOST_AUTO_TEST_CASE(stdrandom_test) BOOST_CHECK(x >= 3); BOOST_CHECK(x <= 9); - std::vector test{1,2,3,4,5,6,7,8,9,10}; + std::vector test{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; std::shuffle(test.begin(), test.end(), ctx); for (int j = 1; j <= 10; ++j) { BOOST_CHECK(std::find(test.begin(), test.end(), j) != test.end()); @@ -117,7 +121,6 @@ BOOST_AUTO_TEST_CASE(stdrandom_test) BOOST_CHECK(std::find(test.begin(), test.end(), j) != test.end()); } } - } /** Test that Shuffle reaches every permutation with equal probability. */ diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 703da04873..752098cbf5 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -1927,6 +1927,9 @@ static UniValue walletpassphrase(const JSONRPCRequest& request) CWallet* const pwallet = wallet.get(); int64_t nSleepTime; + int64_t relock_time; + // Prevent concurrent calls to walletpassphrase with the same wallet. + LOCK(pwallet->m_unlock_mutex); { LOCK(pwallet->cs_wallet); @@ -1975,7 +1978,7 @@ static UniValue walletpassphrase(const JSONRPCRequest& request) pwallet->TopUpKeyPool(); pwallet->nRelockTime = GetTime() + nSleepTime; - + relock_time = pwallet->nRelockTime; } // rpcRunLater must be called without cs_wallet held otherwise a deadlock // can occur. The deadlock would happen when RPCRunLater removes the @@ -1986,9 +1989,11 @@ static UniValue walletpassphrase(const JSONRPCRequest& request) // wallet before the following callback is called. If a valid shared pointer // is acquired in the callback then the wallet is still loaded. std::weak_ptr weak_wallet = wallet; - pwallet->chain().rpcRunLater(strprintf("lockwallet(%s)", pwallet->GetName()), [weak_wallet] { + pwallet->chain().rpcRunLater(strprintf("lockwallet(%s)", pwallet->GetName()), [weak_wallet, relock_time] { if (auto shared_wallet = weak_wallet.lock()) { LOCK(shared_wallet->cs_wallet); + // Skip if this is not the most recent rpcRunLater callback. + if (shared_wallet->nRelockTime != relock_time) return; shared_wallet->Lock(); shared_wallet->nRelockTime = 0; } diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index b816a2d183..e70c4b6050 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -965,8 +965,10 @@ public: std::vector GetDestValues(const std::string& prefix) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); //! Holds a timestamp at which point the wallet is scheduled (externally) to be relocked. Caller must arrange for actual relocking to occur via Lock(). - int64_t nRelockTime = 0; + int64_t nRelockTime GUARDED_BY(cs_wallet){0}; + // Used to prevent concurrent calls to walletpassphrase RPC. + Mutex m_unlock_mutex; bool Unlock(const SecureString& strWalletPassphrase, bool fForMixingOnly = false, bool accept_no_keys = false); bool ChangeWalletPassphrase(const SecureString& strOldWalletPassphrase, const SecureString& strNewWalletPassphrase); bool EncryptWallet(const SecureString& strWalletPassphrase); From 71e7658fdf20e22c90d7713cb6f7be556e973857 Mon Sep 17 00:00:00 2001 From: PastaPastaPasta <6443210+PastaPastaPasta@users.noreply.github.com> Date: Thu, 21 Dec 2023 12:01:39 -0600 Subject: [PATCH 5/8] refactor: pass large structure by const reference on every RPC call (#5780) --- src/rpc/server.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rpc/server.cpp b/src/rpc/server.cpp index 6ccd7c278f..4164486d31 100644 --- a/src/rpc/server.cpp +++ b/src/rpc/server.cpp @@ -30,7 +30,7 @@ static RPCTimerInterface* timerInterface = nullptr; /* Map of name to timer. */ static Mutex g_deadline_timers_mutex; static std::map > deadlineTimers GUARDED_BY(g_deadline_timers_mutex); -static bool ExecuteCommand(const CRPCCommand& command, const JSONRPCRequest& request, UniValue& result, bool last_handler, std::multimap> mapPlatformRestrictions); +static bool ExecuteCommand(const CRPCCommand& command, const JSONRPCRequest& request, UniValue& result, bool last_handler, const std::multimap>& mapPlatformRestrictions); // Any commands submitted by this user will have their commands filtered based on the mapPlatformRestrictions static const std::string defaultPlatformUser = "platform-user"; @@ -503,7 +503,7 @@ UniValue CRPCTable::execute(const JSONRPCRequest &request) const throw JSONRPCError(RPC_METHOD_NOT_FOUND, "Method not found"); } -static bool ExecuteCommand(const CRPCCommand& command, const JSONRPCRequest& request, UniValue& result, bool last_handler, std::multimap> mapPlatformRestrictions) +static bool ExecuteCommand(const CRPCCommand& command, const JSONRPCRequest& request, UniValue& result, bool last_handler, const std::multimap>& mapPlatformRestrictions) { // Before executing the RPC Command, filter commands from platform rpc user if (fMasternodeMode && request.authUser == gArgs.GetArg("-platform-user", defaultPlatformUser)) { From b3a1c8b9c67a87b865b270540ed111fc8c516f0a Mon Sep 17 00:00:00 2001 From: UdjinM6 Date: Fri, 22 Dec 2023 22:56:43 +0300 Subject: [PATCH 6/8] fix: ScanQuorums should not start cache population for outdated quorums (#5784) ## Issue being fixed or feature implemented Cache population for old quorums is a cpu heavy operation and should be avoided for inactive quorums _at least_ oin `ScanQuorums`. This issue is critical for testnet and other small network because every mn participate in almost every platform quorum and cache population for 2 months of quorums can easily block everything for 15+ minutes on a 4 cpu node. On mainnet quorum distribution is much better but it's still a small waste of cpu (or not so small for unlucky nodes). #5761 follow-up ## What was done? Do not start cache population for outdated quorums, improve logs in `StartCachePopulatorThread` to make it easier to see what's going on. ## How Has This Been Tested? run a mn on testnet ## Breaking Changes n/a ## Checklist: - [x] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have added or updated relevant unit/integration/functional/e2e tests - [ ] I have made corresponding changes to the documentation - [x] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_ --- src/llmq/quorums.cpp | 23 ++++++++++++++++------- src/llmq/quorums.h | 4 ++-- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/llmq/quorums.cpp b/src/llmq/quorums.cpp index 0d3969cdd3..e7d8073b64 100644 --- a/src/llmq/quorums.cpp +++ b/src/llmq/quorums.cpp @@ -362,7 +362,7 @@ void CQuorumManager::CheckQuorumConnections(const Consensus::LLMQParams& llmqPar } } -CQuorumPtr CQuorumManager::BuildQuorumFromCommitment(const Consensus::LLMQType llmqType, gsl::not_null pQuorumBaseBlockIndex) const +CQuorumPtr CQuorumManager::BuildQuorumFromCommitment(const Consensus::LLMQType llmqType, gsl::not_null pQuorumBaseBlockIndex, bool populate_cache) const { const uint256& quorumHash{pQuorumBaseBlockIndex->GetBlockHash()}; uint256 minedBlockHash; @@ -392,7 +392,7 @@ CQuorumPtr CQuorumManager::BuildQuorumFromCommitment(const Consensus::LLMQType l } } - if (hasValidVvec) { + if (hasValidVvec && populate_cache) { // pre-populate caches in the background // recovering public key shares is quite expensive and would result in serious lags for the first few signing // sessions if the shares would be calculated on-demand @@ -572,7 +572,9 @@ std::vector CQuorumManager::ScanQuorums(Consensus::LLMQType llmqTyp for (auto& pQuorumBaseBlockIndex : pQuorumBaseBlockIndexes) { assert(pQuorumBaseBlockIndex); - auto quorum = GetQuorum(llmqType, pQuorumBaseBlockIndex); + // populate cache for keepOldConnections most recent quorums only + bool populate_cache = vecResultQuorums.size() < llmq_params_opt->keepOldConnections; + auto quorum = GetQuorum(llmqType, pQuorumBaseBlockIndex, populate_cache); assert(quorum != nullptr); vecResultQuorums.emplace_back(quorum); } @@ -602,7 +604,7 @@ CQuorumCPtr CQuorumManager::GetQuorum(Consensus::LLMQType llmqType, const uint25 return GetQuorum(llmqType, pQuorumBaseBlockIndex); } -CQuorumCPtr CQuorumManager::GetQuorum(Consensus::LLMQType llmqType, gsl::not_null pQuorumBaseBlockIndex) const +CQuorumCPtr CQuorumManager::GetQuorum(Consensus::LLMQType llmqType, gsl::not_null pQuorumBaseBlockIndex, bool populate_cache) const { auto quorumHash = pQuorumBaseBlockIndex->GetBlockHash(); @@ -617,7 +619,7 @@ CQuorumCPtr CQuorumManager::GetQuorum(Consensus::LLMQType llmqType, gsl::not_nul return pQuorum; } - return BuildQuorumFromCommitment(llmqType, pQuorumBaseBlockIndex); + return BuildQuorumFromCommitment(llmqType, pQuorumBaseBlockIndex, populate_cache); } size_t CQuorumManager::GetQuorumRecoveryStartOffset(const CQuorumCPtr pQuorum, const CBlockIndex* pIndex) const @@ -850,7 +852,10 @@ void CQuorumManager::StartCachePopulatorThread(const CQuorumCPtr pQuorum) const } cxxtimer::Timer t(true); - LogPrint(BCLog::LLMQ, "CQuorumManager::StartCachePopulatorThread -- start\n"); + LogPrint(BCLog::LLMQ, "CQuorumManager::StartCachePopulatorThread -- type=%d height=%d hash=%s start\n", + ToUnderlying(pQuorum->params.type), + pQuorum->m_quorum_base_block_index->nHeight, + pQuorum->m_quorum_base_block_index->GetBlockHash().ToString()); // when then later some other thread tries to get keys, it will be much faster workerPool.push([pQuorum, t, this](int threadId) { @@ -862,7 +867,11 @@ void CQuorumManager::StartCachePopulatorThread(const CQuorumCPtr pQuorum) const pQuorum->GetPubKeyShare(i); } } - LogPrint(BCLog::LLMQ, "CQuorumManager::StartCachePopulatorThread -- done. time=%d\n", t.count()); + LogPrint(BCLog::LLMQ, "CQuorumManager::StartCachePopulatorThread -- type=%d height=%d hash=%s done. time=%d\n", + ToUnderlying(pQuorum->params.type), + pQuorum->m_quorum_base_block_index->nHeight, + pQuorum->m_quorum_base_block_index->GetBlockHash().ToString(), + t.count()); }); } diff --git a/src/llmq/quorums.h b/src/llmq/quorums.h index afb515a056..e80e876aee 100644 --- a/src/llmq/quorums.h +++ b/src/llmq/quorums.h @@ -267,10 +267,10 @@ private: // all private methods here are cs_main-free void CheckQuorumConnections(const Consensus::LLMQParams& llmqParams, const CBlockIndex *pindexNew) const; - CQuorumPtr BuildQuorumFromCommitment(Consensus::LLMQType llmqType, gsl::not_null pQuorumBaseBlockIndex) const; + CQuorumPtr BuildQuorumFromCommitment(Consensus::LLMQType llmqType, gsl::not_null pQuorumBaseBlockIndex, bool populate_cache) const; bool BuildQuorumContributions(const CFinalCommitmentPtr& fqc, const std::shared_ptr& quorum) const; - CQuorumCPtr GetQuorum(Consensus::LLMQType llmqType, gsl::not_null pindex) const; + CQuorumCPtr GetQuorum(Consensus::LLMQType llmqType, gsl::not_null pindex, bool populate_cache = true) const; /// Returns the start offset for the masternode with the given proTxHash. This offset is applied when picking data recovery members of a quorum's /// memberlist and is calculated based on a list of all member of all active quorums for the given llmqType in a way that each member /// should receive the same number of request if all active llmqType members requests data from one llmqType quorum. From 12649070f879b46df8c19078e9cfda11ce48f332 Mon Sep 17 00:00:00 2001 From: Odysseas Gabrielides Date: Sun, 24 Dec 2023 13:00:06 +0200 Subject: [PATCH 7/8] chore: bump version to 20.0.3 --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 1dd19c2bde..1231545a14 100644 --- a/configure.ac +++ b/configure.ac @@ -1,7 +1,7 @@ AC_PREREQ([2.69]) define(_CLIENT_VERSION_MAJOR, 20) define(_CLIENT_VERSION_MINOR, 0) -define(_CLIENT_VERSION_BUILD, 2) +define(_CLIENT_VERSION_BUILD, 3) define(_CLIENT_VERSION_RC, 0) define(_CLIENT_VERSION_IS_RELEASE, true) define(_COPYRIGHT_YEAR, 2023) From 3192d8c9f8e516b290fd9a8d4628c1e370a0362e Mon Sep 17 00:00:00 2001 From: Odysseas Gabrielides Date: Sun, 24 Dec 2023 13:00:25 +0200 Subject: [PATCH 8/8] docs: archive v20.0.2 release notes and create v20.0.3 release notes --- doc/release-notes-5774.md | 4 - doc/release-notes.md | 17 ++- .../dash/release-notes-20.0.2.md | 133 ++++++++++++++++++ 3 files changed, 144 insertions(+), 10 deletions(-) delete mode 100644 doc/release-notes-5774.md create mode 100644 doc/release-notes/dash/release-notes-20.0.2.md diff --git a/doc/release-notes-5774.md b/doc/release-notes-5774.md deleted file mode 100644 index 4ceafc25ff..0000000000 --- a/doc/release-notes-5774.md +++ /dev/null @@ -1,4 +0,0 @@ -RPC changes ------------ - -In `getspecialtxes` `instantlock` and `chainlock` fields were always `false`. They should show actual values now. diff --git a/doc/release-notes.md b/doc/release-notes.md index d72df7f9e7..9f5b1a83f0 100644 --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -1,4 +1,4 @@ -# Dash Core version v20.0.2 +# Dash Core version v20.0.3 Release is now available from: @@ -37,18 +37,22 @@ reindex or re-sync the whole chain. ## Masternode fix -A problem has been fixed in the old quorum data cleanup mechanism. It was slowing down masternodes during DKG sessions and causing them to get PoSe scored. +The memory usage during the old quorum data cleanup mechanism was reduced. -## Testnet Crash +## Wallet fix -A fix has been implemented for the reported crash that could occur when upgrading from v19.x to v20.0.0 after v20 activation without re-indexing. +A fix has been implemented for the reported decryption of wallets. + +## RPC changes + +In `getspecialtxes` `instantlock` and `chainlock` fields are reflecting actual values now. ## Other changes Implemented improvements in Github CI and build system for macOS. Fixed compilation issues on FreeBSD. -# v20.0.2 Change log +# v20.0.3 Change log See detailed [set of changes][set-of-changes]. @@ -86,6 +90,7 @@ Dash Core tree 0.12.1.x was a fork of Bitcoin Core tree 0.12. These release are considered obsolete. Old release notes can be found here: +- [v20.0.2](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-20.0.2.md) released December/06/2023 - [v20.0.1](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-20.0.1.md) released November/18/2023 - [v20.0.0](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-20.0.0.md) released November/15/2023 - [v19.3.0](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-19.3.0.md) released July/31/2023 @@ -130,4 +135,4 @@ These release are considered obsolete. Old release notes can be found here: - [v0.10.x](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-0.10.0.md) released Sep/25/2014 - [v0.9.x](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-0.9.0.md) released Mar/13/2014 -[set-of-changes]: https://github.com/dashpay/dash/compare/v20.0.1...dashpay:v20.0.2 +[set-of-changes]: https://github.com/dashpay/dash/compare/v20.0.2...dashpay:v20.0.3 diff --git a/doc/release-notes/dash/release-notes-20.0.2.md b/doc/release-notes/dash/release-notes-20.0.2.md new file mode 100644 index 0000000000..d72df7f9e7 --- /dev/null +++ b/doc/release-notes/dash/release-notes-20.0.2.md @@ -0,0 +1,133 @@ +# Dash Core version v20.0.2 + +Release is now available from: + + + +This is a new patch version release, bringing small bug fixes and build system enhancements. + +This release is optional for all nodes. + +Please report bugs using the issue tracker at GitHub: + + + + +# Upgrading and downgrading + +## How to Upgrade + +If you are running an older version, shut it down. Wait until it has completely +shut down (which might take a few minutes for older versions), then run the +installer (on Windows) or just copy over /Applications/Dash-Qt (on Mac) or +dashd/dash-qt (on Linux). If you upgrade after DIP0003 activation and you were +using version < 0.13 you will have to reindex (start with -reindex-chainstate +or -reindex) to make sure your wallet has all the new data synced. Upgrading +from version 0.13 should not require any additional actions. + +## Downgrade warning + +### Downgrade to a version < v19.2.0 + +Downgrading to a version older than v19.2.0 is not supported due to changes +in the evodb database. If you need to use an older version, you must either +reindex or re-sync the whole chain. + +# Notable changes + +## Masternode fix + +A problem has been fixed in the old quorum data cleanup mechanism. It was slowing down masternodes during DKG sessions and causing them to get PoSe scored. + +## Testnet Crash + +A fix has been implemented for the reported crash that could occur when upgrading from v19.x to v20.0.0 after v20 activation without re-indexing. + +## Other changes + +Implemented improvements in Github CI and build system for macOS. Fixed compilation issues on FreeBSD. + + +# v20.0.2 Change log + +See detailed [set of changes][set-of-changes]. + +# Credits + +Thanks to everyone who directly contributed to this release: + +- Konstantin Akimov (knst) +- Odysseas Gabrielides (ogabrielides) +- PastaPastaPasta +- UdjinM6 + +As well as everyone that submitted issues, reviewed pull requests and helped +debug the release candidates. + +# Older releases + +Dash was previously known as Darkcoin. + +Darkcoin tree 0.8.x was a fork of Litecoin tree 0.8, original name was XCoin +which was first released on Jan/18/2014. + +Darkcoin tree 0.9.x was the open source implementation of masternodes based on +the 0.8.x tree and was first released on Mar/13/2014. + +Darkcoin tree 0.10.x used to be the closed source implementation of Darksend +which was released open source on Sep/25/2014. + +Dash Core tree 0.11.x was a fork of Bitcoin Core tree 0.9, +Darkcoin was rebranded to Dash. + +Dash Core tree 0.12.0.x was a fork of Bitcoin Core tree 0.10. + +Dash Core tree 0.12.1.x was a fork of Bitcoin Core tree 0.12. + +These release are considered obsolete. Old release notes can be found here: + +- [v20.0.1](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-20.0.1.md) released November/18/2023 +- [v20.0.0](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-20.0.0.md) released November/15/2023 +- [v19.3.0](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-19.3.0.md) released July/31/2023 +- [v19.2.0](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-19.2.0.md) released June/19/2023 +- [v19.1.0](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-19.1.0.md) released May/22/2023 +- [v19.0.0](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-19.0.0.md) released Apr/14/2023 +- [v18.2.2](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-18.2.2.md) released Mar/21/2023 +- [v18.2.1](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-18.2.1.md) released Jan/17/2023 +- [v18.2.0](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-18.2.0.md) released Jan/01/2023 +- [v18.1.1](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-18.1.1.md) released January/08/2023 +- [v18.1.0](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-18.1.0.md) released October/09/2022 +- [v18.0.2](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-18.0.2.md) released October/09/2022 +- [v18.0.1](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-18.0.1.md) released August/17/2022 +- [v0.17.0.3](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-0.17.0.3.md) released June/07/2021 +- [v0.17.0.2](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-0.17.0.2.md) released May/19/2021 +- [v0.16.1.1](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-0.16.1.1.md) released November/17/2020 +- [v0.16.1.0](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-0.16.1.0.md) released November/14/2020 +- [v0.16.0.1](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-0.16.0.1.md) released September/30/2020 +- [v0.15.0.0](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-0.15.0.0.md) released Febrary/18/2020 +- [v0.14.0.5](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-0.14.0.5.md) released December/08/2019 +- [v0.14.0.4](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-0.14.0.4.md) released November/22/2019 +- [v0.14.0.3](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-0.14.0.3.md) released August/15/2019 +- [v0.14.0.2](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-0.14.0.2.md) released July/4/2019 +- [v0.14.0.1](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-0.14.0.1.md) released May/31/2019 +- [v0.14.0](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-0.14.0.md) released May/22/2019 +- [v0.13.3](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-0.13.3.md) released Apr/04/2019 +- [v0.13.2](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-0.13.2.md) released Mar/15/2019 +- [v0.13.1](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-0.13.1.md) released Feb/9/2019 +- [v0.13.0](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-0.13.0.md) released Jan/14/2019 +- [v0.12.3.4](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-0.12.3.4.md) released Dec/14/2018 +- [v0.12.3.3](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-0.12.3.3.md) released Sep/19/2018 +- [v0.12.3.2](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-0.12.3.2.md) released Jul/09/2018 +- [v0.12.3.1](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-0.12.3.1.md) released Jul/03/2018 +- [v0.12.2.3](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-0.12.2.3.md) released Jan/12/2018 +- [v0.12.2.2](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-0.12.2.2.md) released Dec/17/2017 +- [v0.12.2](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-0.12.2.md) released Nov/08/2017 +- [v0.12.1](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-0.12.1.md) released Feb/06/2017 +- [v0.12.0](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-0.12.0.md) released Aug/15/2015 +- [v0.11.2](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-0.11.2.md) released Mar/04/2015 +- [v0.11.1](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-0.11.1.md) released Feb/10/2015 +- [v0.11.0](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-0.11.0.md) released Jan/15/2015 +- [v0.10.x](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-0.10.0.md) released Sep/25/2014 +- [v0.9.x](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-0.9.0.md) released Mar/13/2014 + +[set-of-changes]: https://github.com/dashpay/dash/compare/v20.0.1...dashpay:v20.0.2