diff --git a/docker/build-docker.sh b/docker/build-docker.sh index 51aff468b3bf..52908d87e3ee 100755 --- a/docker/build-docker.sh +++ b/docker/build-docker.sh @@ -13,7 +13,7 @@ mkdir docker/bin cp $BUILD_DIR/src/dashd docker/bin/ cp $BUILD_DIR/src/dash-cli docker/bin/ cp $BUILD_DIR/src/dash-tx docker/bin/ -strip docker/bin/dashd +#strip docker/bin/dashd strip docker/bin/dash-cli strip docker/bin/dash-tx diff --git a/qa/rpc-tests/dip3-deterministicmns.py b/qa/rpc-tests/dip3-deterministicmns.py index dfcd14a56cc2..c49c267bd8d2 100755 --- a/qa/rpc-tests/dip3-deterministicmns.py +++ b/qa/rpc-tests/dip3-deterministicmns.py @@ -361,13 +361,14 @@ def run_test(self): banned = False t = time.time() while (not punished or not banned) and (time.time() - t) < 60: - time.sleep(1) + # Init phase needs some time + time.sleep(0.5) - # 2 dummy phases - for j in range(2): + # all phases + for j in range(6): self.nodes[0].generate(2) self.sync_all() - time.sleep(1) + time.sleep(0.5) info = self.nodes[0].protx('info', mn.protx_hash) if not punished: @@ -379,7 +380,7 @@ def run_test(self): # Fast-forward to next DKG session self.nodes[0].generate(24 - (self.nodes[0].getblockcount() % 24)) - self.sync_all() + self.sync_all() assert(punished and banned) def create_mn(self, node, idx, alias): diff --git a/qa/rpc-tests/quorums.py b/qa/rpc-tests/quorums.py new file mode 100755 index 000000000000..f6d225720dba --- /dev/null +++ b/qa/rpc-tests/quorums.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 +# Copyright (c) 2015-2018 The Dash Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +# +# Test deterministic masternodes +# +from concurrent.futures import ThreadPoolExecutor + +from test_framework.blocktools import create_block, create_coinbase, get_masternode_payment +from test_framework.mininode import CTransaction, ToHex, FromHex, CTxOut, COIN, CCbTx +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import * + +class Masternode(object): + pass + +class QuorumsTest(BitcoinTestFramework): + def __init__(self): + super().__init__() + self.num_initial_mn = 60 + self.num_nodes = 1 + self.num_initial_mn + 2 # +1 for controller, +1 for mn-qt, +1 for mn created after dip3 activation + self.setup_clean_chain = True + + self.extra_args = ["-budgetparams=240:100:240"] + self.extra_args += ["-sporkkey=cP4EKFyJsHT39LDqgdcB43Y3YXjNyjb5Fuas1GQSeAtjnZWmZEQK"] + #self.extra_args += ["-debug=1"] + + def setup_network(self): + disable_mocktime() + self.start_controller_node() + self.is_network_split = False + + def start_controller_node(self, extra_args=None): + print("starting controller node") + if self.nodes is None: + self.nodes = [None] + args = self.extra_args + if extra_args is not None: + args += extra_args + self.nodes[0] = start_node(0, self.options.tmpdir, extra_args=args) + for i in range(1, self.num_nodes): + if i < len(self.nodes) and self.nodes[i] is not None: + connect_nodes_bi(self.nodes, 0, i) + + def stop_controller_node(self): + print("stopping controller node") + stop_node(self.nodes[0], 0) + + def restart_controller_node(self): + self.stop_controller_node() + self.start_controller_node() + + def run_test(self): + print("funding controller node") + while self.nodes[0].getbalance() < (self.num_initial_mn + 3) * 1000: + self.nodes[0].generate(10) # generate enough for collaterals + print("controller node has {} dash".format(self.nodes[0].getbalance())) + + print("generating enough blocks for DIP3 activation") + while self.nodes[0].getblockchaininfo()['bip9_softforks']['dip0003']['status'] != 'active': + self.nodes[0].generate(1) + self.nodes[0].generate(1) + self.force_finish_mnsync(self.nodes[0]) + + print("registering MNs") + mns = [] + mn_idx = 1 + for i in range(self.num_initial_mn): + mn = self.create_mn_protx(self.nodes[0], mn_idx, 'mn-%d' % (mn_idx)) + mn_idx += 1 + mns.append(mn) + + # mature collaterals + for i in range(3): + self.nodes[0].generate(1) + time.sleep(1) + + self.sync_all() + + self.stop_controller_node() + for mn in mns: + dirs = ['blocks', 'chainstate', 'evodb'] + src = os.path.join(self.options.tmpdir, 'node0', 'regtest') + dst = os.path.join(mn.datadir, 'regtest') + for d in dirs: + shutil.copytree(os.path.join(src, d), os.path.join(dst, d)) + self.start_controller_node() + + print("starting MNs") + while len(self.nodes) < mn_idx: + self.nodes.append(None) + executor = ThreadPoolExecutor(max_workers=20) + for mn in mns: + mn.start_job = executor.submit(self.start_mn, mn) + for mn in mns: + mn.start_job.result() + self.force_finish_mnsync(mn.node) + + batchsize = 5 + cnode = 0 + for i in range(0, len(mns), batchsize): + batch = mns[i:i+batchsize] + print("connecting %s - %s" % (batch[0].alias, batch[len(batch) - 1].alias)) + for mn in batch: + connect_nodes(self.nodes[mn.idx], cnode, wait_for_handshake=False) + + random_nodes = [] + for i in range(len(self.nodes)): + if i != mn.idx: + random_nodes += [i] + random.shuffle(random_nodes) + for i in range(min(3, len(random_nodes))): + connect_nodes(mn.node, random_nodes[i], wait_for_handshake=False) + + sync_blocks([self.nodes[0]] + [mn.node for mn in batch], timeout=60 * 2) + + # next batch should connect to previous one + cnode = batch[0].idx + + print("syncing blocks for all nodes") + sync_blocks(self.nodes, timeout=120) + + # force finishing of mnsync + for node in self.nodes: + self.force_finish_mnsync(node) + + print("activating spork15") + height = self.nodes[0].getblockchaininfo()['blocks'] + spork15_offset = 1 + self.nodes[0].spork('SPORK_15_DETERMINISTIC_MNS_ENABLED', height + spork15_offset) + self.wait_for_sporks() + self.nodes[0].generate(1) + self.sync_all() + + self.assert_mnlists(mns, False, True) + + blsPubKey = '14bc0bc4b8c9ca44bd19cf28d23fcda60ca1179ad11833ab3c46d2837fa77bd38f2d2da44ece213f3898820bc2c62553' + self.nodes[0].sendtoaddress('yfjAUKch8g9omj5isj9iVr7TZzRrjZk27s', 2000) + self.nodes[0].importprivkey('cUP3knDebUNdFDRNAivB1Qcw4SQscmQgv7hUTYnuiwk39r3wDieE') + self.nodes[0].importprivkey('cQfvBFWEA3oMsXo4XV9TLUMgnosNF5MBB2b442P5c28xyoUhY5US') + # register a MN which you can manually start and debug + self.nodes[0].protx('register_fund', 'yfjAUKch8g9omj5isj9iVr7TZzRrjZk27s', '127.0.0.1:31001', 'yQbDmr3Ad4bUwu2t4rxkHkcpSovyy8FWfM', blsPubKey, 'yQbDmr3Ad4bUwu2t4rxkHkcpSovyy8FWfM', '0', 'yfjAUKch8g9omj5isj9iVr7TZzRrjZk27s') + + for i in range(10): + self.nodes[0].generate(10) + + self.sync_all() + self.nodes[0].spork('SPORK_17_QUORUM_DKG_ENABLED', 0) + + time.sleep(10000) + + def create_mn_protx(self, node, idx, alias): + mn = Masternode() + mn.idx = idx + mn.alias = alias + mn.is_protx = True + mn.p2p_port = p2p_port(mn.idx) + mn.datadir = os.path.join(self.options.tmpdir, "node"+str(idx)) + + blsKey = node.bls('generate') + mn.ownerAddr = node.getnewaddress() + mn.operatorAddr = blsKey['public'] + mn.votingAddr = mn.ownerAddr + mn.legacyMnkey = node.masternode('genkey') + mn.blsMnkey = blsKey['secret'] + mn.collateral_address = node.getnewaddress() + + mn.collateral_txid = node.protx('register_fund', mn.collateral_address, '127.0.0.1:%d' % mn.p2p_port, mn.ownerAddr, mn.operatorAddr, mn.votingAddr, 0, mn.collateral_address) + rawtx = node.getrawtransaction(mn.collateral_txid, 1) + + mn.collateral_vout = -1 + for txout in rawtx['vout']: + if txout['value'] == Decimal(1000): + mn.collateral_vout = txout['n'] + break + assert(mn.collateral_vout != -1) + + return mn + + def start_mn(self, mn): + extra_args = ['-masternode=1', '-masternodeprivkey=%s' % mn.legacyMnkey, '-masternodeblsprivkey=%s' % mn.blsMnkey] + n = start_node(mn.idx, self.options.tmpdir, self.extra_args + extra_args, redirect_stderr=True) + self.nodes[mn.idx] = n + mn.node = self.nodes[mn.idx] + print("started %s" % mn.alias) + + def connect_mns(self, mns): + for mn in mns: + connect_nodes(self.nodes[mn.idx], 0) + + def force_finish_mnsync(self, node): + while True: + s = node.mnsync('next') + if s == 'sync updated to MASTERNODE_SYNC_FINISHED': + break + #time.sleep(0.01) + + def assert_mnlists(self, mns, include_legacy, include_protx): + for node in self.nodes: + self.assert_mnlist(node, mns, include_legacy, include_protx) + + def assert_mnlist(self, node, mns, include_legacy, include_protx): + if not self.compare_mnlist(node, mns, include_legacy, include_protx): + expected = [] + for mn in mns: + if (mn.is_protx and include_protx) or (not mn.is_protx and include_legacy): + expected.append('%s-%d' % (mn.collateral_txid, mn.collateral_vout)) + print('mnlist: ' + str(node.masternode('list', 'status'))) + print('expected: ' + str(expected)) + raise AssertionError("mnlists does not match provided mns") + + def wait_for_sporks(self, timeout=30): + st = time.time() + while time.time() < st + timeout: + if self.compare_sporks(): + return + raise AssertionError("wait_for_sporks timed out") + + def compare_sporks(self): + sporks = self.nodes[0].spork('show') + for node in self.nodes[1:]: + sporks2 = node.spork('show') + if sporks != sporks2: + return False + return True + + def compare_mnlist(self, node, mns, include_legacy, include_protx): + mnlist = node.masternode('list', 'status') + for mn in mns: + s = '%s-%d' % (mn.collateral_txid, mn.collateral_vout) + in_list = s in mnlist + + if mn.is_protx: + if include_protx: + if not in_list: + return False + else: + if in_list: + return False + else: + if include_legacy: + if not in_list: + return False + else: + if in_list: + return False + mnlist.pop(s, None) + if len(mnlist) != 0: + return False + return True + +if __name__ == '__main__': + QuorumsTest().main() diff --git a/qa/rpc-tests/test_framework/util.py b/qa/rpc-tests/test_framework/util.py index e6d204567648..5afc1e9e886f 100644 --- a/qa/rpc-tests/test_framework/util.py +++ b/qa/rpc-tests/test_framework/util.py @@ -30,7 +30,7 @@ COVERAGE_DIR = None # The maximum number of nodes a single test can spawn -MAX_NODES = 15 +MAX_NODES = 300 # Don't assign rpc or p2p ports lower than this PORT_MIN = 11000 # The number of ports to "reserve" for p2p and rpc, each @@ -425,13 +425,14 @@ def set_node_times(nodes, t): for node in nodes: node.setmocktime(t) -def connect_nodes(from_connection, node_num): +def connect_nodes(from_connection, node_num, wait_for_handshake=True): ip_port = "127.0.0.1:"+str(p2p_port(node_num)) from_connection.addnode(ip_port, "onetry") # poll until version handshake complete to avoid race conditions # with transaction relaying - while any(peer['version'] == 0 for peer in from_connection.getpeerinfo()): - time.sleep(0.1) + if wait_for_handshake: + while any(peer['version'] == 0 for peer in from_connection.getpeerinfo()): + time.sleep(0.1) def connect_nodes_bi(nodes, a, b): connect_nodes(nodes[a], b) diff --git a/src/Makefile.am b/src/Makefile.am index 5180097e16a3..f19ea1238f32 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -85,6 +85,7 @@ BITCOIN_CORE_H = \ addrman.h \ alert.h \ base58.h \ + batchedlogger.h \ bip39.h \ bip39_english.h \ blockencodings.h \ @@ -109,6 +110,7 @@ BITCOIN_CORE_H = \ core_memusage.h \ cuckoocache.h \ ctpl.h \ + cxxtimer.hpp \ evo/evodb.h \ evo/specialtx.h \ evo/providertx.h \ @@ -139,11 +141,17 @@ BITCOIN_CORE_H = \ keystore.h \ dbwrapper.h \ limitedmap.h \ - llmq/quorums_commitment.h \ + llmq/quorums.h \ llmq/quorums_blockprocessor.h \ - llmq/quorums_dummydkg.h \ - llmq/quorums_utils.h \ + llmq/quorums_commitment.h \ + llmq/quorums_debug.h \ + llmq/quorums_dkgsessionmgr.h \ + llmq/quorums_dkgsession.h \ llmq/quorums_init.h \ + llmq/quorums_instantx.h \ + llmq/quorums_signing.h \ + llmq/quorums_instantx.h \ + llmq/quorums_utils.h \ masternode.h \ masternode-payments.h \ masternode-sync.h \ @@ -226,6 +234,7 @@ libdash_server_a_SOURCES = \ addrman.cpp \ addrdb.cpp \ alert.cpp \ + batchedlogger.cpp \ bloom.cpp \ blockencodings.cpp \ chain.cpp \ @@ -248,11 +257,17 @@ libdash_server_a_SOURCES = \ governance-validators.cpp \ governance-vote.cpp \ governance-votedb.cpp \ - llmq/quorums_commitment.cpp \ + llmq/quorums.cpp \ llmq/quorums_blockprocessor.cpp \ - llmq/quorums_dummydkg.cpp \ - llmq/quorums_utils.cpp \ + llmq/quorums_commitment.cpp \ + llmq/quorums_debug.cpp \ + llmq/quorums_dkgsessionmgr.cpp \ + llmq/quorums_dkgsession.cpp \ llmq/quorums_init.cpp \ + llmq/quorums_instantx.cpp \ + llmq/quorums_signing.cpp \ + llmq/quorums_instantx.cpp \ + llmq/quorums_utils.cpp \ masternode.cpp \ masternode-payments.cpp \ masternode-sync.cpp \ @@ -279,6 +294,7 @@ libdash_server_a_SOURCES = \ rpc/net.cpp \ rpc/rawtransaction.cpp \ rpc/rpcevo.cpp \ + rpc/rpcquorums.cpp \ rpc/server.cpp \ script/sigcache.cpp \ script/ismine.cpp \ diff --git a/src/batchedlogger.cpp b/src/batchedlogger.cpp new file mode 100644 index 000000000000..c79f61b3da25 --- /dev/null +++ b/src/batchedlogger.cpp @@ -0,0 +1,25 @@ +// Copyright (c) 2018 The Dash Core developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "batchedlogger.h" +#include "util.h" + +CBatchedLogger::CBatchedLogger(const std::string& _header) : + header(_header) +{ +} + +CBatchedLogger::~CBatchedLogger() +{ + Flush(); +} + +void CBatchedLogger::Flush() +{ + if (msg.empty()) { + return; + } + LogPrintStr(strprintf("%s:\n%s", header, msg)); + msg.clear(); +} diff --git a/src/batchedlogger.h b/src/batchedlogger.h new file mode 100644 index 000000000000..3d5a067f8172 --- /dev/null +++ b/src/batchedlogger.h @@ -0,0 +1,28 @@ +// Copyright (c) 2018 The Dash Core developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef DASH_BATCHEDLOGGER_H +#define DASH_BATCHEDLOGGER_H + +#include "tinyformat.h" + +class CBatchedLogger +{ +private: + std::string header; + std::string msg; +public: + CBatchedLogger(const std::string& _header); + virtual ~CBatchedLogger(); + + template + void Printf(const std::string& fmt, const Args&... args) + { + msg += strprintf(fmt, args...); + } + + void Flush(); +}; + +#endif//DASH_BATCHEDLOGGER_H \ No newline at end of file diff --git a/src/bls/bls.h b/src/bls/bls.h index f62ab67566a4..3a903ae8aa42 100644 --- a/src/bls/bls.h +++ b/src/bls/bls.h @@ -165,13 +165,15 @@ class CBLSWrapper // } } template - inline void Unserialize(Stream& s) + inline void Unserialize(Stream& s, bool checkMalleable = true) { char buf[SerSize]; s.read((char*)buf, SerSize); SetBuf(buf, SerSize); - CheckMalleable(buf, SerSize); + if (checkMalleable) { + CheckMalleable(buf, SerSize); + } } inline void CheckMalleable(void* buf, size_t size) const diff --git a/src/chainparams.cpp b/src/chainparams.cpp index 9e6abc4c74b8..6d168ea207ea 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -118,6 +118,14 @@ static Consensus::LLMQParams llmq10_60 = { .dkgPhaseBlocks = 2, .dkgMiningWindowStart = 10, // dkgPhaseBlocks * 5 = after finalization .dkgMiningWindowEnd = 18, + .dkgRndSleepTime = 0, + .dkgBadVotesThreshold = 8, + + .signingActiveQuorumCount = 24, // a full day worth of LLMQs + + .neighborConnections = 2, + .diagonalConnections = 2, + .keepOldConnections = 24, }; static Consensus::LLMQParams llmq50_60 = { @@ -131,6 +139,14 @@ static Consensus::LLMQParams llmq50_60 = { .dkgPhaseBlocks = 2, .dkgMiningWindowStart = 10, // dkgPhaseBlocks * 5 = after finalization .dkgMiningWindowEnd = 18, + .dkgRndSleepTime = 1 * 60 * 1000, + .dkgBadVotesThreshold = 40, + + .signingActiveQuorumCount = 24, // a full day worth of LLMQs + + .neighborConnections = 2, + .diagonalConnections = 2, + .keepOldConnections = 24, }; static Consensus::LLMQParams llmq400_60 = { @@ -144,6 +160,14 @@ static Consensus::LLMQParams llmq400_60 = { .dkgPhaseBlocks = 4, .dkgMiningWindowStart = 20, // dkgPhaseBlocks * 5 = after finalization .dkgMiningWindowEnd = 28, + .dkgRndSleepTime = 2 * 60 * 1000, + .dkgBadVotesThreshold = 300, + + .signingActiveQuorumCount = 4, // two days worth of LLMQs + + .neighborConnections = 4, + .diagonalConnections = 4, + .keepOldConnections = 4, }; // Used for deployment and min-proto-version signalling, so it needs a higher threshold @@ -158,6 +182,14 @@ static Consensus::LLMQParams llmq400_85 = { .dkgPhaseBlocks = 4, .dkgMiningWindowStart = 20, // dkgPhaseBlocks * 5 = after finalization .dkgMiningWindowEnd = 48, // give it a larger mining window to make sure it is mined + .dkgRndSleepTime = 2 * 60 * 1000, + .dkgBadVotesThreshold = 300, + + .signingActiveQuorumCount = 4, // two days worth of LLMQs + + .neighborConnections = 4, + .diagonalConnections = 4, + .keepOldConnections = 4, }; @@ -595,6 +627,7 @@ class CDevNetParams : public CChainParams { consensus.llmqs[Consensus::LLMQ_50_60] = llmq50_60; consensus.llmqs[Consensus::LLMQ_400_60] = llmq400_60; consensus.llmqs[Consensus::LLMQ_400_85] = llmq400_85; + consensus.llmqForInstantSend = Consensus::LLMQ_50_60; fMiningRequiresPeers = true; fDefaultConsistencyChecks = false; @@ -753,6 +786,9 @@ class CRegTestParams : public CChainParams { // long living quorum params consensus.llmqs[Consensus::LLMQ_10_60] = llmq10_60; consensus.llmqs[Consensus::LLMQ_50_60] = llmq50_60; + consensus.llmqForInstantSend = Consensus::LLMQ_50_60; + + consensus.llmqs[Consensus::LLMQ_50_60].dkgRndSleepTime = 0; } void UpdateBIP9Parameters(Consensus::DeploymentPos d, int64_t nStartTime, int64_t nTimeout) diff --git a/src/consensus/params.h b/src/consensus/params.h index bc2ccb8a89d2..ed7bce794054 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -97,6 +97,14 @@ struct LLMQParams { // should at the same time not be too large so that not too much space is wasted with null commitments in case a DKG // session failed. int dkgMiningWindowEnd; + int dkgRndSleepTime; + int dkgBadVotesThreshold; + + int signingActiveQuorumCount; + + int neighborConnections; + int diagonalConnections; + int keepOldConnections; }; /** @@ -158,6 +166,7 @@ struct Params { std::map llmqs; bool fLLMQAllowDummyCommitments; + LLMQType llmqForInstantSend{LLMQ_NONE}; // This is temporary until we reset testnet for retesting of the full DIP3 deployment int nTemporaryTestnetForkDIP3Height{0}; diff --git a/src/cxxtimer.hpp b/src/cxxtimer.hpp new file mode 100644 index 000000000000..c3f0b72222a0 --- /dev/null +++ b/src/cxxtimer.hpp @@ -0,0 +1,184 @@ +/* + +MIT License + +Copyright (c) 2017 André L. Maravilha + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#ifndef CXX_TIMER_HPP +#define CXX_TIMER_HPP + +#include + + +namespace cxxtimer { + +/** + * This class works as a stopwatch. + */ +class Timer { + +public: + + /** + * Constructor. + * + * @param start + * If true, the timer is started just after construction. + * Otherwise, it will not be automatically started. + */ + Timer(bool start = false); + + /** + * Copy constructor. + * + * @param other + * The object to be copied. + */ + Timer(const Timer& other) = default; + + /** + * Transfer constructor. + * + * @param other + * The object to be transfered. + */ + Timer(Timer&& other) = default; + + /** + * Destructor. + */ + virtual ~Timer() = default; + + /** + * Assignment operator by copy. + * + * @param other + * The object to be copied. + * + * @return A reference to this object. + */ + Timer& operator=(const Timer& other) = default; + + /** + * Assignment operator by transfer. + * + * @param other + * The object to be transferred. + * + * @return A reference to this object. + */ + Timer& operator=(Timer&& other) = default; + + /** + * Start/resume the timer. + */ + void start(); + + /** + * Stop/pause the timer. + */ + void stop(); + + /** + * Reset the timer. + */ + void reset(); + + /** + * Return the elapsed time. + * + * @param duration_t + * The duration type used to return the time elapsed. If not + * specified, it returns the time as represented by + * std::chrono::milliseconds. + * + * @return The elapsed time. + */ + template + typename duration_t::rep count() const; + +private: + + bool started_; + bool paused_; + std::chrono::steady_clock::time_point reference_; + std::chrono::duration accumulated_; +}; + +} + + +inline cxxtimer::Timer::Timer(bool start) : + started_(false), paused_(false), + reference_(std::chrono::steady_clock::now()), + accumulated_(std::chrono::duration(0)) { + if (start) { + this->start(); + } +} + +inline void cxxtimer::Timer::start() { + if (!started_) { + started_ = true; + paused_ = false; + accumulated_ = std::chrono::duration(0); + reference_ = std::chrono::steady_clock::now(); + } else if (paused_) { + reference_ = std::chrono::steady_clock::now(); + paused_ = false; + } +} + +inline void cxxtimer::Timer::stop() { + if (started_ && !paused_) { + std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); + accumulated_ = accumulated_ + std::chrono::duration_cast< std::chrono::duration >(now - reference_); + paused_ = true; + } +} + +inline void cxxtimer::Timer::reset() { + if (started_) { + started_ = false; + paused_ = false; + reference_ = std::chrono::steady_clock::now(); + accumulated_ = std::chrono::duration(0); + } +} + +template +typename duration_t::rep cxxtimer::Timer::count() const { + if (started_) { + if (paused_) { + return std::chrono::duration_cast(accumulated_).count(); + } else { + return std::chrono::duration_cast( + accumulated_ + (std::chrono::steady_clock::now() - reference_)).count(); + } + } else { + return duration_t(0).count(); + } +} + + +#endif diff --git a/src/dsnotificationinterface.cpp b/src/dsnotificationinterface.cpp index 625641d01110..0a7f7f05fd53 100644 --- a/src/dsnotificationinterface.cpp +++ b/src/dsnotificationinterface.cpp @@ -10,14 +10,14 @@ #include "masternode-payments.h" #include "masternode-sync.h" #include "privatesend.h" +#include "llmq/quorums.h" +#include "llmq/quorums_dkgsessionmgr.h" #ifdef ENABLE_WALLET #include "privatesend-client.h" #endif // ENABLE_WALLET #include "evo/deterministicmns.h" -#include "llmq/quorums_dummydkg.h" - void CDSNotificationInterface::InitializeCurrentBlockTip() { LOCK(cs_main); @@ -40,7 +40,6 @@ void CDSNotificationInterface::UpdatedBlockTip(const CBlockIndex *pindexNew, con return; deterministicMNManager->UpdatedBlockTip(pindexNew); - llmq::quorumDummyDKG->UpdatedBlockTip(pindexNew, fInitialDownload); masternodeSync.UpdatedBlockTip(pindexNew, fInitialDownload, connman); @@ -64,6 +63,8 @@ void CDSNotificationInterface::UpdatedBlockTip(const CBlockIndex *pindexNew, con instantsend.UpdatedBlockTip(pindexNew); mnpayments.UpdatedBlockTip(pindexNew, connman); governance.UpdatedBlockTip(pindexNew, connman); + llmq::quorumManager->UpdatedBlockTip(pindexNew, pindexFork, fInitialDownload); + llmq::quorumDKGSessionManager->UpdatedBlockTip(pindexNew, pindexFork, fInitialDownload); } void CDSNotificationInterface::SyncTransaction(const CTransaction &tx, const CBlockIndex *pindex, int posInBlock) diff --git a/src/init.cpp b/src/init.cpp index 118700cf6e86..a607936f4c4f 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -67,6 +67,8 @@ #include "warnings.h" #include "evo/deterministicmns.h" +#include "llmq/quorums_init.h" +#include "llmq/quorums.h" #include "llmq/quorums_init.h" @@ -549,6 +551,7 @@ std::string HelpMessage(HelpMessageMode mode) strUsage += HelpMessageOpt("-limitdescendantcount=", strprintf("Do not accept transactions if any ancestor would have or more in-mempool descendants (default: %u)", DEFAULT_DESCENDANT_LIMIT)); strUsage += HelpMessageOpt("-limitdescendantsize=", strprintf("Do not accept transactions if any ancestor would have more than kilobytes of in-mempool descendants (default: %u).", DEFAULT_DESCENDANT_SIZE_LIMIT)); strUsage += HelpMessageOpt("-bip9params=deployment:start:end", "Use given start/end times for specified BIP9 deployment (regtest-only)"); + strUsage += HelpMessageOpt("-watchquorums=", strprintf("Watch and validate quorum communication (default: %u)", llmq::DEFAULT_WATCH_QUORUMS)); } std::string debugCategories = "addrman, alert, bench, cmpctblock, coindb, db, http, leveldb, libevent, lock, mempool, mempoolrej, net, proxy, prune, rand, reindex, rpc, selectcoins, tor, zmq, " "dash (or specifically: gobject, instantsend, keepass, masternode, mnpayments, mnsync, privatesend, spork)"; // Don't translate these and qt below @@ -1726,7 +1729,7 @@ bool AppInitMain(boost::thread_group& threadGroup, CScheduler& scheduler) pcoinsdbview = new CCoinsViewDB(nCoinDBCache, false, fReindex || fReindexChainState); pcoinscatcher = new CCoinsViewErrorCatcher(pcoinsdbview); pcoinsTip = new CCoinsViewCache(pcoinscatcher); - llmq::InitLLMQSystem(*evoDb); + llmq::InitLLMQSystem(*evoDb, &scheduler); if (fReindex) { pblocktree->WriteReindexing(true); diff --git a/src/llmq/quorums.cpp b/src/llmq/quorums.cpp new file mode 100644 index 000000000000..e6671499c9aa --- /dev/null +++ b/src/llmq/quorums.cpp @@ -0,0 +1,391 @@ +// Copyright (c) 2018 The Dash Core developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "quorums.h" +#include "quorums_commitment.h" +#include "quorums_dkgsession.h" +#include "quorums_dkgsessionmgr.h" +#include "quorums_utils.h" +#include "quorums_blockprocessor.h" + +#include "evo/specialtx.h" + +#include "validation.h" +#include "univalue.h" +#include "chainparams.h" +#include "activemasternode.h" + +#include "cxxtimer.hpp" + +namespace llmq +{ + +static const std::string DB_QUORUM_SK_SHARE = "q_Qsk"; +static const std::string DB_QUORUM_QUORUM_VVEC = "q_Qqvvec"; + +CQuorumManager* quorumManager; + +static uint256 MakeQuorumKey(const CQuorum& q) +{ + CHashWriter hw(SER_NETWORK, 0); + hw << (uint8_t)q.params.type; + hw << q.quorumHash; + for (const auto& dmn : q.members) { + hw << dmn->proTxHash; + } + return hw.GetHash(); +} + +CQuorum::~CQuorum() +{ + // most likely the thread is already done + stopCachePopulatorThread = true; + if (cachePopulatorThread.joinable()) { + cachePopulatorThread.join(); + } +} + +void CQuorum::Init(const uint256& _quorumHash, int _height, const std::vector& _members, const std::vector& _validMembers, const CBLSPublicKey& _quorumPublicKey) +{ + quorumHash = _quorumHash; + height = _height; + members = _members; + validMembers = _validMembers; + quorumPublicKey = _quorumPublicKey; +} + +bool CQuorum::IsMember(const uint256& proTxHash) const +{ + for (auto& dmn : members) { + if (dmn->proTxHash == proTxHash) { + return true; + } + } + return false; +} + +bool CQuorum::IsValidMember(const uint256& proTxHash) const +{ + for (size_t i = 0; i < members.size(); i++) { + if (members[i]->proTxHash == proTxHash) { + return validMembers[i]; + } + } + return false; +} + +CBLSPublicKey CQuorum::GetPubKeyShare(size_t memberIdx) const +{ + if (quorumVvec == nullptr || memberIdx >= members.size() || !validMembers[memberIdx]) { + return CBLSPublicKey(); + } + auto& m = members[memberIdx]; + return blsCache.BuildPubKeyShare(m->proTxHash, quorumVvec, CBLSId::FromHash(m->proTxHash)); +} + +CBLSSecretKey CQuorum::GetSkShare() const +{ + return skShare; +} + +int CQuorum::GetMemberIndex(const uint256& proTxHash) const +{ + for (size_t i = 0; i < members.size(); i++) { + if (members[i]->proTxHash == proTxHash) { + return (int)i; + } + } + return -1; +} + +bool CQuorum::WriteContributions(CEvoDB& evoDb) +{ + uint256 dbKey = MakeQuorumKey(*this); + + if (quorumVvec != nullptr) { + evoDb.GetRawDB().Write(std::make_pair(DB_QUORUM_QUORUM_VVEC, dbKey), *quorumVvec); + } + if (skShare.IsValid()) { + evoDb.GetRawDB().Write(std::make_pair(DB_QUORUM_SK_SHARE, dbKey), skShare); + } + return true; +} + +bool CQuorum::ReadContributions(CEvoDB& evoDb) +{ + uint256 dbKey = MakeQuorumKey(*this); + + BLSVerificationVector qv; + if (evoDb.Read(std::make_pair(DB_QUORUM_QUORUM_VVEC, dbKey), qv)) { + quorumVvec = std::make_shared(std::move(qv)); + } else { + return false; + } + + evoDb.Read(std::make_pair(DB_QUORUM_SK_SHARE, dbKey), skShare); + + return true; +} + +void CQuorum::StartCachePopulatorThread(std::shared_ptr _this) +{ + if (_this->quorumVvec == nullptr) { + return; + } + + cxxtimer::Timer t(true); + LogPrintf("CQuorum::StartCachePopulatorThread -- start\n"); + + // this thread will exit after some time + // when then later some other thread tries to get keys, it will be much faster + _this->cachePopulatorThread = std::thread([_this, t]() { + RenameThread("quorum-cachepop"); + for (size_t i = 0; i < _this->members.size() && !_this->stopCachePopulatorThread; i++) { + if (_this->validMembers[i]) { + _this->GetPubKeyShare(i); + } + } + LogPrintf("CQuorum::StartCachePopulatorThread -- done. time=%d\n", t.count()); + }); +} + +CQuorumManager::CQuorumManager(CEvoDB& _evoDb, CBLSWorker& _blsWorker, CDKGSessionManager& _dkgManager) : + evoDb(_evoDb), + blsWorker(_blsWorker), + dkgManager(_dkgManager) +{ +} + +void CQuorumManager::UpdatedBlockTip(const CBlockIndex* pindexNew, const CBlockIndex* pindexFork, bool fInitialDownload) +{ + if (fInitialDownload) { + return; + } + + LOCK(cs_main); + + for (auto& p : Params().GetConsensus().llmqs) { + EnsureQuorumConnections(p.first, pindexNew); + } +} + +void CQuorumManager::EnsureQuorumConnections(Consensus::LLMQType llmqType, const CBlockIndex* pindexNew) +{ + AssertLockHeld(cs_main); + + const auto& params = Params().GetConsensus().llmqs.at(llmqType); + + auto myProTxHash = activeMasternodeInfo.proTxHash; + auto lastQuorums = ScanQuorums(llmqType, (size_t)params.keepOldConnections); + + auto connmanQuorumsToDelete = g_connman->GetMasternodeQuorums(llmqType); + + // don't remove connections for the currently in-progress DKG round + int curDkgHeight = pindexNew->nHeight - (pindexNew->nHeight % params.dkgInterval); + auto curDkgBlock = chainActive[curDkgHeight]->GetBlockHash(); + connmanQuorumsToDelete.erase(curDkgBlock); + + for (auto& quorum : lastQuorums) { + if (!quorum->IsMember(myProTxHash) && !GetBoolArg("-watchquorums", DEFAULT_WATCH_QUORUMS)) { + continue; + } + + if (!g_connman->HasMasternodeQuorumNodes(llmqType, quorum->quorumHash)) { + std::set connections; + if (quorum->IsMember(myProTxHash)) { + connections = CLLMQUtils::GetQuorumConnections(llmqType, quorum->quorumHash, myProTxHash); + } else { + auto cindexes = CLLMQUtils::CalcDeterministicWatchConnections(llmqType, quorum->quorumHash, quorum->members.size(), 1); + for (auto idx : cindexes) { + connections.emplace(quorum->members[idx]->pdmnState->addr); + } + } + if (!connections.empty()) { + std::string debugMsg = strprintf("CQuorumManager::%s -- adding masternodes quorum connections for quorum %s:\n", __func__, quorum->quorumHash.ToString()); + for (auto& c : connections) { + debugMsg += strprintf(" %s\n", c.ToString(false)); + } + LogPrintf(debugMsg); + g_connman->AddMasternodeQuorumNodes(llmqType, quorum->quorumHash, connections); + } + } + connmanQuorumsToDelete.erase(quorum->quorumHash); + } + + for (auto& qh : connmanQuorumsToDelete) { + LogPrintf("CQuorumManager::%s -- removing masternodes quorum connections for quorum %s:\n", __func__, qh.ToString()); + g_connman->RemoveMasternodeQuorumNodes(llmqType, qh); + } +} + +bool CQuorumManager::BuildQuorumFromCommitment(const CFinalCommitment& qc, std::shared_ptr& quorum) const +{ + AssertLockHeld(cs_main); + + if (!mapBlockIndex.count(qc.quorumHash)) { + LogPrintf("CQuorumManager::%s -- block %s not found", __func__, qc.quorumHash.ToString()); + return false; + } + auto& params = Params().GetConsensus().llmqs.at((Consensus::LLMQType)qc.llmqType); + auto quorumIndex = mapBlockIndex[qc.quorumHash]; + auto members = CLLMQUtils::GetAllQuorumMembers((Consensus::LLMQType)qc.llmqType, qc.quorumHash); + + quorum->Init(qc.quorumHash, quorumIndex->nHeight, members, qc.validMembers, qc.quorumPublicKey); + + if (!quorum->ReadContributions(evoDb)) { + if (BuildQuorumContributions(qc, quorum)) { + quorum->WriteContributions(evoDb); + } else { + LogPrintf("CQuorumManager::%s -- quorum.ReadContributions and BuildQuorumContributions for block %s failed", __func__, qc.quorumHash.ToString()); + } + } + + // 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 + CQuorum::StartCachePopulatorThread(quorum); + + return true; +} + +bool CQuorumManager::BuildQuorumContributions(const CFinalCommitment& fqc, std::shared_ptr& quorum) const +{ + std::vector memberIndexes; + std::vector vvecs; + BLSSecretKeyVector skContributions; + if (!dkgManager.GetVerifiedContributions((Consensus::LLMQType)fqc.llmqType, fqc.quorumHash, fqc.validMembers, memberIndexes, vvecs, skContributions)) { + return false; + } + + BLSVerificationVectorPtr quorumVvec; + CBLSSecretKey skShare; + + cxxtimer::Timer t2(true); + quorumVvec = blsWorker.BuildQuorumVerificationVector(vvecs); + if (quorumVvec == nullptr) { + LogPrintf("CQuorumManager::%s -- failed to build quorumVvec\n", __func__); + } + skShare = blsWorker.AggregateSecretKeys(skContributions); + if (!skShare.IsValid()) { + LogPrintf("CQuorumManager::%s -- failed to build skShare\n", __func__); + } + t2.stop(); + + LogPrintf("CQuorumManager::%s -- built quorum vvec and skShare. time=%d\n", __func__, t2.count()); + + quorum->quorumVvec = quorumVvec; + quorum->skShare = skShare; + + return true; +} + +bool CQuorumManager::HasQuorum(Consensus::LLMQType llmqType, const uint256& quorumHash) +{ + return quorumBlockProcessor->HasMinedCommitment(llmqType, quorumHash); +} + +std::vector CQuorumManager::ScanQuorums(Consensus::LLMQType llmqType, size_t maxCount) +{ + LOCK(cs_main); + return ScanQuorums(llmqType, chainActive.Tip()->GetBlockHash(), maxCount); +} + +std::vector CQuorumManager::ScanQuorums(Consensus::LLMQType llmqType, const uint256& startBlock, size_t maxCount) +{ + std::vector result; + + LOCK(cs_main); + if (!mapBlockIndex.count(startBlock)) { + return result; + } + + result.reserve(maxCount); + + CBlockIndex* pindex = mapBlockIndex[startBlock]; + + while (pindex != NULL && result.size() < maxCount && deterministicMNManager->IsDeterministicMNsSporkActive(pindex->nHeight)) { + if (HasQuorum(llmqType, pindex->GetBlockHash())) { + auto quorum = GetQuorum(llmqType, pindex->GetBlockHash()); + if (quorum) { + result.emplace_back(quorum); + } + } + + // TODO speedup (skip blocks where no quorums could have been mined) + pindex = pindex->pprev; + } + + return result; +} + +CQuorumCPtr CQuorumManager::SelectQuorum(Consensus::LLMQType llmqType, const uint256& selectionHash, size_t poolSize) +{ + LOCK(cs_main); + return SelectQuorum(llmqType, chainActive.Tip()->GetBlockHash(), selectionHash, poolSize); +} + +CQuorumCPtr CQuorumManager::SelectQuorum(Consensus::LLMQType llmqType, const uint256& startBlock, const uint256& selectionHash, size_t poolSize) +{ + auto quorums = ScanQuorums(llmqType, startBlock, poolSize); + if (quorums.empty()) { + return nullptr; + } + + std::vector> scores; + scores.reserve(quorums.size()); + for (size_t i = 0; i < quorums.size(); i++) { + CHashWriter h(SER_NETWORK, 0); + h << (uint8_t)llmqType; + h << quorums[i]->quorumHash; + h << selectionHash; + scores.emplace_back(h.GetHash(), i); + } + std::sort(scores.begin(), scores.end()); + return quorums[scores.front().second]; +} + +CQuorumCPtr CQuorumManager::GetQuorum(Consensus::LLMQType llmqType, const uint256& quorumHash) +{ + AssertLockHeld(cs_main); + + // we must check this before we look into the cache. Reorgs might have happened which would mean we might have + // cached quorums which are not in the active chain anymore + if (!HasQuorum(llmqType, quorumHash)) { + return nullptr; + } + + LOCK(quorumsCacheCs); + + auto it = quorumsCache.find(std::make_pair(llmqType, quorumHash)); + if (it != quorumsCache.end()) { + return it->second; + } + + CFinalCommitment qc; + if (!quorumBlockProcessor->GetMinedCommitment(llmqType, quorumHash, qc)) { + return nullptr; + } + + auto& params = Params().GetConsensus().llmqs.at(llmqType); + + auto quorum = std::make_shared(params, blsWorker); + if (!BuildQuorumFromCommitment(qc, quorum)) { + return nullptr; + } + + quorumsCache.emplace(std::make_pair(llmqType, quorumHash), quorum); + + return quorum; +} + +CQuorumCPtr CQuorumManager::GetNewestQuorum(Consensus::LLMQType llmqType) +{ + auto quorums = ScanQuorums(llmqType, 1); + if (quorums.empty()) { + return nullptr; + } + return quorums.front(); +} + +} diff --git a/src/llmq/quorums.h b/src/llmq/quorums.h new file mode 100644 index 000000000000..af17cd59f45e --- /dev/null +++ b/src/llmq/quorums.h @@ -0,0 +1,100 @@ +// Copyright (c) 2018 The Dash Core developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef DASH_QUORUMS_H +#define DASH_QUORUMS_H + +#include "evo/evodb.h" +#include "evo/deterministicmns.h" + +#include "validationinterface.h" +#include "consensus/params.h" + +#include "bls/bls.h" +#include "bls/bls_worker.h" + +namespace llmq +{ + +// If true, we will connect to all new quorums and watch their communication +static const bool DEFAULT_WATCH_QUORUMS = false; + +class CDKGSessionManager; + +class CQuorum +{ +public: + const Consensus::LLMQParams& params; + uint256 quorumHash; + int height; + std::vector members; + std::vector validMembers; + CBLSPublicKey quorumPublicKey; + + // These are only valid when we either participated in the DKG or fully watched it + BLSVerificationVectorPtr quorumVvec; + CBLSSecretKey skShare; + +private: + mutable CBLSWorkerCache blsCache; + std::atomic stopCachePopulatorThread; + std::thread cachePopulatorThread; + +public: + CQuorum(const Consensus::LLMQParams& _params, CBLSWorker& _blsWorker) : params(_params), blsCache(_blsWorker), stopCachePopulatorThread(false) {} + ~CQuorum(); + void Init(const uint256& quorumHash, int height, const std::vector& members, const std::vector& validMembers, const CBLSPublicKey& quorumPublicKey); + + bool IsMember(const uint256& proTxHash) const; + bool IsValidMember(const uint256& proTxHash) const; + int GetMemberIndex(const uint256& proTxHash) const; + + CBLSPublicKey GetPubKeyShare(size_t memberIdx) const; + CBLSSecretKey GetSkShare() const; + + bool WriteContributions(CEvoDB& evoDb); + bool ReadContributions(CEvoDB& evoDb); + static void StartCachePopulatorThread(std::shared_ptr _this); +}; +typedef std::shared_ptr CQuorumPtr; +typedef std::shared_ptr CQuorumCPtr; + +class CQuorumManager +{ +private: + CEvoDB& evoDb; + CBLSWorker& blsWorker; + CDKGSessionManager& dkgManager; + + CCriticalSection quorumsCacheCs; + std::map, CQuorumPtr> quorumsCache; + +public: + CQuorumManager(CEvoDB& _evoDb, CBLSWorker& _blsWorker, CDKGSessionManager& _dkgManager); + + void UpdatedBlockTip(const CBlockIndex *pindexNew, const CBlockIndex *pindexFork, bool fInitialDownload); + +public: + bool HasQuorum(Consensus::LLMQType llmqType, const uint256& quorumHash); + + // all these methods will lock cs_main + CQuorumCPtr GetQuorum(Consensus::LLMQType llmqType,const uint256& quorumHash); + CQuorumCPtr GetNewestQuorum(Consensus::LLMQType llmqType); + std::vector ScanQuorums(Consensus::LLMQType llmqType, size_t maxCount); + std::vector ScanQuorums(Consensus::LLMQType llmqType, const uint256& startBlock, size_t maxCount); + CQuorumCPtr SelectQuorum(Consensus::LLMQType llmqType, const uint256& selectionHash, size_t poolSize); + CQuorumCPtr SelectQuorum(Consensus::LLMQType llmqType, const uint256& startBlock, const uint256& selectionHash, size_t poolSize); + +private: + void EnsureQuorumConnections(Consensus::LLMQType llmqType, const CBlockIndex *pindexNew); + + bool BuildQuorumFromCommitment(const CFinalCommitment& qc, std::shared_ptr& quorum) const; + bool BuildQuorumContributions(const CFinalCommitment& fqc, std::shared_ptr& quorum) const; +}; + +extern CQuorumManager* quorumManager; + +} + +#endif //DASH_QUORUMS_H diff --git a/src/llmq/quorums_commitment.h b/src/llmq/quorums_commitment.h index de5ccbf2deb0..9ca4522b3222 100644 --- a/src/llmq/quorums_commitment.h +++ b/src/llmq/quorums_commitment.h @@ -32,8 +32,8 @@ class CFinalCommitment CBLSPublicKey quorumPublicKey; uint256 quorumVvecHash; - CBLSSignature quorumSig; // recovered threshold sig of blockHash+validMembers+pubKeyHash+vvecHash - CBLSSignature membersSig; // aggregated member sig of blockHash+validMembers+pubKeyHash+vvecHash + CBLSSignature quorumSig; // recovered threshold sig of quorumHash+validMembers+pubKeyHash+vvecHash + CBLSSignature membersSig; // aggregated member sig of quorumHash+validMembers+pubKeyHash+vvecHash public: CFinalCommitment() {} diff --git a/src/llmq/quorums_debug.cpp b/src/llmq/quorums_debug.cpp new file mode 100644 index 000000000000..5304f2ca0579 --- /dev/null +++ b/src/llmq/quorums_debug.cpp @@ -0,0 +1,347 @@ +// Copyright (c) 2018 The Dash Core developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "quorums_debug.h" + +#include "activemasternode.h" +#include "chainparams.h" +#include "net.h" +#include "net_processing.h" +#include "scheduler.h" +#include "validation.h" + +#include "evo/deterministicmns.h" +#include "quorums_utils.h" + +namespace llmq +{ +CDKGDebugManager* quorumDKGDebugManager; + +UniValue CDKGDebugSessionStatus::ToJson(int detailLevel) const +{ + UniValue ret(UniValue::VOBJ); + + if (!Params().GetConsensus().llmqs.count((Consensus::LLMQType)llmqType) || quorumHash.IsNull()) { + return ret; + } + + std::vector dmnMembers; + if (detailLevel == 2) { + dmnMembers = CLLMQUtils::GetAllQuorumMembers((Consensus::LLMQType) llmqType, quorumHash); + } + + ret.push_back(Pair("llmqType", llmqType)); + ret.push_back(Pair("quorumHash", quorumHash.ToString())); + ret.push_back(Pair("quorumHeight", (int)quorumHeight)); + ret.push_back(Pair("phase", (int)phase)); + + ret.push_back(Pair("sentContributions", sentContributions)); + ret.push_back(Pair("sentComplaint", sentComplaint)); + ret.push_back(Pair("sentJustification", sentJustification)); + ret.push_back(Pair("sentPrematureCommitment", sentPrematureCommitment)); + ret.push_back(Pair("aborted", aborted)); + + struct ArrOrCount { + int count{0}; + UniValue arr{UniValue::VARR}; + }; + + ArrOrCount badMembers; + ArrOrCount weComplain; + ArrOrCount receivedContributions; + ArrOrCount receivedComplaints; + ArrOrCount receivedJustifications; + ArrOrCount receivedPrematureCommitments; + ArrOrCount complaintsFromMembers; + + auto add = [&](ArrOrCount& v, size_t idx, bool flag) { + if (flag) { + if (detailLevel == 0) { + v.count++; + } else if (detailLevel == 1) { + v.arr.push_back((int)idx); + } else if (detailLevel == 2) { + UniValue a(UniValue::VOBJ); + a.push_back(Pair("memberIndex", idx)); + if (idx < dmnMembers.size()) { + a.push_back(Pair("proTxHash", dmnMembers[idx]->proTxHash.ToString())); + } + v.arr.push_back(a); + } + } + }; + auto push = [&](ArrOrCount& v, const std::string& name) { + if (detailLevel == 0) { + ret.push_back(Pair(name, v.count)); + } else { + ret.push_back(Pair(name, v.arr)); + } + }; + + for (size_t i = 0; i < members.size(); i++) { + const auto& m = members[i]; + add(badMembers, i, m.bad); + add(weComplain, i, m.weComplain); + add(receivedContributions, i, m.receivedContribution); + add(receivedComplaints, i, m.receivedComplaint); + add(receivedJustifications, i, m.receivedJustification); + add(receivedPrematureCommitments, i, m.receivedPrematureCommitment); + } + push(badMembers, "badMembers"); + push(weComplain, "weComplain"); + push(receivedContributions, "receivedContributions"); + push(receivedComplaints, "receivedComplaints"); + push(receivedJustifications, "receivedJustifications"); + push(receivedPrematureCommitments, "receivedPrematureCommitments"); + + if (detailLevel == 2) { + UniValue arr(UniValue::VARR); + for (const auto& dmn : dmnMembers) { + arr.push_back(dmn->proTxHash.ToString()); + } + ret.push_back(Pair("allMembers", arr)); + } + + return ret; +} + +CDKGDebugManager::CDKGDebugManager(CScheduler* scheduler) +{ + for (const auto& p : Params().GetConsensus().llmqs) { + ResetLocalSessionStatus(p.first, uint256(), 0); + } + + if (scheduler) { + scheduler->scheduleEvery([&]() { + SendLocalStatus(); + }, 10); + } +} + +void CDKGDebugManager::ProcessMessage(CNode* pfrom, const std::string& strCommand, CDataStream& vRecv, CConnman& connman) +{ + if (strCommand == NetMsgType::QDEBUGSTATUS) { + CDKGDebugStatus status; + vRecv >> status; + + { + LOCK(cs_main); + connman.RemoveAskFor(::SerializeHash(status)); + } + + ProcessDebugStatusMessage(pfrom->id, status); + } +} + +void CDKGDebugManager::ProcessDebugStatusMessage(NodeId nodeId, llmq::CDKGDebugStatus& status) +{ + auto dmn = deterministicMNManager->GetListAtChainTip().GetMN(status.proTxHash); + if (!dmn) { + if (nodeId != -1) { + LOCK(cs_main); + Misbehaving(nodeId, 10); + } + return; + } + + { + LOCK(cs); + auto it = statusesForMasternodes.find(status.proTxHash); + if (it != statusesForMasternodes.end()) { + if (statuses[it->second].nTime >= status.nTime) { + // we know a more recent status already + return; + } + } + } + + // check if all expected LLMQ types are present and valid + std::set llmqTypes; + for (const auto& p : status.sessions) { + if (!Params().GetConsensus().llmqs.count((Consensus::LLMQType)p.first)) { + if (nodeId != -1) { + LOCK(cs_main); + Misbehaving(nodeId, 10); + } + return; + } + const auto& params = Params().GetConsensus().llmqs.at((Consensus::LLMQType)p.first); + if (p.second.llmqType != p.first || p.second.members.size() != (size_t)params.size) { + if (nodeId != -1) { + LOCK(cs_main); + Misbehaving(nodeId, 10); + } + return; + } + llmqTypes.emplace((Consensus::LLMQType)p.first); + } + for (const auto& p : Params().GetConsensus().llmqs) { + if (!llmqTypes.count(p.first)) { + if (nodeId != -1) { + LOCK(cs_main); + Misbehaving(nodeId, 10); + } + return; + } + } + + // TODO batch verification/processing + if (!status.sig.VerifyInsecure(dmn->pdmnState->pubKeyOperator, status.GetSignHash())) { + if (nodeId != -1) { + LOCK(cs_main); + Misbehaving(nodeId, 10); + } + return; + } + + LOCK(cs); + auto it = statusesForMasternodes.find(status.proTxHash); + if (it != statusesForMasternodes.end()) { + statuses.erase(it->second); + statusesForMasternodes.erase(it); + } + + auto hash = ::SerializeHash(status); + + statuses[hash] = status; + statusesForMasternodes[status.proTxHash] = hash; + + CInv inv(MSG_QUORUM_DEBUG_STATUS, hash); + g_connman->RelayInv(inv, DMN_PROTO_VERSION); +} + +UniValue CDKGDebugStatus::ToJson(int detailLevel) const +{ + UniValue ret(UniValue::VOBJ); + + ret.push_back(Pair("proTxHash", proTxHash.ToString())); + ret.push_back(Pair("height", (int)nHeight)); + ret.push_back(Pair("time", nTime)); + ret.push_back(Pair("timeStr", DateTimeStrFormat("%Y-%m-%d %H:%M:%S", nTime))); + + UniValue sessionsJson(UniValue::VOBJ); + for (const auto& p : sessions) { + if (!Params().GetConsensus().llmqs.count((Consensus::LLMQType)p.first)) { + continue; + } + const auto& params = Params().GetConsensus().llmqs.at((Consensus::LLMQType)p.first); + sessionsJson.push_back(Pair(params.name, p.second.ToJson(detailLevel))); + } + + ret.push_back(Pair("session", sessionsJson)); + + return ret; +} + +bool CDKGDebugManager::AlreadyHave(const CInv& inv) +{ + LOCK(cs); + return statuses.count(inv.hash) != 0; +} + +bool CDKGDebugManager::GetDebugStatus(const uint256& hash, llmq::CDKGDebugStatus& ret) +{ + LOCK(cs); + auto it = statuses.find(hash); + if (it == statuses.end()) { + return false; + } + ret = it->second; + return true; +} + +bool CDKGDebugManager::GetDebugStatusForMasternode(const uint256& proTxHash, llmq::CDKGDebugStatus& ret) +{ + LOCK(cs); + auto it = statusesForMasternodes.find(proTxHash); + if (it == statusesForMasternodes.end()) { + return false; + } + ret = statuses.at(it->second); + return true; +} + +void CDKGDebugManager::GetLocalDebugStatus(llmq::CDKGDebugStatus& ret) +{ + LOCK(cs); + ret = localStatus; + ret.proTxHash = activeMasternodeInfo.proTxHash; +} + +void CDKGDebugManager::ResetLocalSessionStatus(Consensus::LLMQType llmqType, const uint256& quorumHash, int quorumHeight) +{ + LOCK(cs); + + auto& params = Params().GetConsensus().llmqs.at(llmqType); + + localStatus.nTime = GetAdjustedTime(); + + auto& session = localStatus.sessions[llmqType]; + + session.llmqType = llmqType; + session.quorumHash = quorumHash; + session.quorumHeight = (uint32_t)quorumHeight; + session.phase = 0; + session.statusBitset = 0; + session.members.clear(); + session.members.resize((size_t)params.size); +} + +void CDKGDebugManager::UpdateLocalStatus(std::function&& func) +{ + LOCK(cs); + if (func(localStatus)) { + localStatus.nTime = GetAdjustedTime(); + } +} + +void CDKGDebugManager::UpdateLocalSessionStatus(Consensus::LLMQType llmqType, std::function&& func) +{ + LOCK(cs); + if (func(localStatus.sessions.at(llmqType))) { + localStatus.nTime = GetAdjustedTime(); + } +} + +void CDKGDebugManager::UpdateLocalMemberStatus(Consensus::LLMQType llmqType, size_t memberIdx, std::function&& func) +{ + LOCK(cs); + if (func(localStatus.sessions.at(llmqType).members.at(memberIdx))) { + localStatus.nTime = GetAdjustedTime(); + } +} + +void CDKGDebugManager::SendLocalStatus() +{ + if (!fMasternodeMode) { + return; + } + if (activeMasternodeInfo.proTxHash.IsNull()) { + return; + } + + CDKGDebugStatus status; + { + LOCK(cs); + status = localStatus; + } + + int64_t nTime = status.nTime; + status.proTxHash = activeMasternodeInfo.proTxHash; + status.nTime = 0; + status.sig = CBLSSignature(); + + uint256 newHash = ::SerializeHash(status); + if (newHash == lastStatusHash) { + return; + } + lastStatusHash = newHash; + + status.nTime = nTime; + status.sig = activeMasternodeInfo.blsKeyOperator->Sign(status.GetSignHash()); + + ProcessDebugStatusMessage(-1, status); +} + +} diff --git a/src/llmq/quorums_debug.h b/src/llmq/quorums_debug.h new file mode 100644 index 000000000000..1e5e333a1956 --- /dev/null +++ b/src/llmq/quorums_debug.h @@ -0,0 +1,178 @@ +// Copyright (c) 2018 The Dash Core developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef DASH_QUORUMS_DEBUG_H +#define DASH_QUORUMS_DEBUG_H + +#include "consensus/params.h" +#include "serialize.h" +#include "bls/bls.h" +#include "sync.h" +#include "univalue.h" +#include "net.h" + +class CDataStream; +class CInv; +class CScheduler; + +namespace llmq +{ + +class CDKGDebugMemberStatus +{ +public: + union { + struct + { + // is it locally considered as bad (and thus removed from the validMembers set) + bool bad : 1; + // did we complain about this member + bool weComplain : 1; + + // received message for DKG phases + bool receivedContribution : 1; + bool receivedComplaint : 1; + bool receivedJustification : 1; + bool receivedPrematureCommitment : 1; + }; + uint16_t statusBitset; + }; + + std::set complaintsFromMembers; + +public: + CDKGDebugMemberStatus() : statusBitset(0) {} + +public: + ADD_SERIALIZE_METHODS + + template + inline void SerializationOp(Stream& s, Operation ser_action) + { + READWRITE(statusBitset); + READWRITE(complaintsFromMembers); + } +}; + +class CDKGDebugSessionStatus +{ +public: + uint8_t llmqType{Consensus::LLMQ_NONE}; + uint256 quorumHash; + uint32_t quorumHeight{0}; + uint8_t phase{0}; + + union { + struct + { + // sent messages for DKG phases + bool sentContributions : 1; + bool sentComplaint : 1; + bool sentJustification : 1; + bool sentPrematureCommitment : 1; + + bool aborted : 1; + }; + uint16_t statusBitset; + }; + + std::vector members; + +public: + CDKGDebugSessionStatus() : statusBitset(0) {} + +public: + ADD_SERIALIZE_METHODS + + template + inline void SerializationOp(Stream& s, Operation ser_action) + { + READWRITE(llmqType); + READWRITE(quorumHash); + READWRITE(quorumHeight); + READWRITE(phase); + READWRITE(statusBitset); + READWRITE(members); + } + + UniValue ToJson(int detailLevel) const; +}; + +class CDKGDebugStatus +{ +public: + uint256 proTxHash; + int64_t nTime{0}; + uint32_t nHeight{0}; + + std::map sessions; + + CBLSSignature sig; + +public: + ADD_SERIALIZE_METHODS + + template + inline void SerializationOpWithoutSig(Stream& s, Operation ser_action) + { + READWRITE(proTxHash); + READWRITE(nTime); + READWRITE(nHeight); + READWRITE(sessions); + } + + template + inline void SerializationOp(Stream& s, Operation ser_action) + { + SerializationOpWithoutSig(s, ser_action); + READWRITE(sig); + } + + uint256 GetSignHash() const + { + CHashWriter hw(SER_GETHASH, 0); + NCONST_PTR(this)->SerializationOpWithoutSig(hw, CSerActionSerialize()); + hw << CBLSSignature(); + return hw.GetHash(); + } + + UniValue ToJson(int detailLevel) const; +}; + +class CDKGDebugManager +{ +private: + CCriticalSection cs; + + std::map statuses; + std::map statusesForMasternodes; + + CDKGDebugStatus localStatus; + uint256 lastStatusHash; + +public: + CDKGDebugManager(CScheduler* scheduler); + + void ProcessMessage(CNode* pfrom, const std::string& strCommand, CDataStream& vRecv, CConnman& connman); + void ProcessDebugStatusMessage(NodeId nodeId, CDKGDebugStatus& status); + + bool AlreadyHave(const CInv& inv); + bool GetDebugStatus(const uint256& hash, CDKGDebugStatus& ret); + bool GetDebugStatusForMasternode(const uint256& proTxHash, CDKGDebugStatus& ret); + void GetLocalDebugStatus(CDKGDebugStatus& ret); + + void ResetLocalSessionStatus(Consensus::LLMQType llmqType, const uint256& quorumHash, int quorumHeight); + + void UpdateLocalStatus(std::function&& func); + void UpdateLocalSessionStatus(Consensus::LLMQType llmqType, std::function&& func); + void UpdateLocalMemberStatus(Consensus::LLMQType llmqType, size_t memberIdx, std::function&& func); + + void SendLocalStatus(); +}; + +extern CDKGDebugManager* quorumDKGDebugManager; + +} + +#endif //DASH_QUORUMS_DEBUG_H diff --git a/src/llmq/quorums_dkgsession.cpp b/src/llmq/quorums_dkgsession.cpp new file mode 100644 index 000000000000..194b97e4e280 --- /dev/null +++ b/src/llmq/quorums_dkgsession.cpp @@ -0,0 +1,1282 @@ +// Copyright (c) 2018 The Dash Core developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "quorums_dkgsession.h" + +#include "quorums.h" +#include "quorums_commitment.h" +#include "quorums_debug.h" +#include "quorums_dkgsessionmgr.h" +#include "quorums_utils.h" + +#include "evo/specialtx.h" + +#include "activemasternode.h" +#include "validation.h" +#include "net.h" +#include "netmessagemaker.h" +#include "univalue.h" +#include "chainparams.h" +#include "spork.h" + +#include "init.h" + +#include "cxxtimer.hpp" + +namespace llmq +{ + +double contributionOmitRate = 0; +double contributionLieRate = 0; +double complainLieRate = 0; +double justifyOmitRate = 0; +double justifyLieRate = 0; +double commitOmitRate = 0; +double commitLieRate = 0; + +static bool RandBool(double rate) +{ + const uint64_t v = 100000000; + uint64_t r = GetRand(v + 1); + if (r <= v * rate) + return true; + return false; +} + +CDKGLogger::CDKGLogger(CDKGSession& _quorumDkg, const std::string& _func) : + CDKGLogger(_quorumDkg.params.type, _quorumDkg.quorumHash, _quorumDkg.height, _quorumDkg.AreWeMember(), _func) +{ +} + +CDKGLogger::CDKGLogger(Consensus::LLMQType _llmqType, const uint256& _quorumHash, int _height, bool _areWeMember, const std::string& _func) : + CBatchedLogger(strprintf("QuorumDKG(type=%d, height=%d, member=%d, func=%s)", _llmqType, _height, _areWeMember, _func)) +{ +} + + +CDKGComplaint::CDKGComplaint(const Consensus::LLMQParams& params) : + complainForMembers((size_t)params.size) +{ +} + +CDKGPrematureCommitment::CDKGPrematureCommitment(const Consensus::LLMQParams& params) : + validMembers((size_t)params.size) +{ +} + +CDKGMember::CDKGMember(CDeterministicMNCPtr _dmn, size_t _idx) : + dmn(_dmn), + idx(_idx), + id(CBLSId::FromHash(_dmn->proTxHash)) +{ + +} + +bool CDKGSession::Init(int _height, const uint256& _quorumHash, const std::vector& mns, const uint256& _myProTxHash) +{ + if (mns.size() < params.minSize) { + return false; + } + + height = _height; + quorumHash = _quorumHash; + + members.resize(mns.size()); + memberIds.resize(members.size()); + receivedVvecs.resize(members.size()); + receivedSkContributions.resize(members.size()); + + for (size_t i = 0; i < mns.size(); i++) { + members[i] = std::unique_ptr(new CDKGMember(mns[i], i)); + membersMap.emplace(members[i]->dmn->proTxHash, i); + memberIds[i] = members[i]->id; + } + + if (!_myProTxHash.IsNull()) { + for (size_t i = 0; i < members.size(); i++) { + auto& m = members[i]; + if (m->dmn->proTxHash == _myProTxHash) { + myIdx = i; + myProTxHash = _myProTxHash; + myId = m->id; + break; + } + } + } + + CDKGLogger logger(*this, __func__); + + if (myProTxHash.IsNull()) { + logger.Printf("initialized as observer. mns=%d\n", mns.size()); + } else { + logger.Printf("initialized as member. mns=%d\n", mns.size()); + } + + return true; +} + +void CDKGSession::Contribute() +{ + CDKGLogger logger(*this, __func__); + + if (!AreWeMember()) { + return; + } + + cxxtimer::Timer t1(true); + logger.Printf("generating contributions\n"); + if (!blsWorker.GenerateContributions(params.threshold, memberIds, vvecContribution, skContributions)) { + // this should never happen actually + logger.Printf("GenerateContributions failed\n"); + return; + } + logger.Printf("generated contributions. time=%d\n", t1.count()); + + SendContributions(); +} + +void CDKGSession::SendContributions() +{ + CDKGLogger logger(*this, __func__); + + assert(AreWeMember()); + + logger.Printf("sending contributions\n"); + + if (RandBool(contributionOmitRate)) { + logger.Printf("omitting\n"); + return; + } + + CDKGContribution qc; + qc.llmqType = (uint8_t)params.type; + qc.quorumHash = quorumHash; + qc.proTxHash = myProTxHash; + qc.vvec = vvecContribution; + + cxxtimer::Timer t1(true); + qc.contributions = std::make_shared>(); + qc.contributions->InitEncrypt(members.size()); + + for (size_t i = 0; i < members.size(); i++) { + auto& m = members[i]; + CBLSSecretKey skContrib = skContributions[i]; + + if (RandBool(contributionLieRate)) { + logger.Printf("lying for %s\n", m->dmn->proTxHash.ToString()); + skContrib.MakeNewKey(); + } + + if (!qc.contributions->Encrypt(i, m->dmn->pdmnState->pubKeyOperator, skContrib, PROTOCOL_VERSION)) { + logger.Printf("failed to encrypt contribution for %s\n", m->dmn->proTxHash.ToString()); + return; + } + } + + logger.Printf("encrypted contributions. time=%d\n", t1.count()); + + qc.sig = activeMasternodeInfo.blsKeyOperator->Sign(qc.GetSignHash()); + + logger.Flush(); + + quorumDKGDebugManager->UpdateLocalSessionStatus(params.type, [&](CDKGDebugSessionStatus& status) { + status.sentContributions = true; + return true; + }); + + uint256 hash = ::SerializeHash(qc); + bool ban = false; + if (PreVerifyMessage(hash, qc, ban)) { + ReceiveMessage(hash, qc, ban); + } +} + +// only performs cheap verifications, but not the signature of the message. this is checked with batched verification +bool CDKGSession::PreVerifyMessage(const uint256& hash, const CDKGContribution& qc, bool& retBan) +{ + CDKGLogger logger(*this, __func__); + + cxxtimer::Timer t1(true); + + retBan = false; + + if (qc.quorumHash != quorumHash) { + logger.Printf("contribution for wrong quorum, rejecting\n"); + return false; + } + + if (Seen(hash)) { + return false; + } + + auto member = GetMember(qc.proTxHash); + if (!member) { + logger.Printf("contributor not a member of this quorum, rejecting contribution\n"); + retBan = true; + return false; + } + + if (qc.contributions->blobs.size() != members.size()) { + logger.Printf("invalid contributions count\n"); + retBan = true; + return false; + } + if (qc.vvec->size() != params.threshold) { + logger.Printf("invalid verification vector length\n"); + retBan = true; + return false; + } + + if (!blsWorker.VerifyVerificationVector(*qc.vvec)) { + logger.Printf("invalid verification vector\n"); + retBan = true; + return false; + } + + if (member->contributions.size() >= 2) { + // don't do any further processing if we got more than 1 valid contributions already + // this is a DoS protection against members sending multiple contributions with valid signatures to us + // we must bail out before any expensive BLS verification happens + logger.Printf("dropping contribution from %s as we already got %d contributions\n", member->dmn->proTxHash.ToString(), member->contributions.size()); + return false; + } + + return true; +} + +void CDKGSession::ReceiveMessage(const uint256& hash, const CDKGContribution& qc, bool& retBan) +{ + CDKGLogger logger(*this, __func__); + + retBan = false; + + auto member = GetMember(qc.proTxHash); + + cxxtimer::Timer t1(true); + logger.Printf("received contribution from %s\n", qc.proTxHash.ToString()); + + { + // relay, no matter if further verification fails + // This ensures the whole quorum sees the bad behavior + LOCK(invCs); + + if (member->contributions.size() >= 2) { + // only relay up to 2 contributions, that's enough to let the other members know about his bad behavior + return; + } + + contributions.emplace(hash, qc); + member->contributions.emplace(hash); + + CInv inv(MSG_QUORUM_CONTRIB, hash); + invSet.emplace(inv); + RelayInvToParticipants(inv); + + quorumDKGDebugManager->UpdateLocalMemberStatus(params.type, member->idx, [&](CDKGDebugMemberStatus& status) { + status.receivedContribution = true; + return true; + }); + + if (member->contributions.size() > 1) { + // don't do any further processing if we got more than 1 contribution. we already relayed it, + // so others know about his bad behavior + MarkBadMember(member->idx); + logger.Printf("%s did send multiple contributions\n", member->dmn->proTxHash.ToString()); + return; + } + } + + receivedVvecs[member->idx] = qc.vvec; + + int receivedCount = 0; + for (auto& m : members) { + if (!m->contributions.empty()) { + receivedCount++; + } + } + + logger.Printf("received and relayed contribution. received=%d/%d, time=%d\n", receivedCount, members.size(), t1.count()); + + cxxtimer::Timer t2(true); + + if (!AreWeMember()) { + // can't further validate + return; + } + + dkgManager.WriteVerifiedVvecContribution(params.type, qc.quorumHash, qc.proTxHash, qc.vvec); + + bool complain = false; + CBLSSecretKey skContribution; + if (!qc.contributions->Decrypt(myIdx, *activeMasternodeInfo.blsKeyOperator, skContribution, PROTOCOL_VERSION)) { + logger.Printf("contribution from %s could not be decrypted\n", member->dmn->proTxHash.ToString()); + complain = true; + } else if (RandBool(complainLieRate)) { + logger.Printf("lying/complaining for %s\n", member->dmn->proTxHash.ToString()); + complain = true; + } + + if (complain) { + member->weComplain = true; + quorumDKGDebugManager->UpdateLocalMemberStatus(params.type, member->idx, [&](CDKGDebugMemberStatus& status) { + status.weComplain = true; + return true; + }); + return; + } + + logger.Printf("decrypted our contribution share. time=%d\n", t2.count()); + + bool verifyPending = false; + receivedSkContributions[member->idx] = skContribution; + pendingContributionVerifications.emplace_back(member->idx); + if (pendingContributionVerifications.size() >= 32) { + verifyPending = true; + } + + if (verifyPending) { + VerifyPendingContributions(); + } +} + +// Verifies all pending secret key contributions in one batch +// This is done by aggregating the verification vectors belonging to the secret key contributions +// The resulting aggregated vvec is then used to recover a public key share +// The public key share must match the public key belonging to the aggregated secret key contributions +// See CBLSWorker::VerifyContributionShares for more details. +void CDKGSession::VerifyPendingContributions() +{ + CDKGLogger logger(*this, __func__); + + if (!AreWeMember()) { + return; + } + + cxxtimer::Timer t1(true); + + std::vector pend = std::move(pendingContributionVerifications); + if (pend.empty()) { + return; + } + + std::vector memberIndexes; + std::vector vvecs; + BLSSecretKeyVector skContributions; + + for (auto& idx : pend) { + auto& m = members[idx]; + if (m->bad || m->weComplain) { + continue; + } + memberIndexes.emplace_back(idx); + vvecs.emplace_back(receivedVvecs[idx]); + skContributions.emplace_back(receivedSkContributions[idx]); + } + + auto result = blsWorker.VerifyContributionShares(myId, vvecs, skContributions); + if (result.size() != memberIndexes.size()) { + logger.Printf("VerifyContributionShares returned result of size %d but size %d was expected, something is wrong\n", result.size(), memberIndexes.size()); + return; + } + + for (size_t i = 0; i < memberIndexes.size(); i++) { + if (!result[i]) { + auto& m = members[memberIndexes[i]]; + logger.Printf("invalid contribution from %s. will complain later\n", m->dmn->proTxHash.ToString()); + m->weComplain = true; + quorumDKGDebugManager->UpdateLocalMemberStatus(params.type, m->idx, [&](CDKGDebugMemberStatus& status) { + status.weComplain = true; + return true; + }); + } else { + size_t memberIdx = memberIndexes[i]; + dkgManager.WriteVerifiedSkContribution(params.type, quorumHash, members[memberIdx]->dmn->proTxHash, skContributions[i]); + } + } + + logger.Printf("verified %d pending contributions. time=%d\n", pend.size(), t1.count()); +} + +void CDKGSession::VerifyAndComplain() +{ + VerifyPendingContributions(); + + CDKGLogger logger(*this, __func__); + + // we check all members if they sent us their contributions + // we consider members as bad if they missed to send anything or if they sent multiple + // in both cases we won't give him a second chance as he is either down, buggy or an adversary + // we assume that such a participant will be marked as bad by the whole network in most cases, + // as propagation will ensure that all nodes see the same vvecs/contributions. In case nodes come to + // different conclusions, the aggregation phase will handle this (most voted quorum key wins) + + cxxtimer::Timer t1(true); + + bool weComplain = false; + + for (auto& m : members) { + if (m->bad) { + continue; + } + if (m->contributions.empty()) { + logger.Printf("%s did not send any contribution\n", m->dmn->proTxHash.ToString()); + MarkBadMember(m->idx); + continue; + } + if (m->weComplain) { + weComplain = true; + } + } + + if (!AreWeMember()) { + return; + } + + logger.Printf("verified contributions. time=%d\n", t1.count()); + logger.Flush(); + + if (weComplain) { + SendComplaint(); + } +} + +void CDKGSession::SendComplaint() +{ + CDKGLogger logger(*this, __func__); + + assert(AreWeMember()); + + CDKGComplaint qc(params); + qc.llmqType = (uint8_t)params.type; + qc.quorumHash = quorumHash; + qc.proTxHash = myProTxHash; + + int count = 0; + for (size_t i = 0; i < members.size(); i++) { + auto& m = members[i]; + if (!m->bad && m->weComplain) { + qc.complainForMembers[i] = true; + count++; + } + } + + logger.Printf("sending complaint for %d members\n", count); + + qc.sig = activeMasternodeInfo.blsKeyOperator->Sign(qc.GetSignHash()); + + logger.Flush(); + + quorumDKGDebugManager->UpdateLocalSessionStatus(params.type, [&](CDKGDebugSessionStatus& status) { + status.sentComplaint = true; + return true; + }); + + uint256 hash = ::SerializeHash(qc); + bool ban = false; + if (PreVerifyMessage(hash, qc, ban)) { + ReceiveMessage(hash, qc, ban); + } +} + +// only performs cheap verifications, but not the signature of the message. this is checked with batched verification +bool CDKGSession::PreVerifyMessage(const uint256& hash, const CDKGComplaint& qc, bool& retBan) +{ + CDKGLogger logger(*this, __func__); + + retBan = false; + + if (qc.quorumHash != quorumHash) { + logger.Printf("complaint for wrong quorum, rejecting\n"); + return false; + } + + if (Seen(hash)) { + return false; + } + + auto member = GetMember(qc.proTxHash); + if (!member) { + logger.Printf("complainer not a member of this quorum, rejecting complaint\n"); + retBan = true; + return false; + } + + if (member->complaints.size() >= 2) { + // don't do any further processing if we got more than 1 valid complaints already + // this is a DoS protection against members sending multiple complaints with valid signatures to us + // we must bail out before any expensive BLS verification happens + logger.Printf("dropping complaint from %s as we already got %d complaints\n", + member->dmn->proTxHash.ToString(), member->complaints.size()); + return false; + } + + return true; +} + +void CDKGSession::ReceiveMessage(const uint256& hash, const CDKGComplaint& qc, bool& retBan) +{ + CDKGLogger logger(*this, __func__); + + retBan = false; + + logger.Printf("received complaint from %s\n", qc.proTxHash.ToString()); + + auto member = GetMember(qc.proTxHash); + + { + LOCK(invCs); + + if (member->complaints.size() >= 2) { + // only relay up to 2 complaints, that's enough to let the other members know about his bad behavior + return; + } + + complaints.emplace(hash, qc); + member->complaints.emplace(hash); + + CInv inv(MSG_QUORUM_COMPLAINT, hash); + invSet.emplace(inv); + RelayInvToParticipants(inv); + + quorumDKGDebugManager->UpdateLocalMemberStatus(params.type, member->idx, [&](CDKGDebugMemberStatus& status) { + status.receivedComplaint = true; + return true; + }); + + if (member->complaints.size() > 1) { + // don't do any further processing if we got more than 1 complaint. we already relayed it, + // so others know about his bad behavior + MarkBadMember(member->idx); + logger.Printf("%s did send multiple complaints\n", member->dmn->proTxHash.ToString()); + return; + } + } + + int receivedCount = 0; + for (size_t i = 0; i < members.size(); i++) { + auto& m = members[i]; + if (qc.complainForMembers[i]) { + m->complaintsFromOthers.emplace(qc.proTxHash); + m->someoneComplain = true; + quorumDKGDebugManager->UpdateLocalMemberStatus(params.type, m->idx, [&](CDKGDebugMemberStatus& status) { + return status.complaintsFromMembers.emplace(member->idx).second; + }); + if (AreWeMember() && i == myIdx) { + logger.Printf("%s complained about us\n", member->dmn->proTxHash.ToString()); + } + } + if (!m->complaints.empty()) { + receivedCount++; + } + } + + logger.Printf("received and relayed complaint. received=%d\n", receivedCount); +} + +void CDKGSession::VerifyAndJustify() +{ + CDKGLogger logger(*this, __func__); + + std::set justifyFor; + + for (auto& m : members) { + if (m->bad) { + continue; + } + if (m->complaints.empty()) { + continue; + } + if (m->complaints.size() != 1) { + logger.Printf("%s sent multiple complaints\n", m->dmn->proTxHash.ToString()); + MarkBadMember(m->idx); + continue; + } + + if (AreWeMember()) { + auto& qc = complaints.at(*m->complaints.begin()); + if (qc.complainForMembers[myIdx]) { + justifyFor.emplace(qc.proTxHash); + } + } + } + + logger.Flush(); + if (!justifyFor.empty()) { + SendJustification(justifyFor); + } +} + +void CDKGSession::SendJustification(const std::set& forMembers) +{ + CDKGLogger logger(*this, __func__); + + assert(AreWeMember()); + + logger.Printf("sending justification for %d members\n", forMembers.size()); + + CDKGJustification qj; + qj.llmqType = (uint8_t)params.type; + qj.quorumHash = quorumHash; + qj.proTxHash = myProTxHash; + qj.contributions.reserve(forMembers.size()); + + for (size_t i = 0; i < members.size(); i++) { + auto& m = members[i]; + if (!forMembers.count(m->dmn->proTxHash)) { + continue; + } + logger.Printf("justifying for %s\n", m->dmn->proTxHash.ToString()); + + CBLSSecretKey skContribution = skContributions[i]; + + if (RandBool(justifyLieRate)) { + logger.Printf("lying for %s\n", m->dmn->proTxHash.ToString()); + skContribution.MakeNewKey(); + } + + qj.contributions.emplace_back(i, skContribution); + } + + if (RandBool(justifyOmitRate)) { + logger.Printf("omitting\n"); + return; + } + + qj.sig = activeMasternodeInfo.blsKeyOperator->Sign(qj.GetSignHash()); + + logger.Flush(); + + quorumDKGDebugManager->UpdateLocalSessionStatus(params.type, [&](CDKGDebugSessionStatus& status) { + status.sentJustification = true; + return true; + }); + + uint256 hash = ::SerializeHash(qj); + bool ban = false; + if (PreVerifyMessage(hash, qj, ban)) { + ReceiveMessage(hash, qj, ban); + } +} + +// only performs cheap verifications, but not the signature of the message. this is checked with batched verification +bool CDKGSession::PreVerifyMessage(const uint256& hash, const CDKGJustification& qj, bool& retBan) +{ + CDKGLogger logger(*this, __func__); + + retBan = false; + + if (qj.quorumHash != quorumHash) { + logger.Printf("justification for wrong quorum, rejecting\n"); + return false; + } + + if (Seen(hash)) { + return false; + } + + auto member = GetMember(qj.proTxHash); + if (!member) { + logger.Printf("justifier not a member of this quorum, rejecting justification\n"); + retBan = true; + return false; + } + + if (qj.contributions.empty()) { + logger.Printf("justification with no contributions\n"); + retBan = true; + return false; + } + + std::set contributionsSet; + for (const auto& p : qj.contributions) { + if (p.first > members.size()) { + logger.Printf("invalid contribution index\n"); + retBan = true; + return false; + } + + if (!contributionsSet.emplace(p.first).second) { + logger.Printf("duplicate contribution index\n"); + retBan = true; + return false; + } + + auto& skShare = p.second; + if (!skShare.IsValid()) { + logger.Printf("invalid contribution\n"); + retBan = true; + return false; + } + } + + if (member->justifications.size() >= 2) { + // don't do any further processing if we got more than 1 valid justification already + // this is a DoS protection against members sending multiple justifications with valid signatures to us + // we must bail out before any expensive BLS verification happens + logger.Printf("dropping justification from %s as we already got %d justifications\n", + member->dmn->proTxHash.ToString(), member->justifications.size()); + return false; + } + + return true; +} + +void CDKGSession::ReceiveMessage(const uint256& hash, const CDKGJustification& qj, bool& retBan) +{ + CDKGLogger logger(*this, __func__); + + retBan = false; + + logger.Printf("received justification from %s\n", qj.proTxHash.ToString()); + + auto member = GetMember(qj.proTxHash); + + { + LOCK(invCs); + + if (member->justifications.size() >= 2) { + // only relay up to 2 justifications, that's enough to let the other members know about his bad behavior + return; + } + + justifications.emplace(hash, qj); + member->justifications.emplace(hash); + + // we always relay, even if further verification fails + CInv inv(MSG_QUORUM_JUSTIFICATION, hash); + invSet.emplace(inv); + RelayInvToParticipants(inv); + + quorumDKGDebugManager->UpdateLocalMemberStatus(params.type, member->idx, [&](CDKGDebugMemberStatus& status) { + status.receivedJustification = true; + return true; + }); + + if (member->justifications.size() > 1) { + // don't do any further processing if we got more than 1 justification. we already relayed it, + // so others know about his bad behavior + logger.Printf("%s did send multiple justifications\n", member->dmn->proTxHash.ToString()); + MarkBadMember(member->idx); + return; + } + + if (member->bad) { + // we locally determined him to be bad (sent none or more then one contributions) + // don't give him a second chance (but we relay the justification in case other members disagree) + return; + } + } + + for (const auto& p : qj.contributions) { + auto& member2 = members[p.first]; + + if (!member->complaintsFromOthers.count(member2->dmn->proTxHash)) { + logger.Printf("got justification from %s for %s even though he didn't complain\n", + member->dmn->proTxHash.ToString(), member2->dmn->proTxHash.ToString()); + MarkBadMember(member->idx); + } + } + if (member->bad) { + return; + } + + cxxtimer::Timer t1(true); + + std::list> futures; + for (const auto& p : qj.contributions) { + auto& member2 = members[p.first]; + auto& skContribution = p.second; + + // watch out to not bail out before these async calls finish (they rely on valid references) + futures.emplace_back(blsWorker.AsyncVerifyContributionShare(member2->id, receivedVvecs[member->idx], skContribution)); + } + auto resultIt = futures.begin(); + for (const auto& p : qj.contributions) { + auto& member2 = members[p.first]; + auto& skContribution = p.second; + + bool result = (resultIt++)->get(); + if (!result) { + logger.Printf(" %s did send an invalid justification for %s\n", member->dmn->proTxHash.ToString(), member2->dmn->proTxHash.ToString()); + MarkBadMember(member->idx); + } else { + logger.Printf(" %s justified for %s\n", member->dmn->proTxHash.ToString(), member2->dmn->proTxHash.ToString()); + if (AreWeMember() && member2->id == myId) { + receivedSkContributions[member->idx] = skContribution; + member->weComplain = false; + + dkgManager.WriteVerifiedSkContribution(params.type, quorumHash, member->dmn->proTxHash, skContribution); + } + member->complaintsFromOthers.erase(member2->dmn->proTxHash); + } + } + + int receivedCount = 0; + int expectedCount = 0; + + for (auto& m : members) { + if (!m->justifications.empty()) { + receivedCount++; + } + + if (m->someoneComplain) { + expectedCount++; + } + } + + logger.Printf("verified justification: received=%d/%d time=%d\n", receivedCount, expectedCount, t1.count()); +} + +void CDKGSession::VerifyAndCommit() +{ + CDKGLogger logger(*this, __func__); + + std::vector badMembers; + std::vector openComplaintMembers; + + for (auto& m : members) { + if (m->bad) { + badMembers.emplace_back(m->idx); + continue; + } + if (!m->complaintsFromOthers.empty()) { + MarkBadMember(m->idx); + openComplaintMembers.emplace_back(m->idx); + } + } + + if (!badMembers.empty() || !openComplaintMembers.empty()) { + logger.Printf("verification result:\n"); + } + if (!badMembers.empty()) { + logger.Printf(" members previously determined as bad:\n"); + for (auto& idx : badMembers) { + logger.Printf(" %s\n", members[idx]->dmn->proTxHash.ToString()); + } + } + if (!openComplaintMembers.empty()) { + logger.Printf(" members with open complaints and now marked as bad:\n"); + for (auto& idx : openComplaintMembers) { + logger.Printf(" %s\n", members[idx]->dmn->proTxHash.ToString()); + } + } + + logger.Flush(); + + if (AreWeMember()) { + SendCommitment(); + } +} + +void CDKGSession::SendCommitment() +{ + CDKGLogger logger(*this, __func__); + + assert(AreWeMember()); + + logger.Printf("sending commitment\n"); + + CDKGPrematureCommitment qc(params); + qc.llmqType = (uint8_t)params.type; + qc.quorumHash = quorumHash; + qc.proTxHash = myProTxHash; + + for (size_t i = 0; i < members.size(); i++) { + auto& m = members[i]; + if (!m->bad) { + qc.validMembers[i] = true; + } + } + + if (qc.CountValidMembers() < params.minSize) { + logger.Printf("not enough valid members. not sending commitment\n"); + return; + } + + if (RandBool(commitOmitRate)) { + logger.Printf("omitting\n"); + return; + } + + cxxtimer::Timer timerTotal(true); + + cxxtimer::Timer t1(true); + std::vector memberIndexes; + std::vector vvecs; + BLSSecretKeyVector skContributions; + if (!dkgManager.GetVerifiedContributions(params.type, quorumHash, qc.validMembers, memberIndexes, vvecs, skContributions)) { + logger.Printf("failed to get valid contributions\n"); + return; + } + + BLSVerificationVectorPtr vvec = cache.BuildQuorumVerificationVector(::SerializeHash(memberIndexes), vvecs); + if (vvec == nullptr) { + logger.Printf("failed to build quorum verification vector\n"); + return; + } + t1.stop(); + + cxxtimer::Timer t2(true); + CBLSSecretKey skShare = cache.AggregateSecretKeys(::SerializeHash(memberIndexes), skContributions); + if (!skShare.IsValid()) { + logger.Printf("failed to build own secret share\n"); + return; + } + t2.stop(); + + logger.Printf("skShare=%s, pubKeyShare=%s\n", skShare.ToString(), skShare.GetPublicKey().ToString()); + + cxxtimer::Timer t3(true); + qc.quorumPublicKey = (*vvec)[0]; + qc.quorumVvecHash = ::SerializeHash(*vvec); + + int lieType = -1; + if (RandBool(commitLieRate)) { + lieType = GetRandInt(5); + logger.Printf("lying on commitment. lieType=%d\n", lieType); + } + + if (lieType == 0) { + CBLSSecretKey k; + k.MakeNewKey(); + qc.quorumPublicKey = k.GetPublicKey(); + } else if (lieType == 1) { + (*qc.quorumVvecHash.begin())++; + } + + uint256 commitmentHash = CLLMQUtils::BuildCommitmentHash(qc.llmqType, qc.quorumHash, qc.validMembers, qc.quorumPublicKey, qc.quorumVvecHash); + + if (lieType == 2) { + (*commitmentHash.begin())++; + } + + qc.sig = activeMasternodeInfo.blsKeyOperator->Sign(commitmentHash); + qc.quorumSig = skShare.Sign(commitmentHash); + + if (lieType == 3) { + std::vector buf; + qc.sig.GetBuf(buf); + buf[5]++; + qc.sig.SetBuf(buf); + } else if (lieType == 4) { + std::vector buf; + qc.quorumSig.GetBuf(buf); + buf[5]++; + qc.quorumSig.SetBuf(buf); + } + + t3.stop(); + timerTotal.stop(); + + logger.Printf("built premature commitment. time1=%d, time2=%d, time3=%d, totalTime=%d\n", + t1.count(), t2.count(), t3.count(), timerTotal.count()); + + + logger.Flush(); + + quorumDKGDebugManager->UpdateLocalSessionStatus(params.type, [&](CDKGDebugSessionStatus& status) { + status.sentPrematureCommitment = true; + return true; + }); + + uint256 hash = ::SerializeHash(qc); + bool ban = false; + if (PreVerifyMessage(hash, qc, ban)) { + ReceiveMessage(hash, qc, ban); + } +} + +// only performs cheap verifications, but not the signature of the message. this is checked with batched verification +bool CDKGSession::PreVerifyMessage(const uint256& hash, const CDKGPrematureCommitment& qc, bool& retBan) +{ + CDKGLogger logger(*this, __func__); + + cxxtimer::Timer t1(true); + + retBan = false; + + if (qc.quorumHash != quorumHash) { + logger.Printf("commitment for wrong quorum, rejecting\n"); + return false; + } + + if (Seen(hash)) { + logger.Printf("already received premature commitment\n"); + return false; + } + + auto member = GetMember(qc.proTxHash); + if (!member) { + logger.Printf("committer not a member of this quorum, rejecting premature commitment\n"); + retBan = true; + return false; + } + + if (qc.CountValidMembers() < params.minSize) { + logger.Printf("invalid validMembers count. validMembersCount=%d\n", qc.CountValidMembers()); + retBan = true; + return false; + } + if (!qc.sig.IsValid()) { + logger.Printf("invalid membersSig\n"); + retBan = true; + return false; + } + if (!qc.quorumSig.IsValid()) { + logger.Printf("invalid quorumSig\n"); + retBan = true; + return false; + } + + for (size_t i = members.size(); i < params.size; i++) { + if (qc.validMembers[i]) { + retBan = true; + logger.Printf("invalid validMembers bitset. bit %d should not be set\n", i); + return false; + } + } + + if (member->prematureCommitments.size() >= 2) { + // don't do any further processing if we got more than 1 valid commitment already + // this is a DoS protection against members sending multiple commitments with valid signatures to us + // we must bail out before any expensive BLS verification happens + logger.Printf("dropping commitment from %s as we already got %d commitments\n", + member->dmn->proTxHash.ToString(), member->prematureCommitments.size()); + return false; + } + + return true; +} + +void CDKGSession::ReceiveMessage(const uint256& hash, const CDKGPrematureCommitment& qc, bool& retBan) +{ + CDKGLogger logger(*this, __func__); + + retBan = false; + + cxxtimer::Timer t1(true); + + logger.Printf("received premature commitment from %s. validMembers=%d\n", qc.proTxHash.ToString(), qc.CountValidMembers()); + + auto member = GetMember(qc.proTxHash); + + { + LOCK(invCs); + + // keep track of ALL commitments but only relay valid ones (or if we couldn't build the vvec) + // relaying is done further down + prematureCommitments.emplace(hash, qc); + member->prematureCommitments.emplace(hash); + } + + std::vector memberIndexes; + std::vector vvecs; + BLSSecretKeyVector skContributions; + BLSVerificationVectorPtr quorumVvec; + if (dkgManager.GetVerifiedContributions(params.type, qc.quorumHash, qc.validMembers, memberIndexes, vvecs, skContributions)) { + quorumVvec = cache.BuildQuorumVerificationVector(::SerializeHash(memberIndexes), vvecs); + } + + if (quorumVvec == nullptr) { + logger.Printf("failed to build quorum verification vector. skipping full verification\n"); + // we might be the unlucky one who didn't receive all contributions, but we still have to relay + // the premature commitment as others might be luckier + } else { + // we got all information that is needed to verify everything (even though we might not be a member of the quorum) + // if any of this verification fails, we won't relay this message. This ensures that invalid messages are lost + // in the network. Nodes relaying such invalid messages to us are not punished as they might have not known + // all contributions. We only handle up to 2 commitments per member, so a DoS shouldn't be possible + + if ((*quorumVvec)[0] != qc.quorumPublicKey) { + logger.Printf("calculated quorum public key does not match\n"); + return; + } + uint256 vvecHash = ::SerializeHash(*quorumVvec); + if (qc.quorumVvecHash != vvecHash) { + logger.Printf("calculated quorum vvec hash does not match\n"); + return; + } + + CBLSPublicKey pubKeyShare = cache.BuildPubKeyShare(::SerializeHash(std::make_pair(memberIndexes, member->id)), quorumVvec, member->id); + if (!pubKeyShare.IsValid()) { + logger.Printf("failed to calculate public key share\n"); + return; + } + + if (!qc.quorumSig.VerifyInsecure(pubKeyShare, qc.GetSignHash())) { + logger.Printf("failed to verify quorumSig\n"); + return; + } + } + + LOCK(invCs); + validCommitments.emplace(hash); + + CInv inv(MSG_QUORUM_PREMATURE_COMMITMENT, hash); + invSet.emplace(inv); + RelayInvToParticipants(inv); + + quorumDKGDebugManager->UpdateLocalMemberStatus(params.type, member->idx, [&](CDKGDebugMemberStatus& status) { + status.receivedPrematureCommitment = true; + return true; + }); + + int receivedCount = 0; + for (auto& m : members) { + if (!m->prematureCommitments.empty()) { + receivedCount++; + } + } + + t1.stop(); + + logger.Printf("verified premature commitment. received=%d/%d, time=%d\n", receivedCount, members.size(), t1.count()); +} + +std::vector CDKGSession::FinalizeCommitments() +{ + CDKGLogger logger(*this, __func__); + + cxxtimer::Timer totalTimer(true); + + typedef std::vector Key; + std::map> commitmentsMap; + + for (auto& p : prematureCommitments) { + auto& qc = p.second; + if (!validCommitments.count(p.first)) { + continue; + } + + // should have been verified before + assert(qc.CountValidMembers() >= params.minSize); + + auto it = commitmentsMap.find(qc.validMembers); + if (it == commitmentsMap.end()) { + it = commitmentsMap.emplace(qc.validMembers, std::vector()).first; + } + + it->second.emplace_back(qc); + } + + std::vector finalCommitments; + for (const auto& p : commitmentsMap) { + auto& cvec = p.second; + if (cvec.size() < params.minSize) { + // commitment was signed by a minority + continue; + } + + std::vector signerIds; + std::vector thresholdSigs; + + auto& first = cvec[0]; + + CFinalCommitment fqc(params, first.quorumHash); + fqc.validMembers = first.validMembers; + fqc.quorumPublicKey = first.quorumPublicKey; + fqc.quorumVvecHash = first.quorumVvecHash; + + uint256 commitmentHash = CLLMQUtils::BuildCommitmentHash(fqc.llmqType, fqc.quorumHash, fqc.validMembers, fqc.quorumPublicKey, fqc.quorumVvecHash); + + std::vector aggSigs; + std::vector aggPks; + aggSigs.reserve(cvec.size()); + aggPks.reserve(cvec.size()); + + for (size_t i = 0; i < cvec.size(); i++) { + auto& qc = cvec[i]; + + if (qc.quorumPublicKey != first.quorumPublicKey || qc.quorumVvecHash != first.quorumVvecHash) { + logger.Printf("quorumPublicKey or quorumVvecHash does not match, skipping"); + continue; + } + + size_t signerIndex = membersMap[qc.proTxHash]; + const auto& m = members[signerIndex]; + + fqc.signers[signerIndex] = true; + aggSigs.emplace_back(qc.sig); + aggPks.emplace_back(m->dmn->pdmnState->pubKeyOperator); + + signerIds.emplace_back(m->id); + thresholdSigs.emplace_back(qc.quorumSig); + } + + cxxtimer::Timer t1(true); + fqc.membersSig = CBLSSignature::AggregateSecure(aggSigs, aggPks, commitmentHash); + t1.stop(); + + cxxtimer::Timer t2(true); + if (!fqc.quorumSig.Recover(thresholdSigs, signerIds)) { + logger.Printf("failed to recover quorum sig\n"); + continue; + } + t2.stop(); + + finalCommitments.emplace_back(fqc); + + logger.Printf("final commitment: validMembers=%d, signers=%d, quorumPublicKey=%s, time1=%d, time2=%d\n", + fqc.CountValidMembers(), fqc.CountSigners(), fqc.quorumPublicKey.ToString(), + t1.count(), t2.count()); + } + + logger.Flush(); + + return finalCommitments; +} + +CDKGMember* CDKGSession::GetMember(const uint256& proTxHash) +{ + auto it = membersMap.find(proTxHash); + if (it == membersMap.end()) { + return nullptr; + } + return members[it->second].get(); +} + +void CDKGSession::MarkBadMember(size_t idx) +{ + auto member = members.at(idx).get(); + if (member->bad) { + return; + } + quorumDKGDebugManager->UpdateLocalMemberStatus(params.type, idx, [&](CDKGDebugMemberStatus& status) { + status.bad = true; + return true; + }); + member->bad = true; +} + +bool CDKGSession::Seen(const uint256& msgHash) +{ + return !seenMessages.emplace(msgHash).second; +} + +void CDKGSession::AddParticipatingNode(NodeId nodeId) +{ + LOCK(invCs); + g_connman->ForNode(nodeId, [&](CNode* pnode) { + if (!participatingNodes.emplace(pnode->addr).second) { + return true; + } + + for (auto& inv : invSet) { + pnode->PushInventory(inv); + } + return true; + }); +} + +void CDKGSession::RelayInvToParticipants(const CInv& inv) +{ + LOCK(invCs); + g_connman->ForEachNode([&](CNode* pnode) { + if (participatingNodes.count(pnode->addr)) { + pnode->PushInventory(inv); + } + }); +} + +} diff --git a/src/llmq/quorums_dkgsession.h b/src/llmq/quorums_dkgsession.h new file mode 100644 index 000000000000..db4af0eac13d --- /dev/null +++ b/src/llmq/quorums_dkgsession.h @@ -0,0 +1,320 @@ +// Copyright (c) 2018 The Dash Core developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef DASH_QUORUMS_DKGSESSION_H +#define DASH_QUORUMS_DKGSESSION_H + +#include "consensus/params.h" +#include "net.h" +#include "batchedlogger.h" + +#include "bls/bls_ies.h" +#include "bls/bls_worker.h" + +#include "evo/deterministicmns.h" +#include "evo/evodb.h" + +#include "llmq/quorums_utils.h" + +class UniValue; + +namespace llmq +{ + +class CFinalCommitment; +class CDKGSession; +class CDKGSessionManager; + +class CDKGLogger : public CBatchedLogger +{ +public: + CDKGLogger(CDKGSession& _quorumDkg, const std::string& _func); + CDKGLogger(Consensus::LLMQType _llmqType, const uint256& _quorumHash, int _height, bool _areWeMember, const std::string& _func); +}; + +class CDKGContribution +{ +public: + uint8_t llmqType; + uint256 quorumHash; + uint256 proTxHash; + BLSVerificationVectorPtr vvec; + std::shared_ptr> contributions; + CBLSSignature sig; + +public: + template + inline void SerializeWithoutSig(Stream& s) const + { + s << llmqType; + s << quorumHash; + s << proTxHash; + s << *vvec; + s << *contributions; + } + template + inline void Serialize(Stream& s) const + { + SerializeWithoutSig(s); + s << sig; + } + template + inline void Unserialize(Stream& s) + { + BLSVerificationVector tmp1; + CBLSIESMultiRecipientObjects tmp2; + + s >> llmqType; + s >> quorumHash; + s >> proTxHash; + s >> tmp1; + s >> tmp2; + s >> sig; + + vvec = std::make_shared(std::move(tmp1)); + contributions = std::make_shared>(std::move(tmp2)); + } + + uint256 GetSignHash() const + { + CHashWriter hw(SER_GETHASH, 0); + SerializeWithoutSig(hw); + hw << CBLSSignature(); + return hw.GetHash(); + } +}; + +class CDKGComplaint +{ +public: + uint8_t llmqType; + uint256 quorumHash; + uint256 proTxHash; + std::vector complainForMembers; + CBLSSignature sig; + +public: + CDKGComplaint() {} + CDKGComplaint(const Consensus::LLMQParams& params); + + ADD_SERIALIZE_METHODS + + template + inline void SerializationOp(Stream& s, Operation ser_action) + { + READWRITE(llmqType); + READWRITE(quorumHash); + READWRITE(proTxHash); + READWRITE(DYNBITSET(complainForMembers)); + READWRITE(sig); + } + + uint256 GetSignHash() const + { + CDKGComplaint tmp(*this); + tmp.sig = CBLSSignature(); + return ::SerializeHash(tmp); + } +}; + +class CDKGJustification +{ +public: + uint8_t llmqType; + uint256 quorumHash; + uint256 proTxHash; + std::vector> contributions; + CBLSSignature sig; + +public: + ADD_SERIALIZE_METHODS + + template + inline void SerializationOp(Stream& s, Operation ser_action) + { + READWRITE(llmqType); + READWRITE(quorumHash); + READWRITE(proTxHash); + READWRITE(contributions); + READWRITE(sig); + } + + uint256 GetSignHash() const + { + CDKGJustification tmp(*this); + tmp.sig = CBLSSignature(); + return ::SerializeHash(tmp); + } +}; + +// each member commits to a single set of valid members with this message +// then each node aggregate all received premature commitments +// into a single CFinalCommitment, which is only valid if +// enough (>=minSize) premature commitments were aggregated +class CDKGPrematureCommitment +{ +public: + uint8_t llmqType; + uint256 quorumHash; + uint256 proTxHash; + std::vector validMembers; + + CBLSPublicKey quorumPublicKey; + uint256 quorumVvecHash; + + CBLSSignature quorumSig; // threshold sig share of quorumHash+validMembers+pubKeyHash+vvecHash + CBLSSignature sig; // single member sig of quorumHash+validMembers+pubKeyHash+vvecHash + +public: + CDKGPrematureCommitment() {} + CDKGPrematureCommitment(const Consensus::LLMQParams& params); + + int CountValidMembers() const + { + return (int)std::count(validMembers.begin(), validMembers.end(), true); + } + +public: + ADD_SERIALIZE_METHODS + + template + inline void SerializationOp(Stream& s, Operation ser_action) + { + READWRITE(llmqType); + READWRITE(quorumHash); + READWRITE(proTxHash); + READWRITE(DYNBITSET(validMembers)); + READWRITE(quorumPublicKey); + READWRITE(quorumVvecHash); + READWRITE(quorumSig); + READWRITE(sig); + } + + uint256 GetSignHash() const + { + return CLLMQUtils::BuildCommitmentHash(llmqType, quorumHash, validMembers, quorumPublicKey, quorumVvecHash); + } +}; + +class CDKGMember +{ +public: + CDKGMember(CDeterministicMNCPtr _dmn, size_t _idx); + + CDeterministicMNCPtr dmn; + size_t idx; + CBLSId id; + + std::set contributions; + std::set complaints; + std::set justifications; + std::set prematureCommitments; + + std::set complaintsFromOthers; + + bool bad{false}; + bool weComplain{false}; + bool someoneComplain{false}; +}; + +class CDKGSession +{ + friend class CDKGSessionHandler; + friend class CDKGSessionManager; + friend class CDKGLogger; + template friend class CDKGMessageHandler; + +private: + const Consensus::LLMQParams& params; + + CEvoDB& evoDb; + CBLSWorker& blsWorker; + CBLSWorkerCache cache; + CDKGSessionManager& dkgManager; + + uint256 quorumHash; + int height{-1}; + +private: + std::vector> members; + std::map membersMap; + BLSVerificationVectorPtr vvecContribution; + BLSSecretKeyVector skContributions; + + BLSIdVector memberIds; + std::vector receivedVvecs; + // these are not necessarily verified yet. Only trust in what was written to the DB + BLSSecretKeyVector receivedSkContributions; + + uint256 myProTxHash; + CBLSId myId; + size_t myIdx{(size_t)-1}; + + // all indexed by msg hash + // we expect to only receive a single vvec and contribution per member, but we must also be able to relay + // conflicting messages as otherwise an attacker might be able to broadcast conflicting (valid+invalid) messages + // and thus split the quorum. Such members are later removed from the quorum. + mutable CCriticalSection invCs; + std::map contributions; + std::map complaints; + std::map justifications; + std::map prematureCommitments; + std::set invSet; + std::set participatingNodes; + + std::set seenMessages; + + std::vector pendingContributionVerifications; + + // filled by ReceivePrematureCommitment and used by FinalizeCommitments + std::set validCommitments; + +public: + CDKGSession(const Consensus::LLMQParams& _params, CEvoDB& _evoDb, CBLSWorker& _blsWorker, CDKGSessionManager& _dkgManager) : + params(_params), evoDb(_evoDb), blsWorker(_blsWorker), cache(_blsWorker), dkgManager(_dkgManager) {} + + bool Init(int _height, const uint256& _quorumHash, const std::vector& mns, const uint256& _myProTxHash); + + // Phase 1: contribution + void Contribute(); + void SendContributions(); + bool PreVerifyMessage(const uint256& hash, const CDKGContribution& qc, bool& retBan); + void ReceiveMessage(const uint256& hash, const CDKGContribution& qc, bool& retBan); + void VerifyPendingContributions(); + + // Phase 2: complaint + void VerifyAndComplain(); + void SendComplaint(); + bool PreVerifyMessage(const uint256& hash, const CDKGComplaint& qc, bool& retBan); + void ReceiveMessage(const uint256& hash, const CDKGComplaint& qc, bool& retBan); + + // Phase 3: justification + void VerifyAndJustify(); + void SendJustification(const std::set& forMembers); + bool PreVerifyMessage(const uint256& hash, const CDKGJustification& qj, bool& retBan); + void ReceiveMessage(const uint256& hash, const CDKGJustification& qj, bool& retBan); + + // Phase 4: commit + void VerifyAndCommit(); + void SendCommitment(); + bool PreVerifyMessage(const uint256& hash, const CDKGPrematureCommitment& qc, bool& retBan); + void ReceiveMessage(const uint256& hash, const CDKGPrematureCommitment& qc, bool& retBan); + + // Phase 5: aggregate/finalize + std::vector FinalizeCommitments(); + + bool AreWeMember() const { return !myProTxHash.IsNull(); } + void MarkBadMember(size_t idx); + + bool Seen(const uint256& msgHash); + void AddParticipatingNode(NodeId nodeId); + void RelayInvToParticipants(const CInv& inv); + +public: + CDKGMember* GetMember(const uint256& proTxHash); +}; + +} + +#endif //DASH_QUORUMS_DKGSESSION_H diff --git a/src/llmq/quorums_dkgsessionmgr.cpp b/src/llmq/quorums_dkgsessionmgr.cpp new file mode 100644 index 000000000000..af5df1f1f0eb --- /dev/null +++ b/src/llmq/quorums_dkgsessionmgr.cpp @@ -0,0 +1,809 @@ +// Copyright (c) 2018 The Dash Core developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "quorums_dkgsessionmgr.h" + +#include "quorums.h" +#include "quorums_blockprocessor.h" +#include "quorums_debug.h" +#include "quorums_utils.h" + +#include "evo/specialtx.h" + +#include "activemasternode.h" +#include "validation.h" +#include "netmessagemaker.h" +#include "univalue.h" +#include "chainparams.h" +#include "spork.h" +#include "net_processing.h" + +#include "init.h" + +#include "cxxtimer.hpp" + +namespace llmq +{ + +CDKGSessionManager* quorumDKGSessionManager; + +static const std::string DB_VVEC = "qdkg_V"; +static const std::string DB_SKCONTRIB = "qdkg_S"; + +CDKGPendingMessages::CDKGPendingMessages(size_t _maxMessagesPerNode) : + maxMessagesPerNode(_maxMessagesPerNode) +{ +} + +void CDKGPendingMessages::PushPendingMessage(NodeId from, CDataStream& vRecv) +{ + LOCK(cs); + + // this will also consume the data, even if we bail out early + auto pm = std::make_shared(std::move(vRecv)); + + if (messagesPerNode[from] >= maxMessagesPerNode) { + // TODO ban? + LogPrint("net", "CDKGPendingMessages::%s -- too many messages, peer=%d\n", __func__, from); + return; + } + messagesPerNode[from]++; + + CHashWriter hw(SER_GETHASH, 0); + hw.write(pm->data(), pm->size()); + uint256 hash = hw.GetHash(); + + if (!seenMessages.emplace(hash).second) { + LogPrint("net", "CDKGPendingMessages::%s -- already seen %s, peer=%d", __func__, from); + return; + } + + { + LOCK(cs_main); + g_connman->RemoveAskFor(hash); + } + + pendingMessages.emplace_back(std::make_pair(from, std::move(pm))); +} + +std::list CDKGPendingMessages::PopPendingMessages(size_t maxCount) +{ + LOCK(cs); + + std::list ret; + while (!pendingMessages.empty() && ret.size() < maxCount) { + ret.emplace_back(std::move(pendingMessages.front())); + pendingMessages.pop_front(); + } + + return std::move(ret); +} + +bool CDKGPendingMessages::HasSeen(const uint256& hash) const +{ + LOCK(cs); + return seenMessages.count(hash) != 0; +} + +void CDKGPendingMessages::Clear() +{ + LOCK(cs); + pendingMessages.clear(); + messagesPerNode.clear(); + seenMessages.clear(); +} + +////// + +CDKGSessionHandler::CDKGSessionHandler(const Consensus::LLMQParams& _params, CEvoDB& _evoDb, ctpl::thread_pool& _messageHandlerPool, CBLSWorker& _blsWorker, CDKGSessionManager& _dkgManager) : + params(_params), + evoDb(_evoDb), + messageHandlerPool(_messageHandlerPool), + blsWorker(_blsWorker), + dkgManager(_dkgManager), + curSession(std::make_shared(_params, _evoDb, _blsWorker, _dkgManager)), + pendingContributions((size_t)_params.size * 2), // we allow size*2 messages as we need to make sure we see bad behavior (double messages) + pendingComplaints((size_t)_params.size * 2), + pendingJustifications((size_t)_params.size * 2), + pendingPrematureCommitments((size_t)_params.size * 2) +{ + phaseHandlerThread = std::thread([this] { + RenameThread(strprintf("quorum-phase-%d", (uint8_t)params.type).c_str()); + PhaseHandlerThread(); + }); +} + +CDKGSessionHandler::~CDKGSessionHandler() +{ + if (phaseHandlerThread.joinable()) { + phaseHandlerThread.join(); + } +} + +void CDKGSessionHandler::UpdatedBlockTip(const CBlockIndex* pindexNew, const CBlockIndex* pindexFork, bool fInitialDownload) +{ + AssertLockHeld(cs_main); + LOCK(cs); + + int quorumStageInt = pindexNew->nHeight % params.dkgInterval; + CBlockIndex* pindexQuorum = chainActive[pindexNew->nHeight - quorumStageInt]; + + quorumHeight = pindexQuorum->nHeight; + quorumHash = pindexQuorum->GetBlockHash(); + + QuorumPhase newPhase = phase; + if (quorumStageInt == 0) { + newPhase = QuorumPhase_Initialized; + quorumDKGDebugManager->ResetLocalSessionStatus(params.type, quorumHash, quorumHeight); + } else if (quorumStageInt == params.dkgPhaseBlocks * 1) { + newPhase = QuorumPhase_Contribute; + } else if (quorumStageInt == params.dkgPhaseBlocks * 2) { + newPhase = QuorumPhase_Complain; + } else if (quorumStageInt == params.dkgPhaseBlocks * 3) { + newPhase = QuorumPhase_Justify; + } else if (quorumStageInt == params.dkgPhaseBlocks * 4) { + newPhase = QuorumPhase_Commit; + } else if (quorumStageInt == params.dkgPhaseBlocks * 5) { + newPhase = QuorumPhase_Finalize; + } else if (quorumStageInt == params.dkgPhaseBlocks * 6) { + newPhase = QuorumPhase_Idle; + } + phase = newPhase; + + quorumDKGDebugManager->UpdateLocalStatus([&](CDKGDebugStatus& status) { + bool changed = status.nHeight != pindexNew->nHeight; + status.nHeight = (uint32_t)pindexNew->nHeight; + return changed; + }); + quorumDKGDebugManager->UpdateLocalSessionStatus(params.type, [&](CDKGDebugSessionStatus& status) { + bool changed = status.phase != (uint8_t)phase; + status.phase = (uint8_t)phase; + return changed; + }); +} + +void CDKGSessionHandler::ProcessMessage(CNode* pfrom, const std::string& strCommand, CDataStream& vRecv, CConnman& connman) +{ + // We don't handle messages in the calling thread as deserialization/processing of these would block everything + if (strCommand == NetMsgType::QCONTRIB) { + pendingContributions.PushPendingMessage(pfrom->id, vRecv); + } else if (strCommand == NetMsgType::QCOMPLAINT) { + pendingComplaints.PushPendingMessage(pfrom->id, vRecv); + } else if (strCommand == NetMsgType::QJUSTIFICATION) { + pendingJustifications.PushPendingMessage(pfrom->id, vRecv); + } else if (strCommand == NetMsgType::QPCOMMITMENT) { + pendingPrematureCommitments.PushPendingMessage(pfrom->id, vRecv); + } +} + +bool CDKGSessionHandler::InitNewQuorum(int height, const uint256& quorumHash) +{ + //AssertLockHeld(cs_main); + + const auto& consensus = Params().GetConsensus(); + + curSession = std::make_shared(params, evoDb, blsWorker, dkgManager); + + if (!deterministicMNManager->IsDeterministicMNsSporkActive(height)) { + return false; + } + + auto mns = CLLMQUtils::GetAllQuorumMembers(params.type, quorumHash); + + if (!curSession->Init(height, quorumHash, mns, activeMasternodeInfo.proTxHash)) { + LogPrintf("CDKGSessionManager::%s -- quorum initialiation failed\n", __func__); + return false; + } + + return true; +} + +std::pair CDKGSessionHandler::GetPhaseAndQuorumHash() +{ + LOCK(cs); + return std::make_pair(phase, quorumHash); +} + +class AbortPhaseException : public std::exception { +}; + +void CDKGSessionHandler::WaitForNextPhase(QuorumPhase curPhase, + QuorumPhase nextPhase, + uint256& expectedQuorumHash, + const WhileWaitFunc& runWhileWaiting) +{ + while (true) { + if (ShutdownRequested()) { + throw AbortPhaseException(); + } + auto p = GetPhaseAndQuorumHash(); + if (!expectedQuorumHash.IsNull() && p.second != expectedQuorumHash) { + throw AbortPhaseException(); + } + if (p.first == nextPhase) { + expectedQuorumHash = p.second; + return; + } + if (curPhase != QuorumPhase_None && p.first != curPhase) { + throw AbortPhaseException(); + } + if (!runWhileWaiting()) { + MilliSleep(100); + } + } +} + +void CDKGSessionHandler::WaitForNewQuorum(const uint256& oldQuorumHash) +{ + while (true) { + if (ShutdownRequested()) { + throw AbortPhaseException(); + } + auto p = GetPhaseAndQuorumHash(); + if (p.second != oldQuorumHash) { + return; + } + MilliSleep(100); + } +} + +void CDKGSessionHandler::RandomSleep(QuorumPhase curPhase, + uint256& expectedQuorumHash, + double randomSleepFactor, + const WhileWaitFunc& runWhileWaiting) +{ + // Randomly sleep some time to not fully overload the whole network + int64_t endTime = GetTimeMillis() + GetRandInt((int)(params.dkgRndSleepTime * randomSleepFactor)); + while (GetTimeMillis() < endTime) { + if (ShutdownRequested()) { + throw AbortPhaseException(); + } + auto p = GetPhaseAndQuorumHash(); + if (p.first != curPhase || p.second != expectedQuorumHash) { + throw AbortPhaseException(); + } + if (!runWhileWaiting()) { + MilliSleep(100); + } + } +} + +void CDKGSessionHandler::HandlePhase(QuorumPhase curPhase, + QuorumPhase nextPhase, + uint256& expectedQuorumHash, + double randomSleepFactor, + const StartPhaseFunc& startPhaseFunc, + const WhileWaitFunc& runWhileWaiting) +{ + RandomSleep(curPhase, expectedQuorumHash, randomSleepFactor, runWhileWaiting); + startPhaseFunc(); + WaitForNextPhase(curPhase, nextPhase, expectedQuorumHash, runWhileWaiting); +} + +// returns a set of NodeIds which sent invalid messages +template +std::set BatchVerifyMessageSigs(CDKGSession& session, const std::vector>>& messages) +{ + if (messages.empty()) { + return {}; + } + + std::set ret; + bool revertToSingleVerification = false; + + CBLSSignature aggSig; + std::vector pubKeys; + std::vector messageHashes; + std::set messageHashesSet; + pubKeys.reserve(messages.size()); + messageHashes.reserve(messages.size()); + bool first = true; + for (const auto& p : messages ) { + const auto& msg = *p.second; + + auto member = session.GetMember(msg.proTxHash); + if (!member) { + // should not happen as it was verified before + ret.emplace(p.first); + continue; + } + + if (first) { + aggSig = msg.sig; + } else { + aggSig.AggregateInsecure(msg.sig); + } + first = false; + + auto msgHash = msg.GetSignHash(); + if (!messageHashesSet.emplace(msgHash).second) { + // can only happen in 2 cases: + // 1. Someone sent us the same message twice but with differing signature, meaning that at least one of them + // must be invalid. In this case, we'd have to revert to single message verification nevertheless + // 2. Someone managed to find a way to create two different binary representations of a message that deserializes + // to the same object representation. This would be some form of malleability. However, this shouldn't + // possible as only deterministic/unique BLS signatures and very simple data types are involved + revertToSingleVerification = true; + break; + } + + pubKeys.emplace_back(member->dmn->pdmnState->pubKeyOperator); + messageHashes.emplace_back(msgHash); + } + if (!revertToSingleVerification) { + bool valid = aggSig.VerifyInsecureAggregated(pubKeys, messageHashes); + if (valid) { + // all good + return ret; + } + + // are all messages from the same node? + NodeId firstNodeId; + first = true; + bool nodeIdsAllSame = true; + for (auto it = messages.begin(); it != messages.end(); ++it) { + if (first) { + firstNodeId = it->first; + } else { + first = false; + if (it->first != firstNodeId) { + nodeIdsAllSame = false; + break; + } + } + } + // if yes, take a short path and return a set with only him + if (nodeIdsAllSame) { + ret.emplace(firstNodeId); + return ret; + } + // different nodes, let's figure out who are the bad ones + } + + for (const auto& p : messages) { + if (ret.count(p.first)) { + continue; + } + + const auto& msg = *p.second; + auto member = session.GetMember(msg.proTxHash); + bool valid = msg.sig.VerifyInsecure(member->dmn->pdmnState->pubKeyOperator, msg.GetSignHash()); + if (!valid) { + ret.emplace(p.first); + } + } + return ret; +} + +template +bool ProcessPendingMessageBatch(CDKGSession& session, CDKGPendingMessages& pendingMessages, size_t maxCount) +{ + auto msgs = pendingMessages.PopAndDeserializeMessages(maxCount); + if (msgs.empty()) { + return false; + } + + std::vector hashes; + std::vector>> preverifiedMessages; + hashes.reserve(msgs.size()); + preverifiedMessages.reserve(msgs.size()); + + for (const auto& p : msgs) { + if (!p.second) { + LogPrint("net", "%s -- failed to deserialize message, peer=%d", __func__, p.first); + { + LOCK(cs_main); + Misbehaving(p.first, 100); + } + continue; + } + const auto& msg = *p.second; + + auto hash = ::SerializeHash(msg); + { + LOCK(cs_main); + g_connman->RemoveAskFor(hash); + } + + bool ban = false; + if (!session.PreVerifyMessage(hash, msg, ban)) { + if (ban) { + LogPrint("net", "%s -- banning node due to failed preverification, peer=%d", __func__, p.first); + { + LOCK(cs_main); + Misbehaving(p.first, 100); + } + } + LogPrint("net", "%s -- skipping message due to failed preverification, peer=%d", __func__, p.first); + continue; + } + hashes.emplace_back(hash); + preverifiedMessages.emplace_back(p); + } + if (preverifiedMessages.empty()) { + return true; + } + + auto badNodes = BatchVerifyMessageSigs(session, preverifiedMessages); + if (!badNodes.empty()) { + LOCK(cs_main); + for (auto nodeId : badNodes) { + LogPrint("net", "%s -- failed to deserialize message, peer=%d", __func__, nodeId); + Misbehaving(nodeId, 100); + } + } + + for (size_t i = 0; i < preverifiedMessages.size(); i++) { + NodeId nodeId = preverifiedMessages[i].first; + if (badNodes.count(nodeId)) { + continue; + } + const auto& msg = *preverifiedMessages[i].second; + bool ban = false; + session.ReceiveMessage(hashes[i], msg, ban); + if (ban) { + LogPrint("net", "%s -- banning node after ReceiveMessage failed, peer=%d", __func__, nodeId); + LOCK(cs_main); + Misbehaving(nodeId, 100); + badNodes.emplace(nodeId); + } + } + + for (const auto& p : preverifiedMessages) { + NodeId nodeId = p.first; + if (badNodes.count(nodeId)) { + continue; + } + session.AddParticipatingNode(nodeId); + } + + return true; +} + +void CDKGSessionHandler::HandleDKGRound() +{ + uint256 curQuorumHash; + + WaitForNextPhase(QuorumPhase_None, QuorumPhase_Initialized, curQuorumHash, []{return false;}); + + { + LOCK(cs); + pendingContributions.Clear(); + pendingComplaints.Clear(); + pendingJustifications.Clear(); + pendingPrematureCommitments.Clear(); + } + + if (!InitNewQuorum(quorumHeight, quorumHash)) { + // should actually never happen + WaitForNewQuorum(curQuorumHash); + throw AbortPhaseException(); + } + + if (curSession->AreWeMember() || GetBoolArg("-watchquorums", DEFAULT_WATCH_QUORUMS)) { + std::set connections; + if (curSession->AreWeMember()) { + connections = CLLMQUtils::GetQuorumConnections(params.type, curQuorumHash, curSession->myProTxHash); + } else { + auto cindexes = CLLMQUtils::CalcDeterministicWatchConnections(params.type, curQuorumHash, curSession->members.size(), 1); + for (auto idx : cindexes) { + connections.emplace(curSession->members[idx]->dmn->pdmnState->addr); + } + } + if (!connections.empty()) { + std::string debugMsg = strprintf("CDKGSessionManager::%s -- adding masternodes quorum connections for quorum %s:\n", __func__, curSession->quorumHash.ToString()); + for (auto& c : connections) { + debugMsg += strprintf(" %s\n", c.ToString(false)); + } + LogPrintf(debugMsg); + g_connman->AddMasternodeQuorumNodes(params.type, curQuorumHash, connections); + + LOCK(curSession->invCs); + curSession->participatingNodes = g_connman->GetMasternodeQuorumAddresses(params.type, curQuorumHash); + } + } + + WaitForNextPhase(QuorumPhase_Initialized, QuorumPhase_Contribute, curQuorumHash, []{return false;}); + + // Contribute + auto fContributeStart = [this]() { + curSession->Contribute(); + }; + auto fContributeWait = [this] { + return ProcessPendingMessageBatch(*curSession, pendingContributions, 8); + }; + HandlePhase(QuorumPhase_Contribute, QuorumPhase_Complain, curQuorumHash, 1, fContributeStart, fContributeWait); + + // Complain + auto fComplainStart = [this]() { + curSession->VerifyAndComplain(); + }; + auto fComplainWait = [this] { + return ProcessPendingMessageBatch(*curSession, pendingComplaints, 8); + }; + HandlePhase(QuorumPhase_Complain, QuorumPhase_Justify, curQuorumHash, 0, fComplainStart, fComplainWait); + + // Justify + auto fJustifyStart = [this]() { + curSession->VerifyAndJustify(); + }; + auto fJustifyWait = [this] { + return ProcessPendingMessageBatch(*curSession, pendingJustifications, 8); + }; + HandlePhase(QuorumPhase_Justify, QuorumPhase_Commit, curQuorumHash, 0, fJustifyStart, fJustifyWait); + + // Commit + auto fCommitStart = [this]() { + curSession->VerifyAndCommit(); + }; + auto fCommitWait = [this] { + return ProcessPendingMessageBatch(*curSession, pendingPrematureCommitments, 8); + }; + HandlePhase(QuorumPhase_Commit, QuorumPhase_Finalize, curQuorumHash, 1, fCommitStart, fCommitWait); + + auto finalCommitments = curSession->FinalizeCommitments(); + for (auto& fqc : finalCommitments) { + quorumBlockProcessor->AddMinableCommitment(fqc); + } +} + +void CDKGSessionHandler::PhaseHandlerThread() +{ + while (!ShutdownRequested()) { + try { + HandleDKGRound(); + } catch (AbortPhaseException& e) { + quorumDKGDebugManager->UpdateLocalSessionStatus(params.type, [&](CDKGDebugSessionStatus& status) { + status.aborted = true; + return true; + }); + LogPrintf("CDKGSessionHandler::%s -- aborted current DKG session\n", __func__); + } + } +} + +//// + +CDKGSessionManager::CDKGSessionManager(CEvoDB& _evoDb, CBLSWorker& _blsWorker) : + evoDb(_evoDb), + blsWorker(_blsWorker) +{ + for (auto& qt : Params().GetConsensus().llmqs) { + dkgSessionHandlers.emplace(std::piecewise_construct, + std::forward_as_tuple(qt.first), + std::forward_as_tuple(qt.second, _evoDb, messageHandlerPool, blsWorker, *this)); + } + + messageHandlerPool.resize(2); + RenameThreadPool(messageHandlerPool, "quorum-msg"); +} + +CDKGSessionManager::~CDKGSessionManager() +{ + messageHandlerPool.stop(true); +} + +void CDKGSessionManager::UpdatedBlockTip(const CBlockIndex* pindexNew, const CBlockIndex* pindexFork, bool fInitialDownload) +{ + const auto& consensus = Params().GetConsensus(); + + if (fInitialDownload) + return; + if (!deterministicMNManager->IsDeterministicMNsSporkActive(pindexNew->nHeight)) + return; + if (!sporkManager.IsSporkActive(SPORK_17_QUORUM_DKG_ENABLED)) + return; + + LOCK(cs_main); + + for (auto& qt : dkgSessionHandlers) { + qt.second.UpdatedBlockTip(pindexNew, pindexFork, fInitialDownload); + } +} + +void CDKGSessionManager::ProcessMessage(CNode* pfrom, const std::string& strCommand, CDataStream& vRecv, CConnman& connman) +{ + if (!sporkManager.IsSporkActive(SPORK_17_QUORUM_DKG_ENABLED)) + return; + + if (strCommand != NetMsgType::QCONTRIB + && strCommand != NetMsgType::QCOMPLAINT + && strCommand != NetMsgType::QJUSTIFICATION + && strCommand != NetMsgType::QPCOMMITMENT + && strCommand != NetMsgType::QWATCH) { + return; + } + + if (strCommand == NetMsgType::QWATCH) { + pfrom->qwatch = true; + for (auto& p : dkgSessionHandlers) { + LOCK2(p.second.cs, p.second.curSession->invCs); + p.second.curSession->participatingNodes.emplace(pfrom->addr); + } + return; + } + + if (vRecv.size() < 1) { + LOCK(cs_main); + Misbehaving(pfrom->id, 100); + return; + } + + // peek into the message and see which LLMQType it is. First byte of all messages is always the LLMQType + Consensus::LLMQType llmqType = (Consensus::LLMQType)*vRecv.begin(); + if (!dkgSessionHandlers.count(llmqType)) { + LOCK(cs_main); + Misbehaving(pfrom->id, 100); + return; + } + + dkgSessionHandlers.at(llmqType).ProcessMessage(pfrom, strCommand, vRecv, connman); +} + +bool CDKGSessionManager::AlreadyHave(const CInv& inv) const +{ + if (!sporkManager.IsSporkActive(SPORK_17_QUORUM_DKG_ENABLED)) + return false; + + for (auto& p : dkgSessionHandlers) { + auto& dkgType = p.second; + if (dkgType.pendingContributions.HasSeen(inv.hash) + || dkgType.pendingComplaints.HasSeen(inv.hash) + || dkgType.pendingJustifications.HasSeen(inv.hash) + || dkgType.pendingPrematureCommitments.HasSeen(inv.hash)) { + return true; + } + } + return false; +} + +bool CDKGSessionManager::GetContribution(const uint256& hash, CDKGContribution& ret) const +{ + if (!sporkManager.IsSporkActive(SPORK_17_QUORUM_DKG_ENABLED)) + return false; + + for (auto& p : dkgSessionHandlers) { + auto& dkgType = p.second; + LOCK2(dkgType.cs, dkgType.curSession->invCs); + if (dkgType.phase < QuorumPhase_Initialized || dkgType.phase > QuorumPhase_Contribute) { + continue; + } + auto it = dkgType.curSession->contributions.find(hash); + if (it != dkgType.curSession->contributions.end()) { + ret = it->second; + return true; + } + } + return false; +} + +bool CDKGSessionManager::GetComplaint(const uint256& hash, CDKGComplaint& ret) const +{ + if (!sporkManager.IsSporkActive(SPORK_17_QUORUM_DKG_ENABLED)) + return false; + + for (auto& p : dkgSessionHandlers) { + auto& dkgType = p.second; + LOCK2(dkgType.cs, dkgType.curSession->invCs); + if (dkgType.phase < QuorumPhase_Contribute || dkgType.phase > QuorumPhase_Complain) { + continue; + } + auto it = dkgType.curSession->complaints.find(hash); + if (it != dkgType.curSession->complaints.end()) { + ret = it->second; + return true; + } + } + return false; +} + +bool CDKGSessionManager::GetJustification(const uint256& hash, CDKGJustification& ret) const +{ + if (!sporkManager.IsSporkActive(SPORK_17_QUORUM_DKG_ENABLED)) + return false; + + for (auto& p : dkgSessionHandlers) { + auto& dkgType = p.second; + LOCK2(dkgType.cs, dkgType.curSession->invCs); + if (dkgType.phase < QuorumPhase_Complain || dkgType.phase > QuorumPhase_Justify) { + continue; + } + auto it = dkgType.curSession->justifications.find(hash); + if (it != dkgType.curSession->justifications.end()) { + ret = it->second; + return true; + } + } + return false; +} + +bool CDKGSessionManager::GetPrematureCommitment(const uint256& hash, CDKGPrematureCommitment& ret) const +{ + if (!sporkManager.IsSporkActive(SPORK_17_QUORUM_DKG_ENABLED)) + return false; + + for (auto& p : dkgSessionHandlers) { + auto& dkgType = p.second; + LOCK2(dkgType.cs, dkgType.curSession->invCs); + if (dkgType.phase < QuorumPhase_Justify || dkgType.phase > QuorumPhase_Commit) { + continue; + } + auto it = dkgType.curSession->prematureCommitments.find(hash); + if (it != dkgType.curSession->prematureCommitments.end() && dkgType.curSession->validCommitments.count(hash)) { + ret = it->second; + return true; + } + } + return false; +} + +void CDKGSessionManager::WriteVerifiedVvecContribution(Consensus::LLMQType llmqType, const uint256& quorumHash, const uint256& proTxHash, const BLSVerificationVectorPtr& vvec) +{ + evoDb.GetRawDB().Write(std::make_tuple(DB_VVEC, (uint8_t)llmqType, quorumHash, proTxHash), *vvec); +} + +void CDKGSessionManager::WriteVerifiedSkContribution(Consensus::LLMQType llmqType, const uint256& quorumHash, const uint256& proTxHash, const CBLSSecretKey& skContribution) +{ + evoDb.GetRawDB().Write(std::make_tuple(DB_SKCONTRIB, (uint8_t)llmqType, quorumHash, proTxHash), skContribution); +} + +bool CDKGSessionManager::GetVerifiedContributions(Consensus::LLMQType llmqType, const uint256& quorumHash, const std::vector& validMembers, std::vector& memberIndexesRet, std::vector& vvecsRet, BLSSecretKeyVector& skContributionsRet) +{ + auto members = CLLMQUtils::GetAllQuorumMembers(llmqType, quorumHash); + + if (validMembers.size() != members.size()) { + // should never happen as we should always call this method with correct params + return false; + } + + memberIndexesRet.clear(); + vvecsRet.clear(); + skContributionsRet.clear(); + memberIndexesRet.reserve(members.size()); + vvecsRet.reserve(members.size()); + skContributionsRet.reserve(members.size()); + for (size_t i = 0; i < members.size(); i++) { + if (validMembers[i]) { + BLSVerificationVectorPtr vvec; + CBLSSecretKey skContribution; + if (!GetVerifiedContribution(llmqType, quorumHash, members[i]->proTxHash, vvec, skContribution)) { + return false; + } + + memberIndexesRet.emplace_back(i); + vvecsRet.emplace_back(vvec); + skContributionsRet.emplace_back(skContribution); + } + } + return true; +} + +bool CDKGSessionManager::GetVerifiedContribution(Consensus::LLMQType llmqType, const uint256& quorumHash, const uint256& proTxHash, BLSVerificationVectorPtr& vvecRet, CBLSSecretKey& skContributionRet) +{ + LOCK(contributionsCacheCs); + auto cacheKey = std::make_tuple(llmqType, quorumHash, proTxHash); + auto it = contributionsCache.find(cacheKey); + if (it != contributionsCache.end()) { + vvecRet = it->second.first; + skContributionRet = it->second.second; + return true; + } + + BLSVerificationVector vvec; + BLSVerificationVectorPtr vvecPtr; + CBLSSecretKey skContribution; + if (evoDb.GetRawDB().Read(std::make_tuple(DB_VVEC, (uint8_t)llmqType, quorumHash, proTxHash), vvec)) { + vvecPtr = std::make_shared(std::move(vvec)); + } + evoDb.GetRawDB().Read(std::make_tuple(DB_SKCONTRIB, (uint8_t)llmqType, quorumHash, proTxHash), skContribution); + + it = contributionsCache.emplace(cacheKey, std::make_pair(vvecPtr, skContribution)).first; + + vvecRet = it->second.first; + skContributionRet = it->second.second; + + return true; +} + +} diff --git a/src/llmq/quorums_dkgsessionmgr.h b/src/llmq/quorums_dkgsessionmgr.h new file mode 100644 index 000000000000..96efb388d6f6 --- /dev/null +++ b/src/llmq/quorums_dkgsessionmgr.h @@ -0,0 +1,161 @@ +// Copyright (c) 2018 The Dash Core developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef DASH_QUORUMS_DKGSESSIONMGR_H +#define DASH_QUORUMS_DKGSESSIONMGR_H + +#include "llmq/quorums_dkgsession.h" + +#include "validation.h" + +#include "ctpl.h" + +class UniValue; + +namespace llmq +{ + +enum QuorumPhase { + QuorumPhase_Idle, + QuorumPhase_Initialized, + QuorumPhase_Contribute, + QuorumPhase_Complain, + QuorumPhase_Justify, + QuorumPhase_Commit, + QuorumPhase_Finalize, + + QuorumPhase_None=-1, +}; + +class CDKGPendingMessages +{ +public: + typedef std::pair> BinaryMessage; + +private: + mutable CCriticalSection cs; + size_t maxMessagesPerNode; + std::list pendingMessages; + std::map messagesPerNode; + std::set seenMessages; + +public: + CDKGPendingMessages(size_t _maxMessagesPerNode); + + void PushPendingMessage(NodeId from, CDataStream& vRecv); + std::list PopPendingMessages(size_t maxCount); + bool HasSeen(const uint256& hash) const; + void Clear(); + + // Might return nullptr messages, which indicates that deserialization failed for some reason + template + std::vector>> PopAndDeserializeMessages(size_t maxCount) + { + auto binaryMessages = PopPendingMessages(maxCount); + if (binaryMessages.empty()) { + return {}; + } + + std::vector>> ret; + ret.reserve(binaryMessages.size()); + for (auto& bm : binaryMessages) { + auto msg = std::make_shared(); + try { + *bm.second >> *msg; + } catch (...) { + msg = nullptr; + } + ret.emplace_back(std::make_pair(bm.first, std::move(msg))); + } + + return std::move(ret); + } +}; + +// we have one handler per DKG type +class CDKGSessionHandler +{ +private: + friend class CDKGSessionManager; + +private: + mutable CCriticalSection cs; + + const Consensus::LLMQParams& params; + CEvoDB& evoDb; + ctpl::thread_pool& messageHandlerPool; + CBLSWorker& blsWorker; + CDKGSessionManager& dkgManager; + + QuorumPhase phase{QuorumPhase_Idle}; + int quorumHeight{-1}; + uint256 quorumHash; + std::shared_ptr curSession; + std::thread phaseHandlerThread; + + CDKGPendingMessages pendingContributions; + CDKGPendingMessages pendingComplaints; + CDKGPendingMessages pendingJustifications; + CDKGPendingMessages pendingPrematureCommitments; + +public: + CDKGSessionHandler(const Consensus::LLMQParams& _params, CEvoDB& _evoDb, ctpl::thread_pool& _messageHandlerPool, CBLSWorker& blsWorker, CDKGSessionManager& _dkgManager); + ~CDKGSessionHandler(); + + void UpdatedBlockTip(const CBlockIndex *pindexNew, const CBlockIndex *pindexFork, bool fInitialDownload); + void ProcessMessage(CNode* pfrom, const std::string& strCommand, CDataStream& vRecv, CConnman& connman); + +private: + bool InitNewQuorum(int height, const uint256& quorumHash); + + std::pair GetPhaseAndQuorumHash(); + + typedef std::function StartPhaseFunc; + typedef std::function WhileWaitFunc; + void WaitForNextPhase(QuorumPhase curPhase, QuorumPhase nextPhase, uint256& expectedQuorumHash, const WhileWaitFunc& runWhileWaiting); + void WaitForNewQuorum(const uint256& oldQuorumHash); + void RandomSleep(QuorumPhase curPhase, uint256& expectedQuorumHash, double randomSleepFactor, const WhileWaitFunc& runWhileWaiting); + void HandlePhase(QuorumPhase curPhase, QuorumPhase nextPhase, uint256& expectedQuorumHash, double randomSleepFactor, const StartPhaseFunc& startPhaseFunc, const WhileWaitFunc& runWhileWaiting); + void HandleDKGRound(); + void PhaseHandlerThread(); +}; + +class CDKGSessionManager +{ +private: + CEvoDB& evoDb; + CBLSWorker& blsWorker; + ctpl::thread_pool messageHandlerPool; + + std::map dkgSessionHandlers; + + // TODO cleanup + CCriticalSection contributionsCacheCs; + std::map, std::pair> contributionsCache; + +public: + CDKGSessionManager(CEvoDB& _evoDb, CBLSWorker& _blsWorker); + ~CDKGSessionManager(); + + void UpdatedBlockTip(const CBlockIndex *pindexNew, const CBlockIndex *pindexFork, bool fInitialDownload); + + void ProcessMessage(CNode* pfrom, const std::string& strCommand, CDataStream& vRecv, CConnman& connman); + bool AlreadyHave(const CInv& inv) const; + bool GetContribution(const uint256& hash, CDKGContribution& ret) const; + bool GetComplaint(const uint256& hash, CDKGComplaint& ret) const; + bool GetJustification(const uint256& hash, CDKGJustification& ret) const; + bool GetPrematureCommitment(const uint256& hash, CDKGPrematureCommitment& ret) const; + + // Verified contributions are written while in the DKG + void WriteVerifiedVvecContribution(Consensus::LLMQType llmqType, const uint256& quorumHash, const uint256& proTxHash, const BLSVerificationVectorPtr& vvec); + void WriteVerifiedSkContribution(Consensus::LLMQType llmqType, const uint256& quorumHash, const uint256& proTxHash, const CBLSSecretKey& skContribution); + bool GetVerifiedContributions(Consensus::LLMQType llmqType, const uint256& quorumHash, const std::vector& validMembers, std::vector& memberIndexesRet, std::vector& vvecsRet, BLSSecretKeyVector& skContributionsRet); + bool GetVerifiedContribution(Consensus::LLMQType llmqType, const uint256& quorumHash, const uint256& proTxHash, BLSVerificationVectorPtr& vvecRet, CBLSSecretKey& skContributionRet); +}; + +extern CDKGSessionManager* quorumDKGSessionManager; + +} + +#endif //DASH_QUORUMS_DKGSESSIONMGR_H diff --git a/src/llmq/quorums_dummydkg.cpp b/src/llmq/quorums_dummydkg.cpp deleted file mode 100644 index 384720d9bae3..000000000000 --- a/src/llmq/quorums_dummydkg.cpp +++ /dev/null @@ -1,454 +0,0 @@ -// Copyright (c) 2018 The Dash Core developers -// Distributed under the MIT/X11 software license, see the accompanying -// file COPYING or http://www.opensource.org/licenses/mit-license.php. - -#include "quorums_dummydkg.h" - -#include "quorums_blockprocessor.h" -#include "quorums_commitment.h" -#include "quorums_utils.h" - -#include "evo/specialtx.h" - -#include "activemasternode.h" -#include "chain.h" -#include "chainparams.h" -#include "consensus/validation.h" -#include "net.h" -#include "net_processing.h" -#include "primitives/block.h" -#include "spork.h" -#include "validation.h" - -namespace llmq -{ - -CDummyDKG* quorumDummyDKG; - -void CDummyDKG::ProcessMessage(CNode* pfrom, const std::string& strCommand, CDataStream& vRecv, CConnman& connman) -{ - if (strCommand == NetMsgType::QDCOMMITMENT) { - if (!Params().GetConsensus().fLLMQAllowDummyCommitments) { - Misbehaving(pfrom->id, 100); - return; - } - if (!sporkManager.IsSporkActive(SPORK_17_QUORUM_DKG_ENABLED)) { - return; - } - - CDummyCommitment qc; - vRecv >> qc; - - uint256 hash = ::SerializeHash(qc); - { - LOCK(cs_main); - connman.RemoveAskFor(hash); - } - - ProcessDummyCommitment(pfrom->id, qc); - } -} - -void CDummyDKG::ProcessDummyCommitment(NodeId from, const llmq::CDummyCommitment& qc) -{ - if (!Params().GetConsensus().llmqs.count((Consensus::LLMQType)qc.llmqType)) { - LOCK(cs_main); - LogPrintf("CDummyDKG::%s -- invalid commitment type %d, peer=%d\n", __func__, - qc.llmqType, from); - if (from != -1) { - Misbehaving(from, 100); - } - return; - } - - auto type = (Consensus::LLMQType)qc.llmqType; - const auto& params = Params().GetConsensus().llmqs.at(type); - - if (qc.validMembers.size() != params.size) { - LOCK(cs_main); - LogPrintf("CDummyDKG::%s -- invalid validMembers size %d, peer=%d\n", __func__, - qc.validMembers.size(), from); - if (from != -1) { - Misbehaving(from, 100); - } - return; - } - - int curQuorumHeight; - const CBlockIndex* quorumIndex; - { - LOCK(cs_main); - curQuorumHeight = chainActive.Height() - (chainActive.Height() % params.dkgInterval); - quorumIndex = chainActive[curQuorumHeight]; - } - uint256 quorumHash = quorumIndex->GetBlockHash(); - if (qc.quorumHash != quorumHash) { - LogPrintf("CDummyDKG::%s -- dummy commitment for wrong quorum, peer=%d\n", __func__, - from); - return; - } - - auto members = CLLMQUtils::GetAllQuorumMembers(type, qc.quorumHash); - if (members.size() != params.size) { - LOCK(cs_main); - LogPrintf("CDummyDKG::%s -- invalid members count %d, peer=%d\n", __func__, - members.size(), from); - if (from != -1) { - Misbehaving(from, 100); - } - return; - } - if (qc.signer >= members.size()) { - LOCK(cs_main); - LogPrintf("CDummyDKG::%s -- invalid signer %d, peer=%d\n", __func__, - qc.signer, from); - if (from != -1) { - Misbehaving(from, 100); - } - return; - } - if (qc.CountValidMembers() < params.minSize) { - LOCK(cs_main); - LogPrintf("CDummyDKG::%s -- invalid validMembers count %d, peer=%d\n", __func__, - qc.CountValidMembers(), from); - if (from != -1) { - Misbehaving(from, 100); - } - return; - } - - auto signer = members[qc.signer]; - - { - LOCK(sessionCs); - for (const auto& p : curSessions[type].dummyCommitmentsFromMembers) { - if (p.second.count(signer->proTxHash)) { - return; - } - } - } - - auto svec = BuildDeterministicSvec(type, qc.quorumHash); - auto vvec = BuildVvec(svec); - auto vvecHash = ::SerializeHash(vvec); - - auto commitmentHash = CLLMQUtils::BuildCommitmentHash((uint8_t)type, qc.quorumHash, qc.validMembers, vvec[0], vvecHash); - - // verify member sig - if (!qc.membersSig.VerifyInsecure(signer->pdmnState->pubKeyOperator, commitmentHash)) { - LOCK(cs_main); - LogPrintf("CDummyDKG::%s -- invalid memberSig, peer=%d\n", __func__, - from); - if (from != -1) { - Misbehaving(from, 100); - } - return; - } - - // recover public key share - CBLSPublicKey sharePk; - if (!sharePk.PublicKeyShare(vvec, CBLSId::FromHash(signer->proTxHash))) { - LOCK(cs_main); - LogPrintf("CDummyDKG::%s -- failed to recover public key share, peer=%d\n", __func__, - from); - if (from != -1) { - Misbehaving(from, 100); - } - return; - } - - // verify sig share - if (!qc.quorumSig.VerifyInsecure(sharePk, commitmentHash)) { - LOCK(cs_main); - LogPrintf("CDummyDKG::%s -- invalid quorumSig, peer=%d\n", __func__, - from); - if (from != -1) { - Misbehaving(from, 100); - } - return; - } - - LogPrintf("CDummyDKG::%s -- processed dummy commitment for quorum %s:%d, validMembers=%d, signer=%d, peer=%d\n", __func__, - qc.quorumHash.ToString(), qc.llmqType, qc.CountValidMembers(), qc.signer, from); - - uint256 hash = ::SerializeHash(qc); - { - LOCK(sessionCs); - - // Mark all members as bad initially and clear that state when we receive a valid dummy commitment from them - // This information is then used in the next sessions to determine which ones are valid - if (curSessions[type].dummyCommitments.empty()) { - for (const auto& dmn : members) { - curSessions[type].badMembers.emplace(dmn->proTxHash); - } - } - - curSessions[type].badMembers.erase(signer->proTxHash); - curSessions[type].dummyCommitments[hash] = qc; - curSessions[type].dummyCommitmentsFromMembers[commitmentHash][signer->proTxHash] = hash; - } - - CInv inv(MSG_QUORUM_DUMMY_COMMITMENT, hash); - g_connman->RelayInv(inv, DMN_PROTO_VERSION); -} - -void CDummyDKG::UpdatedBlockTip(const CBlockIndex* pindex, bool fInitialDownload) -{ - if (fInitialDownload) { - return; - } - - if (!fMasternodeMode) { - return; - } - - bool fDIP0003Active = VersionBitsState(chainActive.Tip(), Params().GetConsensus(), Consensus::DEPLOYMENT_DIP0003, versionbitscache) == THRESHOLD_ACTIVE; - if (!fDIP0003Active) { - return; - } - - if (!sporkManager.IsSporkActive(SPORK_17_QUORUM_DKG_ENABLED)) { - return; - } - - for (const auto& p : Params().GetConsensus().llmqs) { - const auto& params = p.second; - int phaseIndex = pindex->nHeight % params.dkgInterval; - if (phaseIndex == 0) { - CreateDummyCommitment(params.type, pindex); - } else if (phaseIndex == params.dkgPhaseBlocks * 2) { - CreateFinalCommitment(params.type, pindex); - } - } -} - -void CDummyDKG::CreateDummyCommitment(Consensus::LLMQType llmqType, const CBlockIndex* pindex) -{ - const auto& params = Params().GetConsensus().llmqs.at(llmqType); - int quorumHeight = pindex->nHeight - (pindex->nHeight % params.dkgInterval); - const CBlockIndex* quorumIndex; - { - LOCK(cs_main); - quorumIndex = chainActive[quorumHeight]; - } - uint256 quorumHash = quorumIndex->GetBlockHash(); - - auto members = CLLMQUtils::GetAllQuorumMembers(llmqType, quorumHash); - if (members.size() != params.size) { - return; - } - - int myIdx = -1; - for (size_t i = 0; i < members.size(); i++) { - if (members[i]->collateralOutpoint == activeMasternodeInfo.outpoint) { - myIdx = (int)i; - break; - } - } - if (myIdx == -1) { - return; - } - auto signer = members[myIdx]; - - auto svec = BuildDeterministicSvec(llmqType, quorumHash); - auto vvec = BuildVvec(svec); - auto vvecHash = ::SerializeHash(vvec); - auto validMembers = GetValidMembers(llmqType, members); - - auto commitmentHash = CLLMQUtils::BuildCommitmentHash((uint8_t)llmqType, quorumHash, validMembers, vvec[0], vvecHash); - - CDummyCommitment qc; - qc.llmqType = (uint8_t)llmqType; - qc.quorumHash = quorumHash; - qc.signer = (uint16_t)myIdx; - qc.validMembers = validMembers; - - qc.membersSig = activeMasternodeInfo.blsKeyOperator->Sign(commitmentHash); - - CBLSSecretKey skShare; - if (!skShare.SecretKeyShare(svec, CBLSId::FromHash(signer->proTxHash))) { - return; - } - qc.quorumSig = skShare.Sign(commitmentHash); - - ProcessDummyCommitment(-1, qc); -} - -void CDummyDKG::CreateFinalCommitment(Consensus::LLMQType llmqType, const CBlockIndex* pindex) -{ - const auto& params = Params().GetConsensus().llmqs.at(llmqType); - int quorumHeight = pindex->nHeight - (pindex->nHeight % params.dkgInterval); - const CBlockIndex* quorumIndex; - { - LOCK(cs_main); - quorumIndex = chainActive[quorumHeight]; - } - uint256 quorumHash = quorumIndex->GetBlockHash(); - - auto members = CLLMQUtils::GetAllQuorumMembers(llmqType, quorumHash); - if (members.size() != params.size) { - return; - } - - auto svec = BuildDeterministicSvec(llmqType, quorumHash); - auto vvec = BuildVvec(svec); - auto vvecHash = ::SerializeHash(vvec); - - LOCK(sessionCs); - auto& session = curSessions[llmqType]; - - for (const auto& p : session.dummyCommitmentsFromMembers) { - const auto& commitmentHash = p.first; - if (p.second.size() < params.minSize) { - continue; - } - - const auto& firstQc = session.dummyCommitments[p.second.begin()->second]; - - CFinalCommitment fqc(params, quorumHash); - fqc.validMembers = firstQc.validMembers; - fqc.quorumPublicKey = vvec[0]; - fqc.quorumVvecHash = vvecHash; - - std::vector quorumSigs; - std::vector quorumSigIds; - std::vector memberSigs; - std::vector memberPubKeys; - - for (const auto& p2 : p.second) { - const auto& proTxHash = p2.first; - const auto& qc = session.dummyCommitments[p2.second]; - - int signerIdx = -1; - for (size_t i = 0; i < members.size(); i++) { - if (members[i]->proTxHash == proTxHash) { - signerIdx = (int)i; - break; - } - } - if (signerIdx == -1) { - break; - } - fqc.signers[signerIdx] = true; - if (quorumSigs.size() < params.threshold) { - quorumSigs.emplace_back(qc.quorumSig); - quorumSigIds.emplace_back(CBLSId::FromHash(proTxHash)); - } - memberSigs.emplace_back(qc.membersSig); - memberPubKeys.emplace_back(members[signerIdx]->pdmnState->pubKeyOperator); - } - - if (!fqc.quorumSig.Recover(quorumSigs, quorumSigIds)) { - LogPrintf("CDummyDKG::%s -- recovery failed for quorum %s:%d, validMembers=%d, signers=%d\n", __func__, - fqc.quorumHash.ToString(), fqc.llmqType, fqc.CountValidMembers(), fqc.CountSigners()); - continue; - } - fqc.membersSig = CBLSSignature::AggregateSecure(memberSigs, memberPubKeys, commitmentHash); - - if (!fqc.Verify(members, true)) { - LogPrintf("CDummyDKG::%s -- self created final commitment is invalid for quorum %s:%d, validMembers=%d, signers=%d\n", __func__, - fqc.quorumHash.ToString(), fqc.llmqType, fqc.CountValidMembers(), fqc.CountSigners()); - continue; - } - - LogPrintf("CDummyDKG::%s -- self created final commitment for quorum %s:%d, validMembers=%d, signers=%d\n", __func__, - fqc.quorumHash.ToString(), fqc.llmqType, fqc.CountValidMembers(), fqc.CountSigners()); - quorumBlockProcessor->AddMinableCommitment(fqc); - } - - prevSessions[llmqType].badMembers = curSessions[llmqType].badMembers; - prevSessions[llmqType].dummyCommitments = std::move(curSessions[llmqType].dummyCommitments); - prevSessions[llmqType].dummyCommitmentsFromMembers = std::move(curSessions[llmqType].dummyCommitmentsFromMembers); -} - -std::vector CDummyDKG::GetValidMembers(Consensus::LLMQType llmqType, const std::vector& members) -{ - const auto& params = Params().GetConsensus().llmqs.at(llmqType); - std::vector ret(params.size, false); - - LOCK(sessionCs); - - // Valid members are members that sent us a dummy commitment in the previous session - - for (size_t i = 0; i < params.size; i++) { - if (!prevSessions[llmqType].badMembers.count(members[i]->proTxHash)) { - ret[i] = true; - } - } - - // set all to valid if last sessions failed (this reboots everything) - if (std::count(ret.begin(), ret.end(), true) < params.minSize) { - for (size_t i = 0; i < params.size; i++) { - ret[i] = true; - } - } - return ret; -} - -// The returned secret key vector is NOT SECURE AT ALL!! -// It is known by everyone. This is only for testnet/devnet/regtest, so this is fine. Also, we won't do any meaningful -// things with the commitments. This is only needed to make the final commitments validate -BLSSecretKeyVector CDummyDKG::BuildDeterministicSvec(Consensus::LLMQType llmqType, const uint256& quorumHash) -{ - const auto& params = Params().GetConsensus().llmqs.at(llmqType); - - CHash256 seed_; - seed_.Write((unsigned char*)&llmqType, 1); - seed_.Write(quorumHash.begin(), quorumHash.size()); - - uint256 seed; - seed_.Finalize(seed.begin()); - - BLSSecretKeyVector svec; - svec.reserve(params.size); - for (size_t i = 0; i < params.threshold; i++) { - CBLSSecretKey sk; - while (true) { - seed = ::SerializeHash(seed); - sk.SetBuf(seed.begin(), seed.size()); - if (sk.IsValid()) { - break; - } - } - svec.emplace_back(sk); - } - - return svec; -} - -BLSPublicKeyVector CDummyDKG::BuildVvec(const BLSSecretKeyVector& svec) -{ - BLSPublicKeyVector vvec; - vvec.reserve(svec.size()); - for (size_t i = 0; i < svec.size(); i++) { - vvec.emplace_back(svec[i].GetPublicKey()); - } - return vvec; -} - -bool CDummyDKG::HasDummyCommitment(const uint256& hash) -{ - LOCK(sessionCs); - for (const auto& p : curSessions) { - auto it = p.second.dummyCommitments.find(hash); - if (it != p.second.dummyCommitments.end()) { - return true; - } - } - return false; -} - -bool CDummyDKG::GetDummyCommitment(const uint256& hash, CDummyCommitment& ret) -{ - LOCK(sessionCs); - for (const auto& p : curSessions) { - auto it = p.second.dummyCommitments.find(hash); - if (it != p.second.dummyCommitments.end()) { - ret = it->second; - return true; - } - } - return false; -} - -} diff --git a/src/llmq/quorums_dummydkg.h b/src/llmq/quorums_dummydkg.h deleted file mode 100644 index ca583f0ea39b..000000000000 --- a/src/llmq/quorums_dummydkg.h +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright (c) 2018 The Dash Core developers -// Distributed under the MIT/X11 software license, see the accompanying -// file COPYING or http://www.opensource.org/licenses/mit-license.php. - -#ifndef DASH_QUORUMS_DUMMYDKG_H -#define DASH_QUORUMS_DUMMYDKG_H - -#include "llmq/quorums_commitment.h" - -#include "consensus/params.h" -#include "net.h" -#include "primitives/transaction.h" -#include "sync.h" - -#include "bls/bls.h" - -#include - -class CNode; -class CConnman; - -/** - * Implementation of an insecure dummy DKG - * - * This is only used on testnet/devnet/regtest and will NEVER be used on - * mainnet. It is NOT SECURE AT ALL! It will actually be removed later when the real DKG is introduced. - * - * It works by using a deterministic secure vector as the secure polynomial. Everyone can calculate this - * polynomial by himself, which makes it insecure by definition. - * - * The purpose of this dummy implementation is to test final LLMQ commitments and simple PoSe on-chain. - * The dummy DKG first creates dummy commitments and propagates these to all nodes. They can then create - * a valid LLMQ commitment from these, which validates with the normal commitment validation code. - * - * After these have been mined on-chain, they are indistinguishable from commitments created from the real - * DKG, making them good enough for testing. - * - * The validMembers bitset is created from information of past dummy DKG sessions. If nodes failed to provide - * the dummy commitments, they will be marked as bad in the next session. This might create some chaos and - * finalizable commitments, but this is ok and will sort itself out after some sessions. - */ - -namespace llmq -{ - -// This message is only allowed on testnet/devnet/regtest -// If any peer tries to send this message on mainnet, it is banned immediately -// It is used to test commitments on testnet without actually running a full-blown DKG. -class CDummyCommitment -{ -public: - uint8_t llmqType{Consensus::LLMQ_NONE}; - uint256 quorumHash; - uint16_t signer{(uint16_t)-1}; - - std::vector validMembers; - - CBLSSignature quorumSig; - CBLSSignature membersSig; - -public: - int CountValidMembers() const - { - return (int)std::count(validMembers.begin(), validMembers.end(), true); - } - -public: - ADD_SERIALIZE_METHODS - - template - inline void SerializationOp(Stream& s, Operation ser_action) - { - READWRITE(llmqType); - READWRITE(quorumHash); - READWRITE(signer); - READWRITE(DYNBITSET(validMembers)); - READWRITE(quorumSig); - READWRITE(membersSig); - } -}; - -class CDummyDKGSession -{ -public: - std::set badMembers; - std::map dummyCommitments; - std::map> dummyCommitmentsFromMembers; -}; - -// It simulates the result of a DKG session by deterministically calculating a secret/public key vector -// !!!THIS IS NOT SECURE AT ALL AND WILL NEVER BE USED ON MAINNET!!! -// The whole dummy DKG will be removed when we add the real DKG -class CDummyDKG -{ -private: - CCriticalSection sessionCs; - std::map prevSessions; - std::map curSessions; - -public: - void ProcessMessage(CNode* pfrom, const std::string& strCommand, CDataStream& vRecv, CConnman& connman); - void ProcessDummyCommitment(NodeId from, const CDummyCommitment& qc); - - void UpdatedBlockTip(const CBlockIndex* pindex, bool fInitialDownload); - void CreateDummyCommitment(Consensus::LLMQType llmqType, const CBlockIndex* pindex); - void CreateFinalCommitment(Consensus::LLMQType llmqType, const CBlockIndex* pindex); - - bool HasDummyCommitment(const uint256& hash); - bool GetDummyCommitment(const uint256& hash, CDummyCommitment& ret); - -private: - std::vector GetValidMembers(Consensus::LLMQType llmqType, const std::vector& members); - BLSSecretKeyVector BuildDeterministicSvec(Consensus::LLMQType llmqType, const uint256& quorumHash); - BLSPublicKeyVector BuildVvec(const BLSSecretKeyVector& svec); -}; - -extern CDummyDKG* quorumDummyDKG; - -} - -#endif//DASH_QUORUMS_DUMMYDKG_H diff --git a/src/llmq/quorums_init.cpp b/src/llmq/quorums_init.cpp index fa29034601f3..2afb5c11b7b0 100644 --- a/src/llmq/quorums_init.cpp +++ b/src/llmq/quorums_init.cpp @@ -4,23 +4,39 @@ #include "quorums_init.h" +#include "quorums.h" #include "quorums_blockprocessor.h" #include "quorums_commitment.h" -#include "quorums_dummydkg.h" +#include "quorums_debug.h" +#include "quorums_dkgsessionmgr.h" +#include "quorums_signing.h" + +#include "scheduler.h" namespace llmq { -void InitLLMQSystem(CEvoDB& evoDb) +static CBLSWorker blsWorker; + +void InitLLMQSystem(CEvoDB& evoDb, CScheduler* scheduler) { quorumBlockProcessor = new CQuorumBlockProcessor(evoDb); - quorumDummyDKG = new CDummyDKG(); + quorumDKGSessionManager = new CDKGSessionManager(evoDb, blsWorker); + quorumManager = new CQuorumManager(evoDb, blsWorker, *quorumDKGSessionManager); + quorumsSigningManager = new CSigningManager(evoDb, blsWorker); + quorumDKGDebugManager = new CDKGDebugManager(scheduler); } void DestroyLLMQSystem() { - delete quorumDummyDKG; - quorumDummyDKG = nullptr; + delete quorumDKGDebugManager; + quorumDKGDebugManager = nullptr; + delete quorumsSigningManager; + quorumsSigningManager = NULL; + delete quorumManager; + quorumManager = NULL; + delete quorumDKGSessionManager; + quorumDKGSessionManager = NULL; delete quorumBlockProcessor; quorumBlockProcessor = nullptr; } diff --git a/src/llmq/quorums_init.h b/src/llmq/quorums_init.h index 137248b88546..9a98c2b16aa8 100644 --- a/src/llmq/quorums_init.h +++ b/src/llmq/quorums_init.h @@ -6,11 +6,12 @@ #define DASH_QUORUMS_INIT_H class CEvoDB; +class CScheduler; namespace llmq { -void InitLLMQSystem(CEvoDB& evoDb); +void InitLLMQSystem(CEvoDB& evoDb, CScheduler* scheduler); void DestroyLLMQSystem(); } diff --git a/src/llmq/quorums_instantx.cpp b/src/llmq/quorums_instantx.cpp new file mode 100644 index 000000000000..67cb60157e0d --- /dev/null +++ b/src/llmq/quorums_instantx.cpp @@ -0,0 +1,64 @@ +// Copyright (c) 2017 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "quorums_instantx.h" + +#include "chainparams.h" + +namespace llmq +{ + +CInstantXManager quorumInstantXManager; + +void CInstantXManager::ProcessTx(CNode* pfrom, const CTransaction& tx, CConnman& connman, const Consensus::Params& params) +{ + auto llmqType = params.llmqForInstantSend; + if (llmqType == Consensus::LLMQ_NONE) { + return; + } + + if (fMasternodeMode) { + auto quorum = quorumManager->GetNewestQuorum(llmqType); + if (quorum != nullptr) { + for (const auto& in : tx.vin) { + uint256 id = ::SerializeHash(in.prevout); + quorumsSigningManager->AsyncSignIfMember(llmqType, id, tx.GetHash()); + } + } + } +} + +bool CInstantXManager::IsLocked(const CTransaction& tx, const Consensus::Params& params) +{ + auto llmqType = params.llmqForInstantSend; + if (llmqType == Consensus::LLMQ_NONE) { + return false; + } + + for (const auto& in : tx.vin) { + uint256 id = ::SerializeHash(in.prevout); + if (!quorumsSigningManager->HasRecoveredSig(llmqType, id, tx.GetHash())) { + return false; + } + } + return true; +} + +bool CInstantXManager::IsConflicting(const CTransaction& tx, const Consensus::Params& params) +{ + auto llmqType = params.llmqForInstantSend; + if (llmqType == Consensus::LLMQ_NONE) { + return false; + } + + for (const auto& in : tx.vin) { + uint256 id = ::SerializeHash(in.prevout); + if (quorumsSigningManager->IsConflicting(llmqType, id, tx.GetHash())) { + return true; + } + } + return false; +} + +} diff --git a/src/llmq/quorums_instantx.h b/src/llmq/quorums_instantx.h new file mode 100644 index 000000000000..e9949cc3e5e6 --- /dev/null +++ b/src/llmq/quorums_instantx.h @@ -0,0 +1,30 @@ +// Copyright (c) 2017 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef DASH_QUORUMS_INSTANTX_H +#define DASH_QUORUMS_INSTANTX_H + +#include "quorums_signing.h" + +#include "primitives/transaction.h" + +namespace llmq +{ + +class CInstantXManager +{ +private: + CCriticalSection cs; + +public: + void ProcessTx(CNode* pfrom, const CTransaction& tx, CConnman& connman, const Consensus::Params& params); + bool IsLocked(const CTransaction& tx, const Consensus::Params& params); + bool IsConflicting(const CTransaction& tx, const Consensus::Params& params); +}; + +extern CInstantXManager quorumInstantXManager; + +} + +#endif//DASH_QUORUMS_INSTANTX_H diff --git a/src/llmq/quorums_signing.cpp b/src/llmq/quorums_signing.cpp new file mode 100644 index 000000000000..4fe4d896ad65 --- /dev/null +++ b/src/llmq/quorums_signing.cpp @@ -0,0 +1,1727 @@ +// Copyright (c) 2018 The Dash Core developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "quorums_signing.h" +#include "validation.h" +#include "activemasternode.h" + +#include "net_processing.h" +#include "netmessagemaker.h" +#include "scheduler.h" +#include "cxxtimer.hpp" + +#include "init.h" + +#include +#include + +namespace llmq +{ + +static uint256 MakeSignHash(Consensus::LLMQType llmqType, const uint256& quorumHash, const uint256& id, const uint256& msgHash) +{ + CHashWriter h(SER_GETHASH, 0); + h << (uint8_t)llmqType; + h << quorumHash; + h << id; + h << msgHash; + return h.GetHash(); +} + +// works for sig shares and recovered sigs +template +static uint256 MakeSignHash(const T& s) +{ + return MakeSignHash((Consensus::LLMQType)s.llmqType, s.quorumHash, s.id, s.msgHash); +} + +template +static std::pair FindBySignHash(const M& m, const uint256& signHash) +{ + return std::make_pair( + m.lower_bound(std::make_pair(signHash, (uint16_t)0)), + m.upper_bound(std::make_pair(signHash, std::numeric_limits::max())) + ); +} +template +static size_t CountBySignHash(const M& m, const uint256& signHash) +{ + auto itPair = FindBySignHash(m, signHash); + size_t count = 0; + while (itPair.first != itPair.second) { + count++; + ++itPair.first; + } + return count; +} + +template +static void EraseBySignHash(M& m, const uint256& signHash) +{ + auto itPair = FindBySignHash(m, signHash); + m.erase(itPair.first, itPair.second); +} + +CSigningManager* quorumsSigningManager; + +CSigningManager::CSigningManager(CEvoDB& _evoDb, CBLSWorker& _blsWorker) : + evoDb(_evoDb), + blsWorker(_blsWorker), + workQueue(0) +{ + workThread = std::thread([this]() { + RenameThread("quorum-signing"); + WorkThreadMain(); + }); +} + +CSigningManager::~CSigningManager() +{ + { + std::unique_lock l(workQueueMutex); + stopWorkThread = true; + } + if (workThread.joinable()) { + workThread.join(); + } +} + +bool CSigningManager::AlreadyHave(const CInv& inv) +{ + LOCK(cs); + return inv.type == MSG_QUORUM_RECOVERED_SIG && recoveredSigs.count(inv.hash); +} + +bool CSigningManager::GetRecoveredSig(const uint256& hash, CRecoveredSig& ret) +{ + LOCK(cs); + auto it = recoveredSigs.find(hash); + if (it == recoveredSigs.end()) { + return false; + } + ret = it->second; + return true; +} + +void CSigningManager::ProcessMessage(CNode* pfrom, const std::string& strCommand, CDataStream& vRecv, CConnman& connman) +{ + // non-masternodes are only interested in recovered signatures + if (strCommand != NetMsgType::QSIGREC && (!fMasternodeMode || activeMasternodeInfo.proTxHash.IsNull())) { + return; + } + + if (strCommand == NetMsgType::QSIGSHARESINV) { + CSigSharesInv inv; + vRecv >> inv; + ProcessMessageSigSharesInv(pfrom, inv, connman); + } else if (strCommand == NetMsgType::QGETSIGSHARES) { + CSigSharesInv inv; + vRecv >> inv; + ProcessMessageGetSigShares(pfrom, inv, connman); + } else if (strCommand == NetMsgType::QBSIGSHARES) { + CBatchedSigShares batchedSigShares; + vRecv >> batchedSigShares; + ProcessMessageBatchedSigShares(pfrom, batchedSigShares, connman); + } else if (strCommand == NetMsgType::QSIGREC) { + CRecoveredSig recoveredSig; + vRecv >> recoveredSig; + ProcessMessageRecoveredSig(pfrom, recoveredSig, connman); + } +} + +bool CSigningManager::VerifySigSharesInv(NodeId from, const CSigSharesInv& inv) +{ + Consensus::LLMQType llmqType = (Consensus::LLMQType)inv.llmqType; + if (!Params().GetConsensus().llmqs.count(llmqType) || inv.signHash.IsNull()) { + LOCK(cs_main); + Misbehaving(from, 100); + return false; + } + + if (!fMasternodeMode || activeMasternodeInfo.proTxHash.IsNull()) { + return false; + } + + size_t quorumSize = (size_t)Params().GetConsensus().llmqs.at(llmqType).size; + + if (inv.inv.size() != quorumSize) { + LOCK(cs_main); + Misbehaving(from, 100); + return false; + } + return true; +} + +void CSigningManager::ProcessMessageSigSharesInv(CNode* pfrom, const CSigSharesInv& inv, CConnman& connman) +{ + if (!VerifySigSharesInv(pfrom->id, inv)) { + return; + } + + LOCK(cs); + if (recoveredSessions.count(inv.signHash)) { + return; + } + + LogPrint("llmq", "CSigningManager::%s -- inv={%s}, node=%d\n", __func__, inv.ToString(), pfrom->id); + + auto& nodeState = nodeStates[pfrom->id]; + nodeState.MarkAnnounced(inv.signHash, inv); + nodeState.MarkKnows(inv.signHash, inv); +} + +void CSigningManager::ProcessMessageGetSigShares(CNode* pfrom, const CSigSharesInv& inv, CConnman& connman) +{ + if (!VerifySigSharesInv(pfrom->id, inv)) { + return; + } + + LOCK(cs); + if (recoveredSessions.count(inv.signHash)) { + return; + } + + LogPrint("llmq", "CSigningManager::%s -- inv={%s}, node=%d\n", __func__, inv.ToString(), pfrom->id); + + auto& nodeState = nodeStates[pfrom->id]; + nodeState.MarkRequested(inv.signHash, inv); + nodeState.MarkKnows(inv.signHash, inv); +} + +void CSigningManager::ProcessMessageBatchedSigShares(CNode* pfrom, const llmq::CBatchedSigShares& batchedSigShares, CConnman& connman) +{ + bool ban = false; + if (!PreVerifyBatchedSigShares(pfrom->id, batchedSigShares, ban)) { + if (ban) { + LOCK(cs_main); + Misbehaving(pfrom->id, 100); + return; + } + return; + } + + std::vector sigShares; + sigShares.reserve(batchedSigShares.sigShares.size()); + + LOCK(cs); + auto& nodeState = nodeStates[pfrom->id]; + + for (size_t i = 0; i < batchedSigShares.sigShares.size(); i++) { + CSigShare sigShare = batchedSigShares.RebuildSigShare(i); + nodeState.requestedSigShares.erase(sigShare.GetKey()); + + // TODO track invalid sig shares received? + // It's important to only skip seen *valid* sig shares here. If a node sends us a + // batch of mostly valid sig shares with a single invalid one and thus batched + // verification fails, we'd skip the valid ones in the future if received from other nodes + if (this->sigShares.count(sigShare.GetKey())) { + continue; + } + if (recoveredSigsForIds.count(std::make_pair((Consensus::LLMQType)sigShare.llmqType, sigShare.id))) { + // skip sig share if we already have a recovered sig + continue; + } + sigShares.emplace_back(sigShare); + } + + LogPrint("llmq", "CSigningManager::%s -- shares=%d, new=%d, inv={%s}, node=%d\n", __func__, + batchedSigShares.sigShares.size(), sigShares.size(), batchedSigShares.ToInv().ToString(), pfrom->id); + + if (sigShares.empty()) { + return; + } + + for (auto& s : sigShares) { + nodeState.pendingIncomingSigShares.emplace(s.GetKey(), s); + } +} + +void CSigningManager::ProcessMessageRecoveredSig(CNode* pfrom, const llmq::CRecoveredSig& recoveredSig, CConnman& connman) +{ + bool ban = false; + if (!PreVerifyRecoveredSig(pfrom->id, recoveredSig, ban)) { + if (ban) { + LOCK(cs_main); + Misbehaving(pfrom->id, 100); + return; + } + return; + } + + LOCK(cs); + // It's important to only skip seen *valid* sig shares here. See comment for CBatchedSigShare + // We don't receive recovered sigs in batches, but we do batched verification per node on these + if (recoveredSigs.count(recoveredSig.GetHash())) { + return; + } + + LogPrint("llmq", "CSigningManager::%s -- signHash=%s, node=%d\n", __func__, MakeSignHash(recoveredSig).ToString(), pfrom->id); + + auto& nodeState = nodeStates[pfrom->id]; + nodeState.pendingIncomingRecSigs.emplace(recoveredSig.GetHash(), recoveredSig); +} + +bool CSigningManager::PreVerifyBatchedSigShares(NodeId nodeId, const llmq::CBatchedSigShares& batchedSigShares, bool& retBan) +{ + retBan = false; + + auto llmqType = (Consensus::LLMQType)batchedSigShares.llmqType; + if (!Params().GetConsensus().llmqs.count(llmqType)) { + retBan = true; + return false; + } + + CQuorumCPtr quorum; + { + LOCK(cs_main); + + quorum = quorumManager->GetQuorum(llmqType, batchedSigShares.quorumHash); + if (!quorum) { + LogPrintf("CSigningManager::%s -- quorum %s not found, node=%d\n", __func__, + batchedSigShares.quorumHash.ToString(), nodeId); + return false; + } + if (!IsQuorumActive(llmqType, quorum->quorumHash)) { + return false; + } + if (!quorum->IsMember(activeMasternodeInfo.proTxHash)) { + return false; + } + if (quorum->quorumVvec == nullptr) { + // TODO we should allow to ask other nodes for the quorum vvec + LogPrintf("CSigningManager::%s -- we don't have the quorum vvec for %s, no verification possible. node=%d\n", __func__, + batchedSigShares.quorumHash.ToString(), nodeId); + return false; + } + } + + std::set dupMembers; + + for (size_t i = 0; i < batchedSigShares.sigShares.size(); i++) { + auto quorumMember = batchedSigShares.sigShares[i].first; + auto& sigShare = batchedSigShares.sigShares[i].second; + if (!sigShare.IsValid()) { + // TODO banning might be bad here actually + retBan = true; + return false; + } + if (!dupMembers.emplace(quorumMember).second) { + // TODO banning might be bad here actually + retBan = true; + return false; + } + + if (quorumMember >= quorum->members.size()) { + LogPrintf("CSigningManager::%s -- quorumMember out of bounds\n", __func__); + // TODO banning might be bad here actually + retBan = true; + return false; + } + if (!quorum->validMembers[quorumMember]) { + LogPrintf("CSigningManager::%s -- quorumMember not valid\n", __func__); + // TODO banning might be bad here actually + retBan = true; + return false; + } + } + return true; +} + +bool CSigningManager::PreVerifyRecoveredSig(NodeId nodeId, const llmq::CRecoveredSig& recoveredSig, bool& retBan) +{ + retBan = false; + + auto llmqType = (Consensus::LLMQType)recoveredSig.llmqType; + if (!Params().GetConsensus().llmqs.count(llmqType)) { + retBan = true; + return false; + } + + CQuorumCPtr quorum; + { + LOCK(cs_main); + + quorum = quorumManager->GetQuorum(llmqType, recoveredSig.quorumHash); + if (!quorum) { + LogPrintf("CSigningManager::%s -- quorum %s not found, node=%d\n", __func__, + recoveredSig.quorumHash.ToString(), nodeId); + return false; + } + if (!IsQuorumActive(llmqType, quorum->quorumHash)) { + return false; + } + } + + if (!recoveredSig.sig.IsValid()) { + // TODO banning might be bad here actually + retBan = true; + return false; + } + + return true; +} + +void CSigningManager::ProcessPendingIncomingSigs(CConnman& connman) +{ + // process recovered sigs first, as they might result in pending sig shares becoming + // obsolete and thus we can speed up processing + ProcessPendingRecoveredSigs(connman); + ProcessPendingSigShares(connman); +} + +template +static void IterateNodeStatesRandom(std::map& nodeStates, Continue&& cont, Callback&& callback, FastRandomContext& rnd) +{ + std::vector::iterator> rndNodeStates; + rndNodeStates.reserve(nodeStates.size()); + for (auto it = nodeStates.begin(); it != nodeStates.end(); ++it) { + rndNodeStates.emplace_back(it); + } + if (rndNodeStates.empty()) { + return; + } + std::random_shuffle(rndNodeStates.begin(), rndNodeStates.end(), rnd); + + size_t idx = 0; + while (!rndNodeStates.empty() && cont()) { + auto nodeId = rndNodeStates[idx]->first; + auto& ns = rndNodeStates[idx]->second; + + if (callback(nodeId, ns)) { + idx = (idx + 1) % rndNodeStates.size(); + } else { + rndNodeStates.erase(rndNodeStates.begin() + idx); + if (rndNodeStates.empty()) { + break; + } + idx %= rndNodeStates.size(); + } + } +} + +void CSigningManager::ProcessPendingSigShares(CConnman& connman) +{ + std::map> groupedByNode; + { + LOCK(cs); + if (nodeStates.empty()) { + return; + } + + std::set> uniqueSignHashes; + IterateNodeStatesRandom(nodeStates, [&]() { + return uniqueSignHashes.size() < 32; + }, [&](NodeId nodeId, CSigSharesNodeState& ns) { + if (ns.pendingIncomingSigShares.empty()) { + return false; + } + auto& sigShare = ns.pendingIncomingSigShares.begin()->second; + + bool alreadyHave = this->sigShares.count(sigShare.GetKey()) || + recoveredSigsForIds.count(std::make_pair((Consensus::LLMQType)sigShare.llmqType, sigShare.id)); + if (!alreadyHave) { + uniqueSignHashes.emplace(nodeId, sigShare.GetSignHash()); + groupedByNode[nodeId].emplace_back(sigShare); + } + ns.pendingIncomingSigShares.erase(ns.pendingIncomingSigShares.begin()); + return !ns.pendingIncomingSigShares.empty(); + }, rnd); + + if (groupedByNode.empty()) { + return; + } + } + + std::map, CQuorumCPtr> quorums; + { + LOCK(cs_main); + for (auto& p : groupedByNode) { + for (auto& sigShare : p.second) { + auto llmqType = (Consensus::LLMQType) sigShare.llmqType; + + auto k = std::make_pair(llmqType, sigShare.quorumHash); + if (quorums.count(k)) { + continue; + } + + CQuorumCPtr quorum = quorumManager->GetQuorum(llmqType, sigShare.quorumHash); + assert(quorum != nullptr); + quorums.emplace(k, quorum); + } + } + } + + // TODO describe this + + const size_t batchedVerifyCount = 8; + std::set invalidNodes; + + while (!groupedByNode.empty()) { + std::map> batched; + size_t sigShareCount = 0; + for (size_t i = 0; i < batchedVerifyCount && !groupedByNode.empty(); i++) { + auto it = groupedByNode.begin(); + NodeId nodeId = it->first; + auto& sigShares = it->second; + + sigShareCount += sigShares.size(); + batched.emplace(nodeId, std::move(sigShares)); + groupedByNode.erase(it); + } + + std::vector verifySigShares; + std::set dupSet; + verifySigShares.reserve(sigShareCount); + { + LOCK(cs); + for (auto& p : batched) { + for (auto& sigShare : p.second) { + if (!dupSet.emplace(sigShare.GetKey()).second) { + // don't include duplicates from different nodes + // if there are duplicates where one of both is invalid, batch verification will fail and we'll + // revert to per-node verification to figure out which one is the valid one + continue; + } + if (this->sigShares.count(sigShare.GetKey())) { + // previous batch validated this one + continue; + } + if (recoveredSigsForIds.count(std::make_pair((Consensus::LLMQType)sigShare.llmqType, sigShare.id))) { + // previous batch resulted in recovery + continue; + } + verifySigShares.emplace_back(sigShare); + } + } + } + + std::set validNodes; + // if verifySigShares is empty, a previous batch verified all sig shares from the current batch, + // so the current batch is also valid + bool batchValid = verifySigShares.empty() || VerifyPendingSigShares(verifySigShares, quorums); + if (!batchValid) { + // at least one node sent at least one invalid sig share, let's figure out who it was + if (batched.size() == 1) { + auto nodeId = batched.begin()->first; + invalidNodes.emplace(nodeId); + } else { + for (auto& p : batched) { + auto nodeId = p.first; + auto& sigShares = p.second; + bool valid = VerifyPendingSigShares(sigShares, quorums); + if (!valid) { + invalidNodes.emplace(nodeId); + } else { + validNodes.emplace(nodeId); + } + } + } + } else { + // all valid + for (auto& p : batched) { + validNodes.emplace(p.first); + } + } + + if (!invalidNodes.empty()) { + LOCK(cs_main); + for (auto nodeId : invalidNodes) { + LogPrintf("CSigningManager::%s -- invalid sig shares. banning node=%d\n", __func__, + nodeId); + Misbehaving(nodeId, 100); + } + } + + for (auto nodeId : validNodes) { + auto& sigShares = batched.at(nodeId); + ProcessPendingSigSharesFromNode(nodeId, sigShares, quorums, connman); + } + } +} + +void CSigningManager::ProcessPendingRecoveredSigs(CConnman& connman) +{ + std::map> groupedByNode; + { + LOCK(cs); + if (nodeStates.empty()) { + return; + } + + std::set> uniqueSignHashes; + IterateNodeStatesRandom(nodeStates, [&]() { + return uniqueSignHashes.size() < 32; + }, [&](NodeId nodeId, CSigSharesNodeState& ns) { + if (ns.pendingIncomingRecSigs.empty()) { + return false; + } + auto& recSig = ns.pendingIncomingRecSigs.begin()->second; + + bool alreadyHave = this->recoveredSigs.count(recSig.GetHash()) != 0; + if (!alreadyHave) { + uniqueSignHashes.emplace(nodeId, MakeSignHash(recSig)); + groupedByNode[nodeId].emplace_back(recSig); + } + ns.pendingIncomingRecSigs.erase(ns.pendingIncomingRecSigs.begin()); + return !ns.pendingIncomingRecSigs.empty(); + }, rnd); + + if (groupedByNode.empty()) { + return; + } + } + + std::map, CQuorumCPtr> quorums; + { + LOCK(cs_main); + for (auto& p : groupedByNode) { + NodeId nodeId = p.first; + auto& v = p.second; + + for (auto it = v.begin(); it != v.end();) { + auto& recSig = *it; + + Consensus::LLMQType llmqType = (Consensus::LLMQType) recSig.llmqType; + auto quorumKey = std::make_pair((Consensus::LLMQType)recSig.llmqType, recSig.quorumHash); + if (!quorums.count(quorumKey)) { + //TODO only recent quorums + CQuorumCPtr quorum = quorumManager->GetQuorum(llmqType, recSig.quorumHash); + if (!quorum) { + LogPrintf("CSigningManager::%s -- quorum %s not found, node=%d\n", __func__, + recSig.quorumHash.ToString(), nodeId); + it = v.erase(it); + continue; + } + if (!IsQuorumActive(llmqType, quorum->quorumHash)) { + LogPrintf("CSigningManager::%s -- quorum %s not active anymore, node=%d\n", __func__, + recSig.quorumHash.ToString(), nodeId); + it = v.erase(it); + continue; + } + + quorums.emplace(quorumKey, quorum); + } + + ++it; + } + } + } + + for (auto& p : groupedByNode) { + NodeId nodeId = p.first; + auto& v = p.second; + + if (!v.empty()) { + // We do batched verification and processing per node, but this means that we might have duplicates + // between nodes. We could not remove these duplicates before as it might result in nodes tricking us into + // skipping non-processes recovered sigs. However, after the calls to ProcessRecoveredSig in this loop + // we might know more valid recovered sigs which we can safely skip + LOCK(cs); + v.remove_if([this](const CRecoveredSig& rs) { + return this->recoveredSigs.count(rs.GetHash()); + }); + } + if (v.empty()) { + continue; + } + + ProcessPendingRecoveredSigsFromNode(nodeId, v, quorums, connman); + } +} + +bool CSigningManager::VerifyPendingSigShares(const std::vector& sigShares, const std::map, CQuorumCPtr>& quorums) +{ + std::map> groupedBySignHash; + + for (size_t i = 0; i < sigShares.size(); i++) { + auto& sigShare = sigShares[i]; + groupedBySignHash[sigShare.GetSignHash()].emplace(i); + } + + // insecure verification is fine here as the public key shares are trusted due to the DKG protocol + // (no rogue public key attack possible) + + CBLSSignature aggSig; + std::vector pubKeys; + std::vector hashes; + + cxxtimer::Timer aggTimer(false); + cxxtimer::Timer verifyTimer(false); + cxxtimer::Timer totalTimer(true); + + aggTimer.start(); + for (auto& p2 : groupedBySignHash) { + auto& signHash = p2.first; + auto& v = p2.second; + + CBLSSignature aggSig2; + CBLSPublicKey aggPubKey2; + + for (auto idx : v) { + auto& sigShare = sigShares[idx]; + auto quorumKey = std::make_pair((Consensus::LLMQType)sigShare.llmqType, sigShare.quorumHash); + auto pubKeyShare = quorums.at(quorumKey)->GetPubKeyShare(sigShare.quorumMember); + if (!pubKeyShare.IsValid()) { + // this should really not happen (we already ensured we have the quorum vvec, + // so we should also be able to create all pubkey shares) + return false; + } + if (!aggSig2.IsValid()) { + aggSig2 = sigShare.sigShare; + aggPubKey2 = pubKeyShare; + } else { + aggSig2.AggregateInsecure(sigShare.sigShare); + aggPubKey2.AggregateInsecure(pubKeyShare); + } + if (!aggSig2.IsValid() || !aggPubKey2.IsValid()) { + return false; + } + } + + if (pubKeys.empty()) { + aggSig = aggSig2; + } else { + aggSig.AggregateInsecure(aggSig2); + } + if (!aggSig.IsValid()) { + return false; + } + pubKeys.emplace_back(aggPubKey2); + hashes.emplace_back(signHash); + } + aggTimer.stop(); + + std::string invStr = ""; + if (LogAcceptCategory("llmq")) { + std::map invs; + for (auto& s : sigShares) { + auto& inv = invs[s.GetSignHash()]; + if (inv.inv.empty()) { + inv.Init((Consensus::LLMQType) s.llmqType, s.GetSignHash()); + } + inv.inv[s.quorumMember] = true; + } + for (auto& p : invs) { + if (!invStr.empty()) { + invStr += ", "; + } + invStr += strprintf("{%s}", p.second.ToString()); + } + } + + verifyTimer.start(); + bool valid = aggSig.VerifyInsecureAggregated(pubKeys, hashes); + verifyTimer.stop(); + if (!valid) { + LogPrint("llmq", "CSigningManager::%s -- invalid sig shares. count=%d, invs={%s}, at=%d, vt=%d, tt=%d\n", __func__, + sigShares.size(), invStr, aggTimer.count(), verifyTimer.count(), totalTimer.count()); + } else { + LogPrint("llmq", "CSigningManager::%s -- valid sig shares. count=%d, invs={%s}, at=%d, vt=%d, tt=%d\n", __func__, + sigShares.size(), invStr, aggTimer.count(), verifyTimer.count(), totalTimer.count()); + } + + return valid; +} + +// It's ensured that no duplicates are passed to this method +void CSigningManager::ProcessPendingSigSharesFromNode(NodeId nodeId, const std::vector& sigShares, const std::map, CQuorumCPtr>& quorums, CConnman& connman) +{ + auto& nodeState = nodeStates[nodeId]; + + cxxtimer::Timer t(true); + for (auto& sigShare : sigShares) { + // he sent us some valid sig shares, so he must be part of this quorum and is thus interested in our sig shares as well + // if this is the first time we received a sig share from this node, we won't announce the currently locally known sig shares to him. + // only the upcoming sig shares will be announced to him. this means the first signing session for a fresh quorum will be a bit + // slower than for older ones. TODO: fix this (risk of DoS when announcing all at once?) + auto quorumKey = std::make_pair((Consensus::LLMQType)sigShare.llmqType, sigShare.quorumHash); + nodeState.interestedIn.emplace(quorumKey); + + ProcessSigShare(nodeId, sigShare, connman, quorums.at(quorumKey)); + } + t.stop(); + + LogPrint("llmq", "CSigningManager::%s -- processed sigShare batch. shares=%d, time=%d, node=%d\n", __func__, + sigShares.size(), t.count(), nodeId); +} + +// It's ensured that no duplicates are passed to this method +void CSigningManager::ProcessPendingRecoveredSigsFromNode(NodeId nodeId, const std::list& recoveredSigs, + const std::map, CQuorumCPtr>& quorums, + CConnman& connman) +{ + const size_t batchedVerifyCount = 8; + + CBLSSignature aggSig; + std::vector pubKeys; + std::vector signHashes; + pubKeys.reserve(recoveredSigs.size()); + signHashes.reserve(recoveredSigs.size()); + + cxxtimer::Timer verifyTimer(false); + size_t verifyCount = 0; + auto verifyBatch = [&]() { + verifyTimer.start(); + if (!aggSig.VerifyInsecureAggregated(pubKeys, signHashes)) { + LogPrintf("CSigningManager::ProcessPendingRecoveredSigsFromNode -- invalid recovered sigs. time=%d, node=%d\n", verifyTimer.count(), nodeId); + LOCK(cs_main); + Misbehaving(nodeId, 100); + return false; + } + verifyTimer.stop(); + verifyCount += pubKeys.size(); + aggSig = CBLSSignature(); + pubKeys.clear(); + signHashes.clear(); + return true; + }; + + for (auto& rs : recoveredSigs) { + auto signHash = MakeSignHash(rs); + + if (pubKeys.empty()) { + aggSig = rs.sig; + } else { + aggSig.AggregateInsecure(rs.sig); + } + if (!aggSig.IsValid()) { + { + LOCK(cs_main); + Misbehaving(nodeId, 100); + } + return; + } + + signHashes.emplace_back(signHash); + + auto& quorum = quorums.at(std::make_pair((Consensus::LLMQType)rs.llmqType, rs.quorumHash)); + pubKeys.emplace_back(quorum->quorumPublicKey); + + // we verify in batches, as otherwise an attacker might send thousands of messages at once and force us to + // perform thousands of BLS pairings, effectively DoS'ing us + // As the attacker can't craft valid signatures, he'll only be able to send us a few valid ones before we + // encounter the first invalid one, so the attack is detected early and the attacker banned + if (pubKeys.size() >= batchedVerifyCount) { + if (!verifyBatch()) { + return; + } + } + } + // Verify remaining signatures + if (!pubKeys.empty() && !verifyBatch()) { + return; + } + LogPrintf("CSigningManager::%s -- verified recovered sig(s). count=%d, vt=%d, node=%d\n", __func__, verifyCount, verifyTimer.count(), nodeId); + + for (auto& rs : recoveredSigs) { + auto& quorum = quorums.at(std::make_pair((Consensus::LLMQType)rs.llmqType, rs.quorumHash)); + ProcessRecoveredSig(nodeId, rs, quorum, connman); + } +} + +// sig shares are already verified when entering this method +void CSigningManager::ProcessSigShare(NodeId nodeId, const CSigShare& sigShare, CConnman& connman, const CQuorumCPtr& quorum) +{ + auto llmqType = quorum->params.type; + + bool canTryRecovery = false; + + // prepare node set for direct-push in case this is our sig share + std::set quorumNodes; + if (sigShare.quorumMember == quorum->GetMemberIndex(activeMasternodeInfo.proTxHash)) { + quorumNodes = connman.GetMasternodeQuorumNodes((Consensus::LLMQType) sigShare.llmqType, sigShare.quorumHash); + // make sure node states are created for these nodes (we might have not received any message from these yet) + for (auto otherNodeId : quorumNodes) { + nodeStates[otherNodeId].interestedIn.emplace(std::make_pair((Consensus::LLMQType)sigShare.llmqType, sigShare.quorumHash)); + } + } + + { + LOCK(cs); + if (recoveredSigsForIds.count(std::make_pair(llmqType, sigShare.id))) { + return; + } + + sigShares.emplace(sigShare.GetKey(), sigShare); + sigSharesToAnnounce.emplace(sigShare.GetKey()); + firstSeenForSessions.emplace(sigShare.GetSignHash(), GetTimeMillis()); + + if (!quorumNodes.empty()) { + // don't announce and wait for other nodes to request this share and directly send it to them + // there is no way the other nodes know about this share as this is the one created on this node + // this will also indicate interest to the other nodes in sig shares for this quorum + for (auto& p : nodeStates) { + if (!quorumNodes.count(p.first) && !p.second.interestedIn.count(std::make_pair((Consensus::LLMQType)sigShare.llmqType, sigShare.quorumHash))) { + continue; + } + p.second.MarkRequested((Consensus::LLMQType)sigShare.llmqType, sigShare.GetSignHash(), sigShare.quorumMember); + p.second.MarkKnows((Consensus::LLMQType)sigShare.llmqType, sigShare.GetSignHash(), sigShare.quorumMember); + } + } + + size_t sigShareCount = CountBySignHash(sigShares, sigShare.GetSignHash()); + if (sigShareCount >= quorum->params.threshold) { + canTryRecovery = true; + } + } + + if (canTryRecovery) { + TryRecoverSig(quorum, sigShare.id, sigShare.msgHash, connman); + } +} + +void CSigningManager::TryRecoverSig(const CQuorumCPtr& quorum, const uint256& id, const uint256& msgHash, CConnman& connman) +{ + std::vector sigSharesForRecovery; + std::vector idsForRecovery; + { + LOCK(cs); + + auto k = std::make_pair(quorum->params.type, id); + + if (recoveredSigsForIds.count(k)) { + return; + } + + auto signHash = MakeSignHash(quorum->params.type, quorum->quorumHash, id, msgHash); + auto itPair = FindBySignHash(sigShares, signHash); + + sigSharesForRecovery.reserve((size_t) quorum->params.threshold); + idsForRecovery.reserve((size_t) quorum->params.threshold); + for (auto it = itPair.first; it != itPair.second && sigSharesForRecovery.size() < quorum->params.threshold; ++it) { + auto& sigShare = it->second; + sigSharesForRecovery.emplace_back(sigShare.sigShare); + idsForRecovery.emplace_back(CBLSId::FromHash(quorum->members[sigShare.quorumMember]->proTxHash)); + } + + // check if we can recover the final signature + if (sigSharesForRecovery.size() < quorum->params.threshold) { + return; + } + } + + // now recover it + cxxtimer::Timer t(true); + CBLSSignature recoveredSig; + if (!recoveredSig.Recover(sigSharesForRecovery, idsForRecovery)) { + LogPrintf("CSigningManager::%s -- failed to recover signature. id=%s, msgHash=%s, time=%d\n", __func__, + id.ToString(), msgHash.ToString(), t.count()); + } else { + LogPrintf("CSigningManager::%s -- recovered signature. id=%s, msgHash=%s, time=%d\n", __func__, + id.ToString(), msgHash.ToString(), t.count()); + + CRecoveredSig rs; + rs.llmqType = quorum->params.type; + rs.quorumHash = quorum->quorumHash; + rs.id = id; + rs.msgHash = msgHash; + rs.sig = recoveredSig; + rs.UpdateHash(); + + auto signHash = MakeSignHash(rs); + bool valid = rs.sig.VerifyInsecure(quorum->quorumPublicKey, signHash); + if (!valid) { + // this should really not happen as we have verified all signature shares before + LogPrintf("CSigningManager::%s -- own recovered signature is invalid. id=%s, msgHash=%s\n", __func__, + id.ToString(), msgHash.ToString()); + return; + } + + ProcessRecoveredSig(-1, rs, quorum, connman); + } +} + +// signature must be verified already +void CSigningManager::ProcessRecoveredSig(NodeId nodeId, const CRecoveredSig& recoveredSig, const CQuorumCPtr& quorum, CConnman& connman) +{ + auto llmqType = (Consensus::LLMQType)recoveredSig.llmqType; + + { + LOCK(cs_main); + connman.RemoveAskFor(recoveredSig.GetHash()); + } + + { + LOCK(cs); + + auto signHash = MakeSignHash(recoveredSig); + + LogPrintf("CSigningManager::%s -- valid recSig. signHash=%s, id=%s, msgHash=%s, node=%d\n", __func__, + signHash.ToString(), recoveredSig.id.ToString(), recoveredSig.msgHash.ToString(), nodeId); + + if (recoveredSigsForIds.count(std::make_pair(llmqType, recoveredSig.id))) { + // this should really not happen, as each masternode is participating in only one vote, + // even if it's a member of multiple quorums. so a majority is only possible on one quorum and one msgHash per id + LogPrintf("CSigningManager::%s -- conflicting recoveredSig for id=%s, msgHash=%s\n", __func__, + recoveredSig.id.ToString(), recoveredSig.msgHash.ToString()); + return; + } + + auto key = std::make_pair(llmqType, recoveredSig.id); + recoveredSigs.emplace(recoveredSig.GetHash(), recoveredSig); + recoveredSigsForIds.emplace(key, recoveredSig.GetHash()); + recoveredSessions.emplace(signHash); + + RemoveSigSharesForSession(signHash); + + votedOnIds.erase(key); + } + + CInv inv(MSG_QUORUM_RECOVERED_SIG, recoveredSig.GetHash()); + g_connman->RelayInv(inv); +} + +bool CSigningManager::IsQuorumActive(Consensus::LLMQType llmqType, const uint256& quorumHash) +{ + AssertLockHeld(cs_main); + + auto& params = Params().GetConsensus().llmqs.at(llmqType); + + // sig shares and recovered sigs are only accepted from recent/active quorums + // we allow one more active quorum as specified in consensus, as otherwise there is a small window where things could + // fail while we are on the brink of a new quorum + auto quorums = quorumManager->ScanQuorums(llmqType, (int)params.signingActiveQuorumCount + 1); + for (auto& q : quorums) { + if (q->quorumHash == quorumHash) { + return true; + } + } + return false; +} + +void CSigningManager::AsyncSignIfMember(Consensus::LLMQType llmqType, const uint256& id, const uint256& msgHash) +{ + // delay around 30% of signing requests + // in most cases, 70% of members signing should be more than enough to create a recovered signature + // in some cases however, it might happen that we still fail, and then the delayed signatures come to the rescue + // TODO does this really improve global performance? + int64_t delay = 0; + double r = GetRand(1000000) / 1000000.0; + if (r >= 0.7) { + delay = 4 * 1000; + } + + PushWork(delay, [this, llmqType, id, msgHash]() { + SignIfMember(llmqType, id, msgHash); + }); +} + +void CSigningManager::SignIfMember(Consensus::LLMQType llmqType, const uint256& id, const uint256& msgHash) +{ + auto& params = Params().GetConsensus().llmqs.at(llmqType); + + if (!fMasternodeMode || activeMasternodeInfo.proTxHash.IsNull()) { + return; + } + + { + LOCK(cs); + + auto key = std::make_pair(llmqType, id); + auto it = votedOnIds.find(key); + if (it != votedOnIds.end()) { + if (it->second != msgHash) { + LogPrintf("CSigningManager::%s -- already voted for id=%s and msgHash=%s. Not voting on conflicting msgHash=%s\n", __func__, + id.ToString(), it->second.ToString(), msgHash.ToString()); + return; + } + } else { + votedOnIds.emplace(key, msgHash); + } + + if (recoveredSigsForIds.count(key)) { + // no need to sign it if we already have a recovered sig + return; + } + } + + + // This might end up giving different results on different members + // This might happen when we are on the brink of confirming a new quorum + // This gives a slight risk of not getting enough shares to recover a signature + // But at least it shouldn't be possible to get conflicting recovered signatures + // TODO fix this by re-signing when the next block arrives, but only when that block results in a change of the quorum list and no recovered signature has been created in the mean time + CQuorumCPtr quorum = quorumManager->SelectQuorum(llmqType, id, params.signingActiveQuorumCount); + if (!quorum) { + LogPrintf("CSigningManager::%s -- failed to select quorum. id=%s, msgHash=%s\n", __func__, id.ToString(), msgHash.ToString()); + return; + } + + if (!quorum->IsValidMember(activeMasternodeInfo.proTxHash)) { + //LogPrintf("CSigningManager::%s -- we're not a valid member of quorum %s\n", __func__, quorum->quorumHash.ToString()); + return; + } + + Sign(quorum, id, msgHash); +} + +void CSigningManager::Sign(const CQuorumCPtr& quorum, const uint256& id, const uint256& msgHash) { + cxxtimer::Timer t(true); + + { + LOCK(cs); + if (recoveredSigsForIds.count(std::make_pair(quorum->params.type, id))) { + // no need to sign it if we already have a recovered sig + return; + } + } + + if (!quorum->IsValidMember(activeMasternodeInfo.proTxHash)) { + return; + } + + CBLSSecretKey skShare = quorum->GetSkShare(); + if (!skShare.IsValid()) { + LogPrintf("CSigningManager::%s -- we don't have our skShare for quorum %s\n", __func__, quorum->quorumHash.ToString()); + return; + } + + int memberIdx = quorum->GetMemberIndex(activeMasternodeInfo.proTxHash); + if (memberIdx == -1) { + // this should really not happen (IsValidMember gave true) + return; + } + + CSigShare sigShare; + sigShare.llmqType = quorum->params.type; + sigShare.quorumHash = quorum->quorumHash; + sigShare.id = id; + sigShare.msgHash = msgHash; + sigShare.quorumMember = (uint16_t)memberIdx; + uint256 signHash = MakeSignHash(sigShare); + + sigShare.sigShare = skShare.Sign(signHash); + if (!sigShare.sigShare.IsValid()) { + LogPrintf("CSigningManager::%s -- failed to sign sigShare. id=%s, msgHash=%s, time=%s\n", __func__, + sigShare.id.ToString(), sigShare.msgHash.ToString(), t.count()); + return; + } + + sigShare.UpdateKey(); + + LogPrintf("CSigningManager::%s -- signed sigShare. id=%s, msgHash=%s, time=%s\n", __func__, + sigShare.id.ToString(), sigShare.msgHash.ToString(), t.count()); + ProcessSigShare(-1, sigShare, *g_connman, quorum); +} + +// cs must be held +void CSigningManager::CollectSigSharesToRequest(std::map>& sigSharesToRequest) +{ + int64_t now = GetTimeMillis(); + std::map> nodesBySigShares; + + const size_t maxRequestsForNode = 32; + + // avoid requesting from same nodes all the time + std::vector shuffledNodeIds; + shuffledNodeIds.reserve(nodeStates.size()); + for (auto& p : nodeStates) { + if (p.second.sessions.empty()) { + continue; + } + shuffledNodeIds.emplace_back(p.first); + } + std::random_shuffle(shuffledNodeIds.begin(), shuffledNodeIds.end(), rnd); + + for (auto& nodeId : shuffledNodeIds) { + auto& nodeState = nodeStates[nodeId]; + + for (auto it = nodeState.requestedSigShares.begin(); it != nodeState.requestedSigShares.end();) { + if (now - it->second >= SIG_SHARE_REQUEST_TIMEOUT) { + // timeout while waiting for this one, so retry it with another node + LogPrint("llmq", "CSigningManager::%s -- timeout while waiting for %s-%d, node=%d\n", __func__, + it->first.first.ToString(), it->first.second, nodeId); + it = nodeState.requestedSigShares.erase(it); + } else { + ++it; + } + } + + std::map* invMap = nullptr; + + for (auto& p2 : nodeState.sessions) { + auto& signHash = p2.first; + auto& session = p2.second; + + if (recoveredSessions.count(signHash)) { + continue; + } + + for (size_t i = 0; i < session.announced.inv.size(); i++) { + if (!session.announced.inv[i]) { + continue; + } + auto k = std::make_pair(signHash, (uint16_t) i); + if (sigShares.count(k)) { + // we already have it + session.announced.inv[i] = false; + continue; + } + if (nodeState.requestedSigShares.size() >= maxRequestsForNode) { + // too many pending requests for this node + break; + } + bool doRequest = false; + auto it = sigSharesRequested.find(k); + if (it != sigSharesRequested.end()) { + if (now - it->second.second >= SIG_SHARE_REQUEST_TIMEOUT && nodeId != it->second.first) { + // other node timed out, re-request from this node + LogPrint("llmq", "CSigningManager::%s -- other node timeout while waiting for %s-%d, re-request from=%d, node=%d\n", __func__, + it->first.first.ToString(), it->first.second, nodeId, it->second.first); + doRequest = true; + } + } else { + doRequest = true; + } + + if (doRequest) { + // track when we initiated the request so that we can detect timeouts + nodeState.requestedSigShares.emplace(k, now); + + // don't request it from other nodes until a timeout happens + auto& r = sigSharesRequested[k]; + r.first = nodeId; + r.second = now; + + if (!invMap) { + invMap = &sigSharesToRequest[nodeId]; + } + auto& inv = (*invMap)[signHash]; + if (inv.inv.empty()) { + inv.Init((Consensus::LLMQType)session.announced.llmqType, signHash); + } + inv.inv[k.second] = true; + + // dont't request it again from this node + session.announced.inv[i] = false; + } + } + } + } +} + +// cs must be held +void CSigningManager::CollectSigSharesToSend(std::map>& sigSharesToSend) +{ + for (auto& p : nodeStates) { + auto nodeId = p.first; + auto& nodeState = p.second; + + std::map* sigSharesToSend2 = nullptr; + + for (auto& p2 : nodeState.sessions) { + auto& signHash = p2.first; + auto& session = p2.second; + + if (recoveredSessions.count(signHash)) { + continue; + } + + CBatchedSigShares batchedSigShares; + + for (size_t i = 0; i < session.requested.inv.size(); i++) { + if (!session.requested.inv[i]) { + continue; + } + session.requested.inv[i] = false; + + auto k = std::make_pair(signHash, (uint16_t)i); + auto it = sigShares.find(k); + if (it == sigShares.end()) { + // he requested something we don'have + session.requested.inv[i] = false; + continue; + } + + auto& sigShare = it->second; + if (batchedSigShares.sigShares.empty()) { + batchedSigShares.llmqType = sigShare.llmqType; + batchedSigShares.quorumHash = sigShare.quorumHash; + batchedSigShares.id = sigShare.id; + batchedSigShares.msgHash = sigShare.msgHash; + } + batchedSigShares.sigShares.emplace_back((uint16_t)i, sigShare.sigShare); + } + + if (!batchedSigShares.sigShares.empty()) { + if (sigSharesToSend2 == nullptr) { + // only create the map if we actually add a batched sig + sigSharesToSend2 = &sigSharesToSend[nodeId]; + } + (*sigSharesToSend2).emplace(signHash, std::move(batchedSigShares)); + } + } + } +} + +// cs must be held +void CSigningManager::CollectSigSharesToAnnounce(std::map>& sigSharesToAnnounce) +{ + std::set> quorumNodesPrepared; + + for (auto& sigShareKey : this->sigSharesToAnnounce) { + auto& signHash = sigShareKey.first; + auto quorumMember = sigShareKey.second; + auto sigShareIt = sigShares.find(sigShareKey); + if (sigShareIt == sigShares.end()) { + continue; + } + auto& sigShare = sigShareIt->second; + + auto quorumKey = std::make_pair((Consensus::LLMQType)sigShare.llmqType, sigShare.quorumHash); + if (quorumNodesPrepared.emplace(quorumKey).second) { + // make sure we announce to at least the nodes which we know through the intra-quorum-communication system + auto nodeIds = g_connman->GetMasternodeQuorumNodes(quorumKey.first, quorumKey.second); + for (auto nodeId : nodeIds) { + auto& nodeState = nodeStates[nodeId]; + nodeState.interestedIn.emplace(quorumKey); + } + } + + for (auto& p : nodeStates) { + auto nodeId = p.first; + auto& nodeState = p.second; + + if (!nodeState.interestedIn.count(quorumKey)) { + // node is not interested in this sig share + // we only consider nodes to be interested if they sent us valid sig share before + // the sig share that we sign by ourself circumvents the inv system and is directly sent to all quorum members + // which are known by the deterministic intra-quorum-communication system. This is also the sig share that + // will tell the other nodes that we are interested in future sig shares + continue; + } + + auto& session = nodeState.GetOrCreateSession((Consensus::LLMQType)sigShare.llmqType, signHash); + + if (session.knows.inv[quorumMember]) { + // he already knows that one + continue; + } + + auto& inv = sigSharesToAnnounce[nodeId][signHash]; + if (inv.inv.empty()) { + inv.Init((Consensus::LLMQType)sigShare.llmqType, signHash); + } + inv.inv[quorumMember] = true; + session.knows.inv[quorumMember] = true; + } + } + + // don't announce these anymore + // node which did not send us a valid sig share before were left out now, but this is ok as it only results in slower + // propagation for the first signing session of a fresh quorum. The sig shares should still arrive on all nodes due to + // the deterministic intra-quorum-communication system + this->sigSharesToAnnounce.clear(); +} + +void CSigningManager::SendMessages() +{ + std::multimap nodesByAddress; + g_connman->ForEachNode([&nodesByAddress](CNode* pnode) { + nodesByAddress.emplace(pnode->addr, pnode->id); + }); + + std::map> sigSharesToRequest; + std::map> sigSharesToSend; + std::map> sigSharesToAnnounce; + + + { + LOCK(cs); + CollectSigSharesToRequest(sigSharesToRequest); + CollectSigSharesToSend(sigSharesToSend); + CollectSigSharesToAnnounce(sigSharesToAnnounce); + } + + g_connman->ForEachNode([&](CNode* pnode) { + CNetMsgMaker msgMaker(pnode->GetSendVersion()); + + auto it = sigSharesToRequest.find(pnode->id); + if (it != sigSharesToRequest.end()) { + for (auto& p : it->second) { + assert(p.second.CountSet() != 0); + LogPrint("llmq", "CSigningManager::SendMessages -- QGETSIGSHARES inv={%s}, node=%d\n", + p.second.ToString(), pnode->id); + g_connman->PushMessage(pnode, msgMaker.Make(NetMsgType::QGETSIGSHARES, p.second)); + } + } + + auto jt = sigSharesToSend.find(pnode->id); + if (jt != sigSharesToSend.end()) { + for (auto& p : jt->second) { + assert(!p.second.sigShares.empty()); + LogPrint("llmq", "CSigningManager::SendMessages -- QBSIGSHARES inv={%s}, node=%d\n", + p.second.ToInv().ToString(), pnode->id); + g_connman->PushMessage(pnode, msgMaker.Make(NetMsgType::QBSIGSHARES, p.second)); + } + } + + auto kt = sigSharesToAnnounce.find(pnode->id); + if (kt != sigSharesToAnnounce.end()) { + for (auto& p : kt->second) { + assert(p.second.CountSet() != 0); + LogPrint("llmq", "CSigningManager::SendMessages -- QSIGSHARESINV inv={%s}, node=%d\n", + p.second.ToString(), pnode->id); + g_connman->PushMessage(pnode, msgMaker.Make(NetMsgType::QSIGSHARESINV, p.second)); + } + } + + return true; + }); +} + +void CSigningManager::Cleanup() +{ + int64_t now = GetTimeMillis(); + if (now - lastCleanupTime < 5000) { + return; + } + + { + LOCK(cs); + std::set sigSharesToDelete; + for (auto& p : sigShares) { + sigSharesToDelete.emplace(p.first); + } + + std::set timeoutSessions; + for (auto& p : firstSeenForSessions) { + auto& signHash = p.first; + int64_t time = p.second; + + if (now - time >= SIGNING_SESSION_TIMEOUT) { + timeoutSessions.emplace(signHash); + } + } + + for (auto& signHash : timeoutSessions) { + size_t count = CountBySignHash(sigShares, signHash); + + if (count > 0) { + auto itPair = FindBySignHash(sigShares, signHash); + auto& firstSigShare = itPair.first->second; + LogPrintf("CSigningManager::%s -- signing session timed out. signHash=%s, id=%s, msgHash=%s, sigShareCount=%d\n", __func__, + signHash.ToString(), firstSigShare.id.ToString(), firstSigShare.msgHash.ToString(), count); + } else { + LogPrintf("CSigningManager::%s -- signing session timed out. signHash=%s, sigShareCount=%d\n", __func__, + signHash.ToString(), count); + } + RemoveSigSharesForSession(signHash); + } + } + + std::set nodeStatesToDelete; + for (auto& p : nodeStates) { + nodeStatesToDelete.emplace(p.first); + } + g_connman->ForEachNode([&](CNode* pnode) { + nodeStatesToDelete.erase(pnode->id); + }); + + LOCK(cs); + for (auto nodeId : nodeStatesToDelete) { + auto& nodeState = nodeStates[nodeId]; + // remove global requested state to force a request from another node + for (auto& p : nodeState.requestedSigShares) { + sigSharesRequested.erase(p.first); + } + nodeStates.erase(nodeId); + } + + lastCleanupTime = GetTimeMillis(); +} + +void CSigningManager::RemoveSigSharesForSession(const uint256& signHash) +{ + for (auto& p : nodeStates) { + auto& ns = p.second; + ns.Erase(signHash); + } + + EraseBySignHash(sigSharesRequested, signHash); + EraseBySignHash(sigSharesToAnnounce, signHash); + EraseBySignHash(sigShares, signHash); + firstSeenForSessions.erase(signHash); +} + +void CSigningManager::RemoveBannedNodeStates() +{ + LOCK2(cs_main, cs); + std::set toRemove; + for (auto it = nodeStates.begin(); it != nodeStates.end();) { + if (IsBanned(it->first)) { + it = nodeStates.erase(it); + } else { + ++it; + } + } +} + +void CSigningManager::WorkThreadMain() +{ + int64_t lastProcessTime = GetTimeMillis(); + while (!stopWorkThread && !ShutdownRequested()) { + if (GetTimeMillis() - lastProcessTime >= 100) { + RemoveBannedNodeStates(); + ProcessPendingIncomingSigs(*g_connman); + SendMessages(); + Cleanup(); + lastProcessTime = GetTimeMillis(); + } + + int64_t now = GetTimeMillis(); + + std::list jobs; + { + std::unique_lock l(workQueueMutex); + for (auto it = workQueue.begin(); it != workQueue.end();) { + auto& job = *it; + if (now >= job.at) { + jobs.emplace_back(std::move(job)); + it = workQueue.erase(it); + } else { + ++it; + } + } + } + + for (auto& job : jobs) { + if (stopWorkThread || ShutdownRequested()) { + break; + } + job.func(); + } + + MilliSleep(100); + } +} + +bool CSigningManager::HasRecoveredSig(Consensus::LLMQType llmqType, const uint256& id, const uint256& msgHash) +{ + CRecoveredSig recoveredSig; + { + LOCK(cs); + auto it = recoveredSigsForIds.find(std::make_pair(llmqType, id)); + if (it == recoveredSigsForIds.end()) { + return false; + } + auto it2 = recoveredSigs.find(it->second); + if (it2 == recoveredSigs.end()) { + // should not happen + return false; + } + recoveredSig = it2->second; + if (recoveredSig.msgHash != msgHash) { + // conflicting + return false; + } + } + + LOCK(cs_main); + if (!IsQuorumActive(llmqType, recoveredSig.quorumHash)) { + return false; + } + return true; +} + +bool CSigningManager::IsConflicting(Consensus::LLMQType llmqType, const uint256& id, const uint256& msgHash) +{ + CRecoveredSig recoveredSig; + { + LOCK(cs); + auto it = recoveredSigsForIds.find(std::make_pair(llmqType, id)); + if (it == recoveredSigsForIds.end()) { + return false; + } + auto it2 = recoveredSigs.find(it->second); + if (it2 == recoveredSigs.end()) { + // should not happen (signal conflict, even thought it's technically not a conflict. makes sure we don't accept something because there is a bug somewhere here) + return true; + } + recoveredSig = it2->second; + } + + LOCK(cs_main); + if (!IsQuorumActive(llmqType, recoveredSig.quorumHash)) { + return false; + } + return recoveredSig.msgHash != msgHash; +} + +void CSigShare::UpdateKey() +{ + key.first = MakeSignHash(*this); + key.second = quorumMember; +} + +void CSigSharesInv::Merge(const llmq::CSigSharesInv& inv2) +{ + assert(llmqType == inv2.llmqType); + assert(signHash == inv2.signHash); + for (size_t i = 0; i < inv.size(); i++) { + if (inv2.inv[i]) { + inv[i] = inv2.inv[i]; + } + } +} + +size_t CSigSharesInv::CountSet() const +{ + return std::count(inv.begin(), inv.end(), true); +} + +std::string CSigSharesInv::ToString() const +{ + std::string str = strprintf("signHash=%s, inv=(", signHash.ToString()); + bool first = true; + for (size_t i = 0; i < inv.size(); i++) { + if (!inv[i]) { + continue; + } + + if (!first) { + str += ","; + } + first = false; + str += strprintf("%d", i); + } + str += ")"; + return str; +} + +void CSigSharesInv::Init(Consensus::LLMQType _llmqType, const uint256& _signHash) +{ + llmqType = _llmqType; + signHash = _signHash; + + size_t llmqSize = (size_t)(Params().GetConsensus().llmqs.at(_llmqType).size); + inv.resize(llmqSize, false); +} + +bool CSigSharesInv::IsMarked(uint16_t quorumMember) const +{ + assert(quorumMember < inv.size()); + return inv[quorumMember]; +} + +void CSigSharesInv::Set(uint16_t quorumMember, bool v) +{ + assert(quorumMember < inv.size()); + inv[quorumMember] = v; +} + +CSigSharesNodeState::Session& CSigSharesNodeState::GetOrCreateSession(Consensus::LLMQType llmqType, const uint256& signHash) +{ + auto& s = sessions[signHash]; + if (s.announced.inv.empty()) { + s.announced.Init(llmqType, signHash); + s.requested.Init(llmqType, signHash); + s.knows.Init(llmqType, signHash); + } else { + assert(s.announced.llmqType == llmqType); + assert(s.requested.llmqType == llmqType); + assert(s.knows.llmqType == llmqType); + } + return s; +} + +void CSigSharesNodeState::MarkAnnounced(const uint256& signHash, const CSigSharesInv& inv) +{ + GetOrCreateSession((Consensus::LLMQType)inv.llmqType, signHash).announced.Merge(inv); +} + +void CSigSharesNodeState::MarkRequested(const uint256& signHash, const CSigSharesInv& inv) +{ + GetOrCreateSession((Consensus::LLMQType)inv.llmqType, signHash).requested.Merge(inv); +} + +void CSigSharesNodeState::MarkKnows(const uint256& signHash, const CSigSharesInv& inv) +{ + GetOrCreateSession((Consensus::LLMQType)inv.llmqType, signHash).knows.Merge(inv); +} + +void CSigSharesNodeState::MarkAnnounced(Consensus::LLMQType llmqType, const uint256& signHash, uint16_t quorumMember) +{ + GetOrCreateSession(llmqType, signHash).announced.Set(quorumMember, true); +} + +void CSigSharesNodeState::MarkRequested(Consensus::LLMQType llmqType, const uint256& signHash, uint16_t quorumMember) +{ + GetOrCreateSession(llmqType, signHash).requested.Set(quorumMember, true); +} + +void CSigSharesNodeState::MarkKnows(Consensus::LLMQType llmqType, const uint256& signHash, uint16_t quorumMember) +{ + GetOrCreateSession(llmqType, signHash).knows.Set(quorumMember, true); +} + +bool CSigSharesNodeState::Announced(const uint256& signHash, uint16_t quorumMember) const +{ + auto it = sessions.find(signHash); + if (it == sessions.end()) { + return false; + } + return it->second.announced.IsMarked(quorumMember); +} + +bool CSigSharesNodeState::Requested(const uint256& signHash, uint16_t quorumMember) const +{ + auto it = sessions.find(signHash); + if (it == sessions.end()) { + return false; + } + return it->second.requested.IsMarked(quorumMember); +} + +bool CSigSharesNodeState::Knows(const uint256& signHash, uint16_t quorumMember) const +{ + auto it = sessions.find(signHash); + if (it == sessions.end()) { + return false; + } + return it->second.knows.IsMarked(quorumMember); +} + +void CSigSharesNodeState::Erase(const uint256& signHash, uint16_t quorumMember) +{ + auto it = sessions.find(signHash); + if (it == sessions.end()) { + return; + } + auto& s = it->second; + s.announced.Set(quorumMember, false); + s.requested.Set(quorumMember, false); + s.knows.Set(quorumMember, false); + + bool anySet = false; + for (size_t i = 0; i < s.announced.inv.size(); i++) { + if (s.announced.inv[i] || s.requested.inv[i] || s.knows.inv[i]) { + anySet = true; + break; + } + } + if (!anySet) { + sessions.erase(it); + } +} + +void CSigSharesNodeState::Erase(const uint256& signHash) +{ + sessions.erase(signHash); + pendingIncomingRecSigs.erase(signHash); + EraseBySignHash(requestedSigShares, signHash); + EraseBySignHash(pendingIncomingSigShares, signHash); +} + +CSigSharesInv CBatchedSigShares::ToInv() const +{ + CSigSharesInv inv; + inv.Init((Consensus::LLMQType)llmqType, MakeSignHash(*this)); + for (size_t i = 0; i < sigShares.size(); i++) { + inv.inv[sigShares[i].first] = true; + } + return inv; +} + +} diff --git a/src/llmq/quorums_signing.h b/src/llmq/quorums_signing.h new file mode 100644 index 000000000000..1e0f878f97d4 --- /dev/null +++ b/src/llmq/quorums_signing.h @@ -0,0 +1,415 @@ +// Copyright (c) 2018 The Dash Core developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef DASH_QUORUMS_SIGNING_H +#define DASH_QUORUMS_SIGNING_H + +#include "llmq/quorums.h" + +#include "net.h" +#include "chainparams.h" + +#include +#include + +namespace cxxtimer { + class Timer; +} + +class CScheduler; + +namespace llmq +{ + +// +typedef std::pair SigShareKey; + +// this one does not get transmitted over the wire as it is batched inside CBatchedSigShares +class CSigShare +{ +public: + uint8_t llmqType; + uint256 quorumHash; + uint16_t quorumMember; + uint256 id; + uint256 msgHash; + CBLSSignature sigShare; + + // only in-memory + SigShareKey key; + +public: + ADD_SERIALIZE_METHODS + + template + inline void SerializationOp(Stream& s, Operation ser_action) + { + READWRITE(llmqType); + READWRITE(quorumHash); + READWRITE(quorumMember); + READWRITE(id); + READWRITE(msgHash); + READWRITE(sigShare); + if (ser_action.ForRead()) { + UpdateKey(); + } + }; + + void UpdateKey(); + const SigShareKey& GetKey() const + { + return key; + } + const uint256& GetSignHash() const + { + assert(!key.first.IsNull()); + return key.first; + } +}; + +class CSigSharesInv +{ +public: + uint8_t llmqType; + uint256 signHash; + std::vector inv; + +public: + template + void Serialize(Stream& s) const { + auto& consensus = Params().GetConsensus(); + auto it = consensus.llmqs.find((Consensus::LLMQType)llmqType); + assert(it != consensus.llmqs.end()); + assert(inv.size() == it->second.size); + size_t cnt = CountSet(); + size_t s1 = (inv.size() + 7) / 8; // as bitset + size_t s2 = 1; // as series of var int diffs with 0 as stop signal + for (size_t i = 0; i < inv.size(); i++) { + if (inv[i]) { + s2 += GetSizeOfVarInt(i + 1); + } + } + if (s1 < s2) { + s << llmqType; + s << signHash; + s << FIXEDBITSET(inv, inv.size()); + } else { + s << (uint8_t)(llmqType | 0x80); + s << signHash; + size_t last = 0; + for (size_t i = 0; i < inv.size(); i++) { + if (inv[i]) { + s << VARINT((i - last) + 1); // +1 because 0 is the stopper + last = i; + } + } + WriteVarInt(s, 0); // stopper + } + } + template + void Unserialize(Stream& s) { + s >> llmqType; + s >> signHash; + + bool isBitset = true; + if (llmqType & 0x80) { + llmqType &= 0x7F; + isBitset = false; + } + auto& consensus = Params().GetConsensus(); + auto it = consensus.llmqs.find((Consensus::LLMQType)llmqType); + if (it == consensus.llmqs.end()) { + throw std::ios_base::failure(strprintf("invalid llmqType %d", llmqType)); + } + + if (isBitset) { + s >> FIXEDBITSET(inv, (size_t)it->second.size); + } else { + inv.resize((size_t)it->second.size); + size_t last = 0; + while(true) { + uint64_t v; + s >> VARINT(v); + if (v == 0) { + break; + } + uint64_t idx = last + v - 1; + if (idx >= inv.size()) { + throw std::ios_base::failure(strprintf("out of bounds index %d, last=%d", idx, last)); + } + if (last != 0 && idx <= last) { + throw std::ios_base::failure(strprintf("unexpected index %d, last=%d", idx, last)); + } + if (inv[idx]) { + throw std::ios_base::failure(strprintf("duplicate index %d, last=%d", idx, last)); + } + inv[idx] = true; + last = idx; + } + } + } + + void Init(Consensus::LLMQType _llmqType, const uint256& _signHash); + bool IsMarked(uint16_t quorumMember) const; + void Set(uint16_t quorumMember, bool v); + void Merge(const CSigSharesInv& inv2); + + size_t CountSet() const; + std::string ToString() const; +}; + +// sent through the message QBSIGSHARES as a vector of multiple batches +class CBatchedSigShares +{ +public: + uint8_t llmqType; + uint256 quorumHash; + uint256 id; + uint256 msgHash; + std::vector> sigShares; + +public: + template + inline void SerializationOpBase(Stream& s, Operation ser_action) + { + READWRITE(llmqType); + READWRITE(quorumHash); + READWRITE(id); + READWRITE(msgHash); + } + + template + inline void Serialize(Stream& s) const + { + s << llmqType; + s << quorumHash; + s << id; + s << msgHash; + s << sigShares; + } + template + inline void Unserialize(Stream& s) + { + s >> llmqType; + s >> quorumHash; + s >> id; + s >> msgHash; + + // we do custom deserialization here with the malleability check skipped for signatures + // we can do this here because we never use the hash of a sig share for identification and are only interested + // in validity + uint64_t nSize = ReadCompactSize(s); + if (nSize > 400) { // we don't support larger quorums, so this is the limit + throw std::ios_base::failure(strprintf("too many elements (%d) in CBatchedSigShares", nSize)); + } + sigShares.resize(nSize); + for (size_t i = 0; i < nSize; i++) { + s >> sigShares[i].first; + sigShares[i].second.Unserialize(s, false); + } + }; + + CSigShare RebuildSigShare(size_t idx) const + { + assert(idx < sigShares.size()); + auto& s = sigShares[idx]; + CSigShare sigShare; + sigShare.llmqType = llmqType; + sigShare.quorumHash = quorumHash; + sigShare.quorumMember = s.first; + sigShare.id = id; + sigShare.msgHash = msgHash; + sigShare.sigShare = s.second; + sigShare.UpdateKey(); + return sigShare; + } + + CSigSharesInv ToInv() const; +}; + +class CRecoveredSig +{ +public: + uint8_t llmqType; + uint256 quorumHash; + uint256 id; + uint256 msgHash; + CBLSSignature sig; + + // only in-memory + uint256 hash; + +public: + ADD_SERIALIZE_METHODS + + template + inline void SerializationOp(Stream& s, Operation ser_action) + { + READWRITE(llmqType); + READWRITE(quorumHash); + READWRITE(id); + READWRITE(msgHash); + READWRITE(sig); + if (ser_action.ForRead()) { + UpdateHash(); + } + }; + + void UpdateHash() + { + hash = ::SerializeHash(*this); + } + + const uint256& GetHash() const + { + assert(!hash.IsNull()); + return hash; + } +}; + +class CSigSharesNodeState +{ +public: + struct Session { + CSigSharesInv announced; + CSigSharesInv requested; + CSigSharesInv knows; + }; + // TODO limit number of sessions per node + std::map sessions; + + std::map pendingIncomingSigShares; + std::map pendingIncomingRecSigs; // k = signHash + std::map requestedSigShares; + + // elements are added whenever we receive a valid sig share from this node + // this triggers us to send inventory items to him as he seems to be interested in these + std::set> interestedIn; + + Session& GetOrCreateSession(Consensus::LLMQType llmqType, const uint256& signHash); + + void MarkAnnounced(const uint256& signHash, const CSigSharesInv& inv); + void MarkRequested(const uint256& signHash, const CSigSharesInv& inv); + void MarkKnows(const uint256& signHash, const CSigSharesInv& inv); + + void MarkAnnounced(Consensus::LLMQType llmqType, const uint256& signHash, uint16_t quorumMember); + void MarkRequested(Consensus::LLMQType llmqType, const uint256& signHash, uint16_t quorumMember); + void MarkKnows(Consensus::LLMQType llmqType, const uint256& signHash, uint16_t quorumMember); + + bool Announced(const uint256& signHash, uint16_t quorumMember) const; + bool Requested(const uint256& signHash, uint16_t quorumMember) const; + bool Knows(const uint256& signHash, uint16_t quorumMember) const; + + void Erase(const uint256& signHash, uint16_t quorumMember); + void Erase(const uint256& signHash); +}; + +class CSigningManager +{ + static const int64_t SIGNING_SESSION_TIMEOUT = 60 * 1000; + static const int64_t SIG_SHARE_REQUEST_TIMEOUT = 5 * 1000; +private: + CCriticalSection cs; + CEvoDB& evoDb; + CBLSWorker& blsWorker; + + // TODO cleanup (also stuff from deleted quorums) + std::map sigShares; + std::map recoveredSigs; + + std::map firstSeenForSessions; + std::map, uint256> recoveredSigsForIds; + std::set recoveredSessions; + + std::map, uint256> votedOnIds; + + struct WorkQueueItem { + int64_t at; + std::function func; + }; + std::list workQueue; + std::mutex workQueueMutex; + std::thread workThread; + std::atomic stopWorkThread{false}; + + std::map nodeStates; + std::map> sigSharesRequested; + std::set sigSharesToAnnounce; + + // must be protected by cs + FastRandomContext rnd; + + int64_t lastCleanupTime{0}; + +public: + CSigningManager(CEvoDB& _evoDb, CBLSWorker& _blsWorker); + ~CSigningManager(); + +public: + bool AlreadyHave(const CInv& inv); + bool GetRecoveredSig(const uint256& hash, CRecoveredSig& ret); + + void ProcessMessage(CNode* pnode, const std::string& strCommand, CDataStream& vRecv, CConnman& connman); + +private: + void ProcessMessageSigSharesInv(CNode* pfrom, const CSigSharesInv& inv, CConnman& connman); + void ProcessMessageGetSigShares(CNode* pfrom, const CSigSharesInv& inv, CConnman& connman); + void ProcessMessageBatchedSigShares(CNode* pfrom, const CBatchedSigShares& batchedSigShares, CConnman& connman); + void ProcessMessageRecoveredSig(CNode* pfrom, const CRecoveredSig& recoveredSig, CConnman& connman); + bool VerifySigSharesInv(NodeId from, const CSigSharesInv& inv); + + bool PreVerifyBatchedSigShares(NodeId nodeId, const CBatchedSigShares& batchedSigShares, bool& retBan); + bool PreVerifyRecoveredSig(NodeId nodeId, const CRecoveredSig& recoveredSig, bool& retBan); + + void ProcessPendingIncomingSigs(CConnman& connman); + void ProcessPendingSigShares(CConnman& connman); + void ProcessPendingRecoveredSigs(CConnman& connman); + + bool VerifyPendingSigShares(const std::vector& sigShares, const std::map, CQuorumCPtr>& quorums); + void ProcessPendingSigSharesFromNode(NodeId nodeId, const std::vector& sigShares, const std::map, CQuorumCPtr>& quorums, CConnman& connman); + void ProcessPendingRecoveredSigsFromNode(NodeId nodeId, const std::list& recoveredSigs, const std::map, CQuorumCPtr>& quorums, CConnman& connman); + + void ProcessSigShare(NodeId nodeId, const CSigShare& sigShare, CConnman& connman, const CQuorumCPtr& quorum); + + void TryRecoverSig(const CQuorumCPtr& quorum, const uint256& id, const uint256& msgHash, CConnman& connman); + void ProcessRecoveredSig(NodeId nodeId, const CRecoveredSig& recoveredSig, const CQuorumCPtr& quorum, CConnman& connman); + + bool IsQuorumActive(Consensus::LLMQType llmqType, const uint256& quorumHash); + +public: + // public interface + void AsyncSignIfMember(Consensus::LLMQType llmqType, const uint256& id, const uint256& msgHash); + void SignIfMember(Consensus::LLMQType llmqType, const uint256& id, const uint256& msgHash); + void Sign(const CQuorumCPtr& quorum, const uint256& id, const uint256& msgHash); + bool HasRecoveredSig(Consensus::LLMQType llmqType, const uint256& id, const uint256& msgHash); + bool IsConflicting(Consensus::LLMQType llmqType, const uint256& id, const uint256& msgHash); + +private: + void Cleanup(); + void RemoveSigSharesForSession(const uint256& signHash); + void RemoveBannedNodeStates(); + + template + void PushWork(int64_t delay, Callable&& func) { + if (delay != 0) { + delay += GetTimeMillis(); + } + WorkQueueItem wi; + wi.at = delay; + wi.func = func; + std::unique_lock l(workQueueMutex); + workQueue.emplace_back(std::move(wi)); + } + void SendMessages(); + void CollectSigSharesToRequest(std::map>& sigSharesToRequest); + void CollectSigSharesToSend(std::map>& sigSharesToSend); + void CollectSigSharesToAnnounce(std::map>& sigSharesToAnnounce); + void WorkThreadMain(); +}; + +extern CSigningManager* quorumsSigningManager; + +} + +#endif //DASH_QUORUMS_SIGNING_H diff --git a/src/llmq/quorums_utils.cpp b/src/llmq/quorums_utils.cpp index 452909e07b58..b73d15733e36 100644 --- a/src/llmq/quorums_utils.cpp +++ b/src/llmq/quorums_utils.cpp @@ -29,5 +29,60 @@ uint256 CLLMQUtils::BuildCommitmentHash(uint8_t llmqType, const uint256& blockHa return hw.GetHash(); } +std::set CLLMQUtils::GetQuorumConnections(Consensus::LLMQType llmqType, const uint256& blockHash, const uint256& forMember) +{ + auto& params = Params().GetConsensus().llmqs.at(llmqType); + + auto mns = GetAllQuorumMembers(llmqType, blockHash); + std::set result; + for (size_t i = 0; i < mns.size(); i++) { + auto& dmn = mns[i]; + if (dmn->proTxHash == forMember) { + for (int n = 0; n < params.neighborConnections; n++) { + size_t idx = (i + 1 + n) % mns.size(); + auto& otherDmn = mns[idx]; + if (otherDmn == dmn) { + continue; + } + result.emplace(otherDmn->pdmnState->addr); + } + size_t startIdx = i + mns.size() / 2; + startIdx -= (params.diagonalConnections / 2) * params.neighborConnections; + startIdx %= mns.size(); + for (int n = 0; n < params.diagonalConnections; n++) { + size_t idx = startIdx + n * params.neighborConnections; + idx %= mns.size(); + auto& otherDmn = mns[idx]; + if (otherDmn == dmn) { + continue; + } + result.emplace(otherDmn->pdmnState->addr); + } + } + } + return result; +} + +std::set CLLMQUtils::CalcDeterministicWatchConnections(Consensus::LLMQType llmqType, const uint256& blockHash, size_t memberCount, size_t connectionCount) +{ + static uint256 qwatchConnectionSeed; + static std::atomic qwatchConnectionSeedGenerated{false}; + static CCriticalSection qwatchConnectionSeedCs; + if (!qwatchConnectionSeedGenerated) { + LOCK(qwatchConnectionSeedCs); + if (!qwatchConnectionSeedGenerated) { + qwatchConnectionSeed = GetRandHash(); + qwatchConnectionSeedGenerated = true; + } + } + + std::set result; + uint256 rnd = qwatchConnectionSeed; + for (size_t i = 0; i < connectionCount; i++) { + rnd = ::SerializeHash(std::make_pair(rnd, std::make_pair((uint8_t)llmqType, blockHash))); + result.emplace(rnd.GetUint64(0) % memberCount); + } + return result; +} } diff --git a/src/llmq/quorums_utils.h b/src/llmq/quorums_utils.h index 2aab41628dcf..7935aa9c48eb 100644 --- a/src/llmq/quorums_utils.h +++ b/src/llmq/quorums_utils.h @@ -21,6 +21,9 @@ class CLLMQUtils static std::vector GetAllQuorumMembers(Consensus::LLMQType llmqType, const uint256& blockHash); static uint256 BuildCommitmentHash(uint8_t llmqType, const uint256& blockHash, const std::vector& validMembers, const CBLSPublicKey& pubKey, const uint256& vvecHash); + + static std::set GetQuorumConnections(Consensus::LLMQType llmqType, const uint256& blockHash, const uint256& forMember); + static std::set CalcDeterministicWatchConnections(Consensus::LLMQType llmqType, const uint256& blockHash, size_t memberCount, size_t connectionCount); }; } diff --git a/src/masternodeman.cpp b/src/masternodeman.cpp index 4f8910fa150a..784c4113174a 100644 --- a/src/masternodeman.cpp +++ b/src/masternodeman.cpp @@ -894,8 +894,8 @@ void CMasternodeMan::ProcessMasternodeConnections(CConnman& connman) privateSendClient.GetMixingMasternodesInfo(vecMnInfo); #endif // ENABLE_WALLET - connman.ForEachNode(CConnman::AllNodes, [&vecMnInfo](CNode* pnode) { - if (pnode->fMasternode) { + connman.ForEachNode(CConnman::AllNodes, [&vecMnInfo, &connman](CNode* pnode) { + if (pnode->fMasternode && !connman.IsMasternodeQuorumNode(pnode->addr)) { #ifdef ENABLE_WALLET bool fFound = false; for (const auto& mnInfo : vecMnInfo) { diff --git a/src/net.cpp b/src/net.cpp index 651e672c0d60..5e40a26edbea 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -1953,23 +1953,45 @@ void CConnman::ThreadOpenMasternodeConnections() if (!interruptNet.sleep_for(std::chrono::milliseconds(1000))) return; + std::set connectedNodes; + ForEachNode([&connectedNodes](const CNode* pnode) { + connectedNodes.emplace(pnode->addr); + }); + CSemaphoreGrant grant(*semMasternodeOutbound); if (interruptNet) return; // NOTE: Process only one pending masternode at a time - LOCK(cs_vPendingMasternodes); - if (vPendingMasternodes.empty()) { - // nothing to do, keep waiting - continue; - } + CService addr; + { // don't hold lock while calling OpenMasternodeConnection as cs_main is locked deep inside + LOCK2(cs_vNodes, cs_vPendingMasternodes); - const CService addr = vPendingMasternodes.front(); - vPendingMasternodes.erase(vPendingMasternodes.begin()); - if (IsMasternodeOrDisconnectRequested(addr)) { - // nothing to do, try the next one - continue; + std::vector pending; + for (auto& group : masternodeQuorumNodes) { + for (auto& addr : group.second) { + if (!connectedNodes.count(addr) && !IsMasternodeOrDisconnectRequested(addr)) { + pending.emplace_back(addr); + } + } + } + + if (!vPendingMasternodes.empty()) { + auto addr = vPendingMasternodes.front(); + vPendingMasternodes.erase(vPendingMasternodes.begin()); + if (!connectedNodes.count(addr) && !IsMasternodeOrDisconnectRequested(addr)) { + pending.emplace_back(addr); + } + } + + if (pending.empty()) { + // nothing to do, keep waiting + continue; + } + + std::random_shuffle(pending.begin(), pending.end()); + addr = pending.front(); } OpenMasternodeConnection(CAddress(addr, NODE_NETWORK)); @@ -2573,6 +2595,93 @@ bool CConnman::AddPendingMasternode(const CService& service) return true; } +bool CConnman::AddMasternodeQuorumNodes(Consensus::LLMQType llmqType, const uint256& quorumHash, const std::set& addresses) +{ + LOCK(cs_vPendingMasternodes); + auto it = masternodeQuorumNodes.find(std::make_pair(llmqType, quorumHash)); + if (it != masternodeQuorumNodes.end()) { + return false; + } + masternodeQuorumNodes.emplace(std::make_pair(llmqType, quorumHash), addresses); + return true; +} + +bool CConnman::HasMasternodeQuorumNodes(Consensus::LLMQType llmqType, const uint256& quorumHash) +{ + LOCK(cs_vPendingMasternodes); + return masternodeQuorumNodes.count(std::make_pair(llmqType, quorumHash)); +} + +std::set> CConnman::GetMasternodeQuorums() +{ + LOCK(cs_vPendingMasternodes); + std::set> result; + for (auto& p : masternodeQuorumNodes) { + result.emplace(p.first); + } + return result; +} + +std::set CConnman::GetMasternodeQuorums(Consensus::LLMQType llmqType) +{ + LOCK(cs_vPendingMasternodes); + std::set result; + for (auto& p : masternodeQuorumNodes) { + if (p.first.first != llmqType) { + continue; + } + result.emplace(p.first.second); + } + return result; +} + +std::set CConnman::GetMasternodeQuorumAddresses(Consensus::LLMQType llmqType, const uint256& quorumHash) const +{ + LOCK(cs_vPendingMasternodes); + auto it = masternodeQuorumNodes.find(std::make_pair(llmqType, quorumHash)); + if (it == masternodeQuorumNodes.end()) { + return {}; + } + return it->second; +} + +std::set CConnman::GetMasternodeQuorumNodes(Consensus::LLMQType llmqType, const uint256& quorumHash) const +{ + LOCK2(cs_vNodes, cs_vPendingMasternodes); + auto it = masternodeQuorumNodes.find(std::make_pair(llmqType, quorumHash)); + if (it == masternodeQuorumNodes.end()) { + return {}; + } + std::set nodes; + for (auto pnode : vNodes) { + if (pnode->fDisconnect) { + continue; + } + if (!pnode->qwatch && !it->second.count(pnode->addr)) { + continue; + } + nodes.emplace(pnode->id); + } + return nodes; +} + +void CConnman::RemoveMasternodeQuorumNodes(Consensus::LLMQType llmqType, const uint256& quorumHash) +{ + LOCK(cs_vPendingMasternodes); + masternodeQuorumNodes.erase(std::make_pair(llmqType, quorumHash)); +} + +bool CConnman::IsMasternodeQuorumNode(const CService& addr) +{ + LOCK(cs_vPendingMasternodes); + for (const auto& p : masternodeQuorumNodes) { + if (p.second.count(addr)) { + return true; + } + } + return false; +} + size_t CConnman::GetNodeCount(NumConnections flags) { LOCK(cs_vNodes); @@ -2891,7 +3000,7 @@ CNode::~CNode() delete pfilter; } -void CNode::AskFor(const CInv& inv) +void CNode::AskFor(const CInv& inv, int64_t doubleRequestDelay) { if (mapAskFor.size() > MAPASKFOR_MAX_SZ || setAskFor.size() > SETASKFOR_MAX_SZ) { int64_t nNow = GetTime(); @@ -2929,7 +3038,7 @@ void CNode::AskFor(const CInv& inv) nLastTime = nNow; // Each retry is 2 minutes after the last - nRequestTime = std::max(nRequestTime + 2 * 60 * 1000000, nNow); + nRequestTime = std::max(nRequestTime + doubleRequestDelay, nNow); if (it != mapAlreadyAskedFor.end()) mapAlreadyAskedFor.update(it, nRequestTime); else @@ -3017,6 +3126,20 @@ bool CConnman::ForNode(NodeId id, std::function cond, return found != nullptr && cond(found) && func(found); } +void CConnman::ForEachQuorumMember(Consensus::LLMQType llmqType, const uint256& quorumHash, std::function func) const +{ + LOCK2(cs_vNodes, cs_vPendingMasternodes); + auto it = masternodeQuorumNodes.find(std::make_pair(llmqType, quorumHash)); + if (it == masternodeQuorumNodes.end()) { + return; + } + for (auto&& pnode : vNodes) { + if(it->second.count(pnode->addr)) { + func(pnode); + } + } +} + bool CConnman::IsMasternodeOrDisconnectRequested(const CService& addr) { return ForNode(addr, AllNodes, [](CNode* pnode){ return pnode->fMasternode || pnode->fDisconnect; diff --git a/src/net.h b/src/net.h index 3fdb14cc3cfc..c06236b526ac 100644 --- a/src/net.h +++ b/src/net.h @@ -20,6 +20,7 @@ #include "uint256.h" #include "util.h" #include "threadinterrupt.h" +#include "consensus/params.h" #include #include @@ -65,7 +66,7 @@ static const int MAX_OUTBOUND_CONNECTIONS = 8; /** Maximum number of addnode outgoing nodes */ static const int MAX_ADDNODE_CONNECTIONS = 8; /** Maximum number if outgoing masternodes */ -static const int MAX_OUTBOUND_MASTERNODE_CONNECTIONS = 30; +static const int MAX_OUTBOUND_MASTERNODE_CONNECTIONS = 100; /** -listen default */ static const bool DEFAULT_LISTEN = true; /** -upnp default */ @@ -79,7 +80,7 @@ static const size_t MAPASKFOR_MAX_SZ = MAX_INV_SZ; /** The maximum number of entries in setAskFor (larger due to getdata latency)*/ static const size_t SETASKFOR_MAX_SZ = 2 * MAX_INV_SZ; /** The maximum number of peer connections to maintain. */ -static const unsigned int DEFAULT_MAX_PEER_CONNECTIONS = 125; +static const unsigned int DEFAULT_MAX_PEER_CONNECTIONS = 200; /** The default for -maxuploadtarget. 0 = Unlimited */ static const uint64_t DEFAULT_MAX_UPLOAD_TARGET = 0; /** The default timeframe for -maxuploadtarget. 1 day. */ @@ -302,6 +303,8 @@ class CConnman ForEachNodeThen(FullyConnectedOnly, pre, post); } + void ForEachQuorumMember(Consensus::LLMQType llmqType, const uint256& quorumHash, std::function func) const; + std::vector CopyNodeVector(std::function cond); std::vector CopyNodeVector(); void ReleaseNodeVector(const std::vector& vecNodes); @@ -312,6 +315,7 @@ class CConnman void RelayInvFiltered(CInv &inv, const CTransaction &relatedTx, const int minProtoVersion = MIN_PEER_PROTO_VERSION); // This overload will not update node filters, so use it only for the cases when other messages will update related transaction data in filters void RelayInvFiltered(CInv &inv, const uint256 &relatedTxHash, const int minProtoVersion = MIN_PEER_PROTO_VERSION); + void RelayInvToQuorum(Consensus::LLMQType llmqType, const uint256& quorumHash, CInv &inv, const int minProtoVersion = MIN_PEER_PROTO_VERSION); void RemoveAskFor(const uint256& hash); // Addrman functions @@ -351,9 +355,19 @@ class CConnman bool AddNode(const std::string& node); bool RemoveAddedNode(const std::string& node); - bool AddPendingMasternode(const CService& addr); std::vector GetAddedNodeInfo(); + bool AddPendingMasternode(const CService& addr); + bool AddMasternodeQuorumNodes(Consensus::LLMQType llmqType, const uint256& quorumHash, const std::set& addresses); + bool HasMasternodeQuorumNodes(Consensus::LLMQType llmqType, const uint256& quorumHash); + std::set> GetMasternodeQuorums(); + std::set GetMasternodeQuorums(Consensus::LLMQType llmqType); + std::set GetMasternodeQuorumAddresses(Consensus::LLMQType llmqType, const uint256& quorumHash) const; + // also returns QWATCH nodes + std::set GetMasternodeQuorumNodes(Consensus::LLMQType llmqType, const uint256& quorumHash) const; + void RemoveMasternodeQuorumNodes(Consensus::LLMQType llmqType, const uint256& quorumHash); + bool IsMasternodeQuorumNode(const CService& addr); + size_t GetNodeCount(NumConnections num); void GetNodeStats(std::vector& vstats); bool DisconnectNode(const std::string& node); @@ -480,7 +494,8 @@ class CConnman std::vector vAddedNodes; CCriticalSection cs_vAddedNodes; std::vector vPendingMasternodes; - CCriticalSection cs_vPendingMasternodes; + std::map, std::set> masternodeQuorumNodes; // protected by cs_vPendingMasternodes + mutable CCriticalSection cs_vPendingMasternodes; std::vector vNodes; std::list vNodesDisconnected; mutable CCriticalSection cs_vNodes; @@ -799,6 +814,9 @@ class CNode // Whether a ping is requested. std::atomic fPingQueued; + // If true, we will send him all quorum related messages, even if he is not a member of our quorums + std::atomic_bool qwatch{false}; + CNode(NodeId id, ServiceFlags nLocalServicesIn, int nMyStartingHeightIn, SOCKET hSocketIn, const CAddress &addrIn, uint64_t nKeyedNetGroupIn, uint64_t nLocalHostNonceIn, const std::string &addrNameIn = "", bool fInboundIn = false); ~CNode(); @@ -931,7 +949,7 @@ class CNode vBlockHashesToAnnounce.push_back(hash); } - void AskFor(const CInv& inv); + void AskFor(const CInv& inv, int64_t doubleRequestDelay = 2 * 60 * 1000000); void RemoveAskFor(const uint256& hash); void CloseSocketDisconnect(); diff --git a/src/net_processing.cpp b/src/net_processing.cpp index c7e2ae53da10..3bbb4abf94ea 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -44,9 +44,13 @@ #include "evo/deterministicmns.h" #include "evo/simplifiedmns.h" -#include "llmq/quorums_commitment.h" -#include "llmq/quorums_dummydkg.h" +#include "llmq/quorums.h" #include "llmq/quorums_blockprocessor.h" +#include "llmq/quorums_commitment.h" +#include "llmq/quorums_debug.h" +#include "llmq/quorums_dkgsessionmgr.h" +#include "llmq/quorums_signing.h" +#include "llmq/quorums_instantx.h" #include @@ -733,7 +737,17 @@ void Misbehaving(NodeId pnode, int howmuch) LogPrintf("%s: %s peer=%d (%d -> %d)\n", __func__, state->name, pnode, state->nMisbehavior-howmuch, state->nMisbehavior); } - +// Requires cs_main. +bool IsBanned(NodeId pnode) +{ + CNodeState *state = State(pnode); + if (state == NULL) + return false; + if (state->fShouldBan) { + return true; + } + return false; +} @@ -968,8 +982,15 @@ bool static AlreadyHave(const CInv& inv) EXCLUSIVE_LOCKS_REQUIRED(cs_main) case MSG_QUORUM_FINAL_COMMITMENT: return llmq::quorumBlockProcessor->HasMinableCommitment(inv.hash); - case MSG_QUORUM_DUMMY_COMMITMENT: - return llmq::quorumDummyDKG->HasDummyCommitment(inv.hash); + case MSG_QUORUM_CONTRIB: + case MSG_QUORUM_COMPLAINT: + case MSG_QUORUM_JUSTIFICATION: + case MSG_QUORUM_PREMATURE_COMMITMENT: + return llmq::quorumDKGSessionManager->AlreadyHave(inv); + case MSG_QUORUM_RECOVERED_SIG: + return llmq::quorumsSigningManager->AlreadyHave(inv); + case MSG_QUORUM_DEBUG_STATUS: + return llmq::quorumDKGDebugManager->AlreadyHave(inv); } // Don't know what it is, just say we already got one @@ -1291,16 +1312,45 @@ void static ProcessGetData(CNode* pfrom, const Consensus::Params& consensusParam } } - if (!push && (inv.type == MSG_QUORUM_DUMMY_COMMITMENT)) { - if (!consensusParams.fLLMQAllowDummyCommitments) { - Misbehaving(pfrom->id, 100); - pfrom->fDisconnect = true; - return; + if (!push && (inv.type == MSG_QUORUM_CONTRIB)) { + llmq::CDKGContribution o; + if (llmq::quorumDKGSessionManager->GetContribution(inv.hash, o)) { + connman.PushMessage(pfrom, msgMaker.Make(NetMsgType::QCONTRIB, o)); + push = true; } - - llmq::CDummyCommitment o; - if (llmq::quorumDummyDKG->GetDummyCommitment(inv.hash, o)) { - connman.PushMessage(pfrom, msgMaker.Make(NetMsgType::QDCOMMITMENT, o)); + } + if (!push && (inv.type == MSG_QUORUM_COMPLAINT)) { + llmq::CDKGComplaint o; + if (llmq::quorumDKGSessionManager->GetComplaint(inv.hash, o)) { + connman.PushMessage(pfrom, msgMaker.Make(NetMsgType::QCOMPLAINT, o)); + push = true; + } + } + if (!push && (inv.type == MSG_QUORUM_JUSTIFICATION)) { + llmq::CDKGJustification o; + if (llmq::quorumDKGSessionManager->GetJustification(inv.hash, o)) { + connman.PushMessage(pfrom, msgMaker.Make(NetMsgType::QJUSTIFICATION, o)); + push = true; + } + } + if (!push && (inv.type == MSG_QUORUM_PREMATURE_COMMITMENT)) { + llmq::CDKGPrematureCommitment o; + if (llmq::quorumDKGSessionManager->GetPrematureCommitment(inv.hash, o)) { + connman.PushMessage(pfrom, msgMaker.Make(NetMsgType::QPCOMMITMENT, o)); + push = true; + } + } + if (!push && (inv.type == MSG_QUORUM_RECOVERED_SIG)) { + llmq::CRecoveredSig o; + if (llmq::quorumsSigningManager->GetRecoveredSig(inv.hash, o)) { + connman.PushMessage(pfrom, msgMaker.Make(NetMsgType::QSIGREC, o)); + push = true; + } + } + if (!push && (inv.type == MSG_QUORUM_DEBUG_STATUS)) { + llmq::CDKGDebugStatus o; + if (llmq::quorumDKGDebugManager->GetDebugStatus(inv.hash, o)) { + connman.PushMessage(pfrom, msgMaker.Make(NetMsgType::QDEBUGSTATUS, o)); push = true; } } @@ -1634,6 +1684,10 @@ bool static ProcessMessage(CNode* pfrom, const std::string& strCommand, CDataStr connman.PushMessage(pfrom, msgMaker.Make(NetMsgType::SENDCMPCT, fAnnounceUsingCMPCTBLOCK, nCMPCTBLOCKVersion)); } + if (GetBoolArg("-watchquorums", llmq::DEFAULT_WATCH_QUORUMS)) { + connman.PushMessage(pfrom, msgMaker.Make(NetMsgType::QWATCH)); + } + pfrom->fSuccessfullyConnected = true; } @@ -1780,8 +1834,16 @@ bool static ProcessMessage(CNode* pfrom, const std::string& strCommand, CDataStr pfrom->AddInventoryKnown(inv); if (fBlocksOnly) LogPrint("net", "transaction (%s) inv sent in violation of protocol peer=%d\n", inv.hash.ToString(), pfrom->id); - else if (!fAlreadyHave && !fImporting && !fReindex && !IsInitialBlockDownload()) - pfrom->AskFor(inv); + else if (!fAlreadyHave && !fImporting && !fReindex && !IsInitialBlockDownload()) { + int64_t doubleRequestDelay = 2 * 60 * 1000000; + // some messages need to be re-requested faster when the first announcing peer did not answer to GETDATA + switch (inv.type) { + case MSG_QUORUM_RECOVERED_SIG: + doubleRequestDelay = 5 * 1000000; + break; + } + pfrom->AskFor(inv, doubleRequestDelay); + } } // Track requests for our stuff @@ -2080,6 +2142,10 @@ bool static ProcessMessage(CNode* pfrom, const std::string& strCommand, CDataStr instantsend.Vote(tx.GetHash(), connman); } + if (strCommand == NetMsgType::TX || strCommand == NetMsgType::TXLOCKREQUEST) { + llmq::quorumInstantXManager.ProcessTx(pfrom, tx, connman, chainparams.GetConsensus()); + } + mempool.check(pcoinsTip); connman.RelayTransaction(tx); for (unsigned int i = 0; i < tx.vout.size(); i++) { @@ -2940,7 +3006,9 @@ bool static ProcessMessage(CNode* pfrom, const std::string& strCommand, CDataStr masternodeSync.ProcessMessage(pfrom, strCommand, vRecv); governance.ProcessMessage(pfrom, strCommand, vRecv, connman); llmq::quorumBlockProcessor->ProcessMessage(pfrom, strCommand, vRecv, connman); - llmq::quorumDummyDKG->ProcessMessage(pfrom, strCommand, vRecv, connman); + llmq::quorumDKGSessionManager->ProcessMessage(pfrom, strCommand, vRecv, connman); + llmq::quorumsSigningManager->ProcessMessage(pfrom, strCommand, vRecv, connman); + llmq::quorumDKGDebugManager->ProcessMessage(pfrom, strCommand, vRecv, connman); } else { diff --git a/src/net_processing.h b/src/net_processing.h index 3e7d596a597b..aff2d328da6b 100644 --- a/src/net_processing.h +++ b/src/net_processing.h @@ -53,6 +53,7 @@ struct CNodeStateStats { bool GetNodeStateStats(NodeId nodeid, CNodeStateStats &stats); /** Increase a node's misbehavior score. */ void Misbehaving(NodeId nodeid, int howmuch); +bool IsBanned(NodeId nodeid); /** Process protocol messages received from a given node */ bool ProcessMessages(CNode* pfrom, CConnman& connman, const std::atomic& interrupt); diff --git a/src/protocol.cpp b/src/protocol.cpp index 1cde07d8297d..378cee3c1ec1 100644 --- a/src/protocol.cpp +++ b/src/protocol.cpp @@ -72,7 +72,16 @@ const char *MNVERIFY="mnv"; const char *GETMNLISTDIFF="getmnlistd"; const char *MNLISTDIFF="mnlistdiff"; const char *QFCOMMITMENT="qfcommit"; -const char *QDCOMMITMENT="qdcommit"; +const char *QCONTRIB="qcontrib"; +const char *QCOMPLAINT="qcomplaint"; +const char *QJUSTIFICATION="qjustify"; +const char *QPCOMMITMENT="qpcommit"; +const char *QSIGSHARESINV="qsigsinv"; +const char *QGETSIGSHARES="qgetsigs"; +const char *QBSIGSHARES="qbsigs"; +const char *QSIGREC="qsigrec"; +const char *QWATCH="qwatch"; +const char *QDEBUGSTATUS="qdebugstatus"; }; static const char* ppszTypeName[] = @@ -101,7 +110,13 @@ static const char* ppszTypeName[] = NetMsgType::MNVERIFY, "compact block", // Should never occur NetMsgType::QFCOMMITMENT, - NetMsgType::QDCOMMITMENT, + "qdcommit", // was only shortly used on testnet + NetMsgType::QCONTRIB, + NetMsgType::QCOMPLAINT, + NetMsgType::QJUSTIFICATION, + NetMsgType::QPCOMMITMENT, + NetMsgType::QSIGREC, + NetMsgType::QDEBUGSTATUS, }; /** All known message types. Keep this in the same order as the list of @@ -162,7 +177,16 @@ const static std::string allNetMessageTypes[] = { NetMsgType::GETMNLISTDIFF, NetMsgType::MNLISTDIFF, NetMsgType::QFCOMMITMENT, - NetMsgType::QDCOMMITMENT, + NetMsgType::QCONTRIB, + NetMsgType::QCOMPLAINT, + NetMsgType::QJUSTIFICATION, + NetMsgType::QPCOMMITMENT, + NetMsgType::QSIGSHARESINV, + NetMsgType::QGETSIGSHARES, + NetMsgType::QBSIGSHARES, + NetMsgType::QSIGREC, + NetMsgType::QWATCH, + NetMsgType::QDEBUGSTATUS, }; const static std::vector allNetMessageTypesVec(allNetMessageTypes, allNetMessageTypes+ARRAYLEN(allNetMessageTypes)); diff --git a/src/protocol.h b/src/protocol.h index 4a5326f7b11f..2e67e5f52cfd 100644 --- a/src/protocol.h +++ b/src/protocol.h @@ -271,7 +271,16 @@ extern const char *MNVERIFY; extern const char *GETMNLISTDIFF; extern const char *MNLISTDIFF; extern const char *QFCOMMITMENT; -extern const char *QDCOMMITMENT; +extern const char *QCONTRIB; +extern const char *QCOMPLAINT; +extern const char *QJUSTIFICATION; +extern const char *QPCOMMITMENT; +extern const char *QSIGSHARESINV; +extern const char *QGETSIGSHARES; +extern const char *QBSIGSHARES; +extern const char *QSIGREC; +extern const char *QWATCH; +extern const char *QDEBUGSTATUS; }; /* Get a vector of all valid message types (see above) */ @@ -374,7 +383,13 @@ enum GetDataMsg { // MSG_CMPCT_BLOCK should not appear in any invs except as a part of getdata. MSG_CMPCT_BLOCK = 20, //!< Defined in BIP152 MSG_QUORUM_FINAL_COMMITMENT = 21, - MSG_QUORUM_DUMMY_COMMITMENT = 22, // only valid on testnet/devnet/regtest + /* MSG_QUORUM_DUMMY_COMMITMENT = 22, */ // was shortly used on testnet/devnet/regtest + MSG_QUORUM_CONTRIB = 23, + MSG_QUORUM_COMPLAINT = 24, + MSG_QUORUM_JUSTIFICATION = 25, + MSG_QUORUM_PREMATURE_COMMITMENT = 26, + MSG_QUORUM_RECOVERED_SIG = 27, + MSG_QUORUM_DEBUG_STATUS = 28, }; /** inv message data */ diff --git a/src/qt/test/rpcnestedtests.cpp b/src/qt/test/rpcnestedtests.cpp index e67b63ec8e9a..001a702b2046 100644 --- a/src/qt/test/rpcnestedtests.cpp +++ b/src/qt/test/rpcnestedtests.cpp @@ -55,7 +55,7 @@ void RPCNestedTests::rpcNestedTests() pcoinsdbview = new CCoinsViewDB(1 << 23, true); deterministicMNManager = new CDeterministicMNManager(*evoDb); - llmq::InitLLMQSystem(*evoDb); + llmq::InitLLMQSystem(*evoDb, nullptr); pcoinsTip = new CCoinsViewCache(pcoinsdbview); InitBlockIndex(chainparams); diff --git a/src/rpc/register.h b/src/rpc/register.h index de5cd4f2e5ea..7907ca5039a9 100644 --- a/src/rpc/register.h +++ b/src/rpc/register.h @@ -25,6 +25,8 @@ void RegisterMasternodeRPCCommands(CRPCTable &tableRPC); void RegisterGovernanceRPCCommands(CRPCTable &tableRPC); /** Register Evo RPC commands */ void RegisterEvoRPCCommands(CRPCTable &tableRPC); +/** Register Quorums RPC commands */ +void RegisterQuorumsRPCCommands(CRPCTable &tableRPC); static inline void RegisterAllCoreRPCCommands(CRPCTable &t) { @@ -36,6 +38,7 @@ static inline void RegisterAllCoreRPCCommands(CRPCTable &t) RegisterMasternodeRPCCommands(t); RegisterGovernanceRPCCommands(t); RegisterEvoRPCCommands(t); + RegisterQuorumsRPCCommands(t); } #endif diff --git a/src/rpc/rpcquorums.cpp b/src/rpc/rpcquorums.cpp new file mode 100644 index 000000000000..a22280dccb35 --- /dev/null +++ b/src/rpc/rpcquorums.cpp @@ -0,0 +1,188 @@ +// Copyright (c) 2017 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "server.h" +#include "base58.h" +#include "validation.h" + +#include "llmq/quorums.h" +#include "llmq/quorums_debug.h" +#include "llmq/quorums_dkgsession.h" + +void quorum_list_help() +{ + throw std::runtime_error( + "quorum list (count)\n" + "\nArguments:\n" + "1. count (number, optional) Number of quorums to list.\n" + ); +} + +UniValue quorum_list(const JSONRPCRequest& request) +{ + if (request.fHelp || (request.params.size() != 1 && request.params.size() != 2)) + quorum_list_help(); + + LOCK(cs_main); + + int count = 10; + if (request.params.size() > 1) { + count = ParseInt32V(request.params[1], "count"); + } + + UniValue ret(UniValue::VOBJ); + + for (auto& p : Params().GetConsensus().llmqs) { + UniValue v(UniValue::VARR); + + auto quorums = llmq::quorumManager->ScanQuorums(p.first, chainActive.Tip()->GetBlockHash(), count); + for (auto& q : quorums) { + v.push_back(q->quorumHash.ToString()); + } + + ret.push_back(Pair(p.second.name, v)); + } + + + return ret; +} + +void quorum_info_help() +{ + throw std::runtime_error( + "quorum info \"llmqType\" \"blockquorumHash\" (includeSkShare)\n" + "\nArguments:\n" + "1. \"llmqType\" (int, required) LLMQ type.\n" + "2. \"quorumHash\" (string, required) Block hash of quorum.\n" + "3. \"includeSkShare\" (boolean, optional) Include secret key share in output.\n" + ); +} + +UniValue quorum_info(const JSONRPCRequest& request) +{ + if (request.fHelp || (request.params.size() != 3 && request.params.size() != 4)) + quorum_info_help(); + + LOCK(cs_main); + + Consensus::LLMQType llmqType = (Consensus::LLMQType)ParseInt32V(request.params[1], "llmqType"); + if (!Params().GetConsensus().llmqs.count(llmqType)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "invalid LLMQ type"); + } + + uint256 blockHash = ParseHashV(request.params[2], "quorumHash"); + bool includeSkShare = false; + if (request.params.size() > 3) { + includeSkShare = ParseBoolV(request.params[3], "includeSkShare"); + } + + auto quorum = llmq::quorumManager->GetQuorum(llmqType, blockHash); + if (!quorum) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "quorum not found"); + } + + UniValue ret(UniValue::VOBJ); + + ret.push_back(Pair("height", quorum->height)); + ret.push_back(Pair("quorumHash", quorum->quorumHash.ToString())); + + UniValue membersArr(UniValue::VARR); + for (size_t i = 0; i < quorum->members.size(); i++) { + auto& dmn = quorum->members[i]; + UniValue mo(UniValue::VOBJ); + mo.push_back(Pair("proTxHash", dmn->proTxHash.ToString())); + mo.push_back(Pair("valid", quorum->validMembers[i])); + if (quorum->validMembers[i]) { + CBLSPublicKey pubKey = quorum->GetPubKeyShare(i); + if (pubKey.IsValid()) { + mo.push_back(Pair("pubKeyShare", pubKey.ToString())); + } + } + membersArr.push_back(mo); + } + + ret.push_back(Pair("members", membersArr)); + ret.push_back(Pair("quorumPublicKey", quorum->quorumPublicKey.ToString())); + CBLSSecretKey skShare = quorum->GetSkShare(); + if (includeSkShare && skShare.IsValid()) { + ret.push_back(Pair("secretKeyShare", skShare.ToString())); + } + + return ret; +} + +void quorum_dkgstatus_help() +{ + throw std::runtime_error( + "quorum dkgstatus (detailed)\n" + "\nArguments:\n" + "1. \"proTxHash\" (string, optional, default=0) ProTxHash of masternode to show status for.\n" + " If set to an empty string or 0, the local status is shown.\n" + "2. \"detailLevel\" (number, optional, default=0) Detail level of output.\n" + " 0=Only show counts. 1=Show member indexes. 2=Show member's ProTxHashes.\n" + ); +} + +UniValue quorum_dkgstatus(const JSONRPCRequest& request) +{ + if (request.fHelp || (request.params.size() != 1 && request.params.size() != 2 && request.params.size() != 3)) + quorum_info_help(); + + uint256 proTxHash; + if (request.params.size() > 1 && request.params[1].get_str() != "" && request.params[1].get_str() != "0") { + proTxHash = ParseHashV(request.params[1], "proTxHash"); + } + + int detailLevel = 0; + if (request.params.size() > 2) { + detailLevel = ParseInt32V(request.params[2], "detailLevel"); + if (detailLevel < 0 || detailLevel > 2) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "invalid detailLevel"); + } + } + + llmq::CDKGDebugStatus status; + if (proTxHash.IsNull()) { + llmq::quorumDKGDebugManager->GetLocalDebugStatus(status); + } else { + if (!llmq::quorumDKGDebugManager->GetDebugStatusForMasternode(proTxHash, status)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("no status for %s found", proTxHash.ToString())); + } + } + + return status.ToJson(detailLevel); +} + +UniValue quorum(const JSONRPCRequest& request) +{ + if (request.params.empty()) { + throw std::runtime_error( + "quorum \"command\" ...\n" + ); + } + + std::string command = request.params[0].get_str(); + + if (command == "list") { + return quorum_list(request); + } else if (command == "info") { + return quorum_info(request); + } else if (command == "dkgstatus") { + return quorum_dkgstatus(request); + } else { + throw std::runtime_error("invalid command: " + command); + } +} + +static const CRPCCommand commands[] = +{ // category name actor (function) okSafeMode + // --------------------- ------------------------ ----------------------- ---------- + { "evo", "quorum", &quorum, false, {} }, +}; + +void RegisterQuorumsRPCCommands(CRPCTable &tableRPC) +{ + for (unsigned int vcidx = 0; vcidx < ARRAYLEN(commands); vcidx++) + tableRPC.appendCommand(commands[vcidx].name, &commands[vcidx]); +} diff --git a/src/test/test_dash.cpp b/src/test/test_dash.cpp index 7bc3f7e9f734..67a87a2e6a95 100644 --- a/src/test/test_dash.cpp +++ b/src/test/test_dash.cpp @@ -74,7 +74,7 @@ TestingSetup::TestingSetup(const std::string& chainName) : BasicTestingSetup(cha pblocktree = new CBlockTreeDB(1 << 20, true); pcoinsdbview = new CCoinsViewDB(1 << 23, true); deterministicMNManager = new CDeterministicMNManager(*evoDb); - llmq::InitLLMQSystem(*evoDb); + llmq::InitLLMQSystem(*evoDb, nullptr); pcoinsTip = new CCoinsViewCache(pcoinsdbview); InitBlockIndex(chainparams); { diff --git a/src/validation.cpp b/src/validation.cpp index 69856143a162..7988ddadda8b 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -46,6 +46,7 @@ #include "evo/providertx.h" #include "evo/deterministicmns.h" #include "evo/cbtx.h" +#include "llmq/quorums_instantx.h" #include #include @@ -697,6 +698,10 @@ bool AcceptToMemoryPoolWorker(CTxMemPool& pool, CValidationState& state, const C REJECT_INVALID, "tx-txlock-conflict"); } + if (llmq::quorumInstantXManager.IsConflicting(tx, Params().GetConsensus())) { + LogPrint("mempool", "Conflicting tx %s\n", tx.GetHash().ToString()); + } + // Check for conflicts with in-memory transactions { LOCK(pool.cs); // protect pool.mapNextTx @@ -3262,6 +3267,9 @@ bool CheckBlock(const CBlock& block, CValidationState& state, const Consensus::P strprintf("transaction %s conflicts with transaction lock %s", tx->GetHash().ToString(), hashLocked.ToString())); } } + if (llmq::quorumInstantXManager.IsConflicting(*tx, consensusParams)) { + LogPrintf("CheckBlock(DASH): conflicting transaction %s in block\n", tx->GetHash().ToString()); + } } } else { LogPrintf("CheckBlock(DASH): spork is off, skipping transaction locking checks\n");