Implement and enforce ChainLocks
This commit is contained in:
parent
2bf6eb1c7c
commit
29532ba196
@ -144,6 +144,7 @@ BITCOIN_CORE_H = \
|
|||||||
llmq/quorums.h \
|
llmq/quorums.h \
|
||||||
llmq/quorums_blockprocessor.h \
|
llmq/quorums_blockprocessor.h \
|
||||||
llmq/quorums_commitment.h \
|
llmq/quorums_commitment.h \
|
||||||
|
llmq/quorums_chainlocks.h \
|
||||||
llmq/quorums_debug.h \
|
llmq/quorums_debug.h \
|
||||||
llmq/quorums_dkgsessionhandler.h \
|
llmq/quorums_dkgsessionhandler.h \
|
||||||
llmq/quorums_dkgsessionmgr.h \
|
llmq/quorums_dkgsessionmgr.h \
|
||||||
@ -259,6 +260,7 @@ libdash_server_a_SOURCES = \
|
|||||||
llmq/quorums.cpp \
|
llmq/quorums.cpp \
|
||||||
llmq/quorums_blockprocessor.cpp \
|
llmq/quorums_blockprocessor.cpp \
|
||||||
llmq/quorums_commitment.cpp \
|
llmq/quorums_commitment.cpp \
|
||||||
|
llmq/quorums_chainlocks.cpp \
|
||||||
llmq/quorums_debug.cpp \
|
llmq/quorums_debug.cpp \
|
||||||
llmq/quorums_dkgsessionhandler.cpp \
|
llmq/quorums_dkgsessionhandler.cpp \
|
||||||
llmq/quorums_dkgsessionmgr.cpp \
|
llmq/quorums_dkgsessionmgr.cpp \
|
||||||
|
@ -331,6 +331,7 @@ public:
|
|||||||
vSporkAddresses = {"Xgtyuk76vhuFW2iT7UAiHgNdWXCf3J34wh"};
|
vSporkAddresses = {"Xgtyuk76vhuFW2iT7UAiHgNdWXCf3J34wh"};
|
||||||
nMinSporkKeys = 1;
|
nMinSporkKeys = 1;
|
||||||
fBIP9CheckMasternodesUpgraded = true;
|
fBIP9CheckMasternodesUpgraded = true;
|
||||||
|
consensus.llmqChainLocks = Consensus::LLMQ_400_60;
|
||||||
|
|
||||||
checkpointData = (CCheckpointData) {
|
checkpointData = (CCheckpointData) {
|
||||||
boost::assign::map_list_of
|
boost::assign::map_list_of
|
||||||
@ -498,6 +499,7 @@ public:
|
|||||||
vSporkAddresses = {"yjPtiKh2uwk3bDutTEA2q9mCtXyiZRWn55"};
|
vSporkAddresses = {"yjPtiKh2uwk3bDutTEA2q9mCtXyiZRWn55"};
|
||||||
nMinSporkKeys = 1;
|
nMinSporkKeys = 1;
|
||||||
fBIP9CheckMasternodesUpgraded = true;
|
fBIP9CheckMasternodesUpgraded = true;
|
||||||
|
consensus.llmqChainLocks = Consensus::LLMQ_400_60;
|
||||||
|
|
||||||
checkpointData = (CCheckpointData) {
|
checkpointData = (CCheckpointData) {
|
||||||
boost::assign::map_list_of
|
boost::assign::map_list_of
|
||||||
@ -644,6 +646,7 @@ public:
|
|||||||
nMinSporkKeys = 1;
|
nMinSporkKeys = 1;
|
||||||
// devnets are started with no blocks and no MN, so we can't check for upgraded MN (as there are none)
|
// devnets are started with no blocks and no MN, so we can't check for upgraded MN (as there are none)
|
||||||
fBIP9CheckMasternodesUpgraded = false;
|
fBIP9CheckMasternodesUpgraded = false;
|
||||||
|
consensus.llmqChainLocks = Consensus::LLMQ_400_60;
|
||||||
|
|
||||||
checkpointData = (CCheckpointData) {
|
checkpointData = (CCheckpointData) {
|
||||||
boost::assign::map_list_of
|
boost::assign::map_list_of
|
||||||
@ -758,6 +761,7 @@ public:
|
|||||||
nMinSporkKeys = 1;
|
nMinSporkKeys = 1;
|
||||||
// regtest usually has no masternodes in most tests, so don't check for upgraged MNs
|
// regtest usually has no masternodes in most tests, so don't check for upgraged MNs
|
||||||
fBIP9CheckMasternodesUpgraded = false;
|
fBIP9CheckMasternodesUpgraded = false;
|
||||||
|
consensus.llmqChainLocks = Consensus::LLMQ_10_60;
|
||||||
|
|
||||||
checkpointData = (CCheckpointData){
|
checkpointData = (CCheckpointData){
|
||||||
boost::assign::map_list_of
|
boost::assign::map_list_of
|
||||||
|
@ -181,6 +181,7 @@ struct Params {
|
|||||||
int nHighSubsidyFactor{1};
|
int nHighSubsidyFactor{1};
|
||||||
|
|
||||||
std::map<LLMQType, LLMQParams> llmqs;
|
std::map<LLMQType, LLMQParams> llmqs;
|
||||||
|
LLMQType llmqChainLocks;
|
||||||
};
|
};
|
||||||
} // namespace Consensus
|
} // namespace Consensus
|
||||||
|
|
||||||
|
@ -9,8 +9,6 @@
|
|||||||
#include "masternode-payments.h"
|
#include "masternode-payments.h"
|
||||||
#include "masternode-sync.h"
|
#include "masternode-sync.h"
|
||||||
#include "privatesend.h"
|
#include "privatesend.h"
|
||||||
#include "llmq/quorums.h"
|
|
||||||
#include "llmq/quorums_dkgsessionmgr.h"
|
|
||||||
#ifdef ENABLE_WALLET
|
#ifdef ENABLE_WALLET
|
||||||
#include "privatesend-client.h"
|
#include "privatesend-client.h"
|
||||||
#endif // ENABLE_WALLET
|
#endif // ENABLE_WALLET
|
||||||
@ -18,6 +16,10 @@
|
|||||||
|
|
||||||
#include "evo/deterministicmns.h"
|
#include "evo/deterministicmns.h"
|
||||||
|
|
||||||
|
#include "llmq/quorums.h"
|
||||||
|
#include "llmq/quorums_chainlocks.h"
|
||||||
|
#include "llmq/quorums_dkgsessionmgr.h"
|
||||||
|
|
||||||
void CDSNotificationInterface::InitializeCurrentBlockTip()
|
void CDSNotificationInterface::InitializeCurrentBlockTip()
|
||||||
{
|
{
|
||||||
LOCK(cs_main);
|
LOCK(cs_main);
|
||||||
@ -26,6 +28,7 @@ void CDSNotificationInterface::InitializeCurrentBlockTip()
|
|||||||
|
|
||||||
void CDSNotificationInterface::AcceptedBlockHeader(const CBlockIndex *pindexNew)
|
void CDSNotificationInterface::AcceptedBlockHeader(const CBlockIndex *pindexNew)
|
||||||
{
|
{
|
||||||
|
llmq::chainLocksHandler->AcceptedBlockHeader(pindexNew);
|
||||||
masternodeSync.AcceptedBlockHeader(pindexNew);
|
masternodeSync.AcceptedBlockHeader(pindexNew);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,6 +58,8 @@ void CDSNotificationInterface::UpdatedBlockTip(const CBlockIndex *pindexNew, con
|
|||||||
if (fLiteMode)
|
if (fLiteMode)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
llmq::chainLocksHandler->UpdatedBlockTip(pindexNew, pindexFork);
|
||||||
|
|
||||||
CPrivateSend::UpdatedBlockTip(pindexNew);
|
CPrivateSend::UpdatedBlockTip(pindexNew);
|
||||||
#ifdef ENABLE_WALLET
|
#ifdef ENABLE_WALLET
|
||||||
privateSendClient.UpdatedBlockTip(pindexNew);
|
privateSendClient.UpdatedBlockTip(pindexNew);
|
||||||
|
392
src/llmq/quorums_chainlocks.cpp
Normal file
392
src/llmq/quorums_chainlocks.cpp
Normal file
@ -0,0 +1,392 @@
|
|||||||
|
// Copyright (c) 2019 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_chainlocks.h"
|
||||||
|
#include "quorums_signing.h"
|
||||||
|
#include "quorums_utils.h"
|
||||||
|
|
||||||
|
#include "chain.h"
|
||||||
|
#include "net_processing.h"
|
||||||
|
#include "scheduler.h"
|
||||||
|
#include "validation.h"
|
||||||
|
|
||||||
|
namespace llmq
|
||||||
|
{
|
||||||
|
|
||||||
|
static const std::string CLSIG_REQUESTID_PREFIX = "clsig";
|
||||||
|
|
||||||
|
CChainLocksHandler* chainLocksHandler;
|
||||||
|
|
||||||
|
std::string CChainLockSig::ToString() const
|
||||||
|
{
|
||||||
|
return strprintf("CChainLockSig(nHeight=%d, blockHash=%s)", nHeight, blockHash.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
CChainLocksHandler::CChainLocksHandler(CScheduler* _scheduler) :
|
||||||
|
scheduler(_scheduler)
|
||||||
|
{
|
||||||
|
quorumSigningManager->RegisterRecoveredSigsListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
CChainLocksHandler::~CChainLocksHandler()
|
||||||
|
{
|
||||||
|
quorumSigningManager->UnregisterRecoveredSigsListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CChainLocksHandler::AlreadyHave(const CInv& inv)
|
||||||
|
{
|
||||||
|
LOCK(cs);
|
||||||
|
return seenChainLocks.count(inv.hash) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CChainLocksHandler::GetChainLockByHash(const uint256& hash, llmq::CChainLockSig& ret)
|
||||||
|
{
|
||||||
|
LOCK(cs);
|
||||||
|
|
||||||
|
if (hash != bestChainLockHash) {
|
||||||
|
// we only propagate the best one and ditch all the old ones
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = bestChainLock;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CChainLocksHandler::ProcessMessage(CNode* pfrom, const std::string& strCommand, CDataStream& vRecv, CConnman& connman)
|
||||||
|
{
|
||||||
|
if (strCommand == NetMsgType::CLSIG) {
|
||||||
|
CChainLockSig clsig;
|
||||||
|
vRecv >> clsig;
|
||||||
|
|
||||||
|
auto hash = ::SerializeHash(clsig);
|
||||||
|
|
||||||
|
{
|
||||||
|
LOCK(cs_main);
|
||||||
|
connman.RemoveAskFor(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessNewChainLock(pfrom->id, clsig, hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CChainLocksHandler::ProcessNewChainLock(NodeId from, const llmq::CChainLockSig& clsig, const uint256& hash)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
LOCK(cs);
|
||||||
|
if (!seenChainLocks.emplace(hash, GetTimeMillis()).second) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestChainLock.nHeight != -1 && clsig.nHeight <= bestChainLock.nHeight) {
|
||||||
|
// no need to process/relay older CLSIGs
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint256 requestId = ::SerializeHash(std::make_pair(CLSIG_REQUESTID_PREFIX, clsig.nHeight));
|
||||||
|
uint256 msgHash = clsig.blockHash;
|
||||||
|
if (!quorumSigningManager->VerifyRecoveredSig(Params().GetConsensus().llmqChainLocks, clsig.nHeight, requestId, msgHash, clsig.sig)) {
|
||||||
|
LogPrintf("CChainLocksHandler::%s -- invalid CLSIG (%s), peer=%d\n", __func__, clsig.ToString(), from);
|
||||||
|
if (from != -1) {
|
||||||
|
LOCK(cs_main);
|
||||||
|
Misbehaving(from, 100);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
LOCK2(cs_main, cs);
|
||||||
|
|
||||||
|
if (InternalHasConflictingChainLock(clsig.nHeight, clsig.blockHash)) {
|
||||||
|
// This should not happen. If it happens, it means that a malicious entity controls a large part of the MN
|
||||||
|
// network. In this case, we don't allow him to reorg older chainlocks.
|
||||||
|
LogPrintf("CChainLocksHandler::%s -- new CLSIG (%s) tries to reorg previous CLSIG (%s), peer=%d\n",
|
||||||
|
__func__, clsig.ToString(), bestChainLock.ToString(), from);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bestChainLockHash = hash;
|
||||||
|
bestChainLock = clsig;
|
||||||
|
|
||||||
|
CInv inv(MSG_CLSIG, hash);
|
||||||
|
g_connman->RelayInv(inv);
|
||||||
|
|
||||||
|
auto blockIt = mapBlockIndex.find(clsig.blockHash);
|
||||||
|
if (blockIt == mapBlockIndex.end()) {
|
||||||
|
// we don't know the block/header for this CLSIG yet, so bail out for now
|
||||||
|
// when the block or the header later comes in, we will enforce the correct chain
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blockIt->second->nHeight != clsig.nHeight) {
|
||||||
|
// Should not happen, same as the conflict check from above.
|
||||||
|
LogPrintf("CChainLocksHandler::%s -- height of CLSIG (%s) does not match the specified block's height (%d)\n",
|
||||||
|
__func__, clsig.ToString(), blockIt->second->nHeight);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CBlockIndex* pindex = blockIt->second;
|
||||||
|
bestChainLockWithKnownBlock = bestChainLock;
|
||||||
|
bestChainLockBlockIndex = pindex;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnforceBestChainLock();
|
||||||
|
|
||||||
|
LogPrintf("CChainLocksHandler::%s -- processed new CLSIG (%s), peer=%d\n",
|
||||||
|
__func__, clsig.ToString(), from);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CChainLocksHandler::AcceptedBlockHeader(const CBlockIndex* pindexNew)
|
||||||
|
{
|
||||||
|
bool doEnforce = false;
|
||||||
|
{
|
||||||
|
LOCK2(cs_main, cs);
|
||||||
|
|
||||||
|
if (pindexNew->GetBlockHash() == bestChainLock.blockHash) {
|
||||||
|
LogPrintf("CChainLocksHandler::%s -- block header %s came in late, updating and enforcing\n", __func__, pindexNew->GetBlockHash().ToString());
|
||||||
|
|
||||||
|
if (bestChainLock.nHeight != pindexNew->nHeight) {
|
||||||
|
// Should not happen, same as the conflict check from ProcessNewChainLock.
|
||||||
|
LogPrintf("CChainLocksHandler::%s -- height of CLSIG (%s) does not match the specified block's height (%d)\n",
|
||||||
|
__func__, bestChainLock.ToString(), pindexNew->nHeight);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bestChainLockBlockIndex = pindexNew;
|
||||||
|
doEnforce = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (doEnforce) {
|
||||||
|
EnforceBestChainLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CChainLocksHandler::UpdatedBlockTip(const CBlockIndex* pindexNew, const CBlockIndex* pindexFork)
|
||||||
|
{
|
||||||
|
if (!fMasternodeMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!pindexNew->pprev) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DIP8 defines a process called "Signing attempts" which should run before the CLSIG is finalized
|
||||||
|
// To simplify the initial implementation, we skip this process and directly try to create a CLSIG
|
||||||
|
// This will fail when multiple blocks compete, but we accept this for the initial implementation.
|
||||||
|
// Later, we'll add the multiple attempts process.
|
||||||
|
|
||||||
|
uint256 requestId = ::SerializeHash(std::make_pair(CLSIG_REQUESTID_PREFIX, pindexNew->nHeight));
|
||||||
|
uint256 msgHash = pindexNew->GetBlockHash();
|
||||||
|
|
||||||
|
{
|
||||||
|
LOCK(cs);
|
||||||
|
|
||||||
|
if (InternalHasConflictingChainLock(pindexNew->nHeight, pindexNew->GetBlockHash())) {
|
||||||
|
if (!inInvalidate) {
|
||||||
|
// we accepted this block when there was no lock yet, but now a conflicting lock appeared. Invalidate it.
|
||||||
|
LogPrintf("CChainLocksHandler::%s -- conflicting lock after block was accepted, invalidating now\n",
|
||||||
|
__func__);
|
||||||
|
ScheduleInvalidateBlock(pindexNew);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestChainLock.nHeight >= pindexNew->nHeight) {
|
||||||
|
// already got the same CLSIG or a better one
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pindexNew->nHeight == lastSignedHeight) {
|
||||||
|
// already signed this one
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastSignedHeight = pindexNew->nHeight;
|
||||||
|
lastSignedRequestId = requestId;
|
||||||
|
lastSignedMsgHash = msgHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
quorumSigningManager->AsyncSignIfMember(Params().GetConsensus().llmqChainLocks, requestId, msgHash);
|
||||||
|
|
||||||
|
Cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
// WARNING: cs_main and cs should not be held!
|
||||||
|
void CChainLocksHandler::EnforceBestChainLock()
|
||||||
|
{
|
||||||
|
CChainLockSig clsig;
|
||||||
|
const CBlockIndex* pindex;
|
||||||
|
{
|
||||||
|
LOCK(cs);
|
||||||
|
clsig = bestChainLockWithKnownBlock;
|
||||||
|
pindex = bestChainLockBlockIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
LOCK(cs_main);
|
||||||
|
|
||||||
|
// Go backwards through the chain referenced by clsig until we find a block that is part of the main chain.
|
||||||
|
// For each of these blocks, check if there are children that are NOT part of the chain referenced by clsig
|
||||||
|
// and invalidate each of them.
|
||||||
|
inInvalidate = true; // avoid unnecessary ScheduleInvalidateBlock calls inside UpdatedBlockTip
|
||||||
|
while (pindex && !chainActive.Contains(pindex)) {
|
||||||
|
// Invalidate all blocks that have the same prevBlockHash but are not equal to blockHash
|
||||||
|
auto itp = mapPrevBlockIndex.equal_range(pindex->pprev->GetBlockHash());
|
||||||
|
for (auto jt = itp.first; jt != itp.second; ++jt) {
|
||||||
|
if (jt->second == pindex) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
LogPrintf("CChainLocksHandler::%s -- CLSIG (%s) invalidates block %s\n",
|
||||||
|
__func__, bestChainLockWithKnownBlock.ToString(), jt->second->GetBlockHash().ToString());
|
||||||
|
DoInvalidateBlock(jt->second, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
pindex = pindex->pprev;
|
||||||
|
}
|
||||||
|
inInvalidate = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CValidationState state;
|
||||||
|
if (!ActivateBestChain(state, Params())) {
|
||||||
|
LogPrintf("CChainLocksHandler::UpdatedBlockTip -- ActivateBestChain failed: %s\n", FormatStateMessage(state));
|
||||||
|
// This should not have happened and we are in a state were it's not safe to continue anymore
|
||||||
|
assert(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CChainLocksHandler::HandleNewRecoveredSig(const llmq::CRecoveredSig& recoveredSig)
|
||||||
|
{
|
||||||
|
CChainLockSig clsig;
|
||||||
|
{
|
||||||
|
LOCK(cs);
|
||||||
|
|
||||||
|
if (recoveredSig.id != lastSignedRequestId || recoveredSig.msgHash != lastSignedMsgHash) {
|
||||||
|
// this is not what we signed, so lets not create a CLSIG for it
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (bestChainLock.nHeight >= lastSignedHeight) {
|
||||||
|
// already got the same or a better CLSIG through the CLSIG message
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clsig.nHeight = lastSignedHeight;
|
||||||
|
clsig.blockHash = lastSignedMsgHash;
|
||||||
|
clsig.sig = recoveredSig.sig;
|
||||||
|
}
|
||||||
|
ProcessNewChainLock(-1, clsig, ::SerializeHash(clsig));
|
||||||
|
}
|
||||||
|
|
||||||
|
void CChainLocksHandler::ScheduleInvalidateBlock(const CBlockIndex* pindex)
|
||||||
|
{
|
||||||
|
// Calls to InvalidateBlock and ActivateBestChain might result in re-invocation of the UpdatedBlockTip and other
|
||||||
|
// signals, so we can't directly call it from signal handlers. We solve this by doing the call from the scheduler
|
||||||
|
|
||||||
|
scheduler->scheduleFromNow([this, pindex]() {
|
||||||
|
DoInvalidateBlock(pindex, true);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// WARNING, do not hold cs while calling this method as we'll otherwise run into a deadlock
|
||||||
|
void CChainLocksHandler::DoInvalidateBlock(const CBlockIndex* pindex, bool activateBestChain)
|
||||||
|
{
|
||||||
|
auto& params = Params();
|
||||||
|
|
||||||
|
{
|
||||||
|
LOCK(cs_main);
|
||||||
|
|
||||||
|
// get the non-const pointer
|
||||||
|
CBlockIndex* pindex2 = mapBlockIndex[pindex->GetBlockHash()];
|
||||||
|
|
||||||
|
CValidationState state;
|
||||||
|
if (!InvalidateBlock(state, params, pindex2)) {
|
||||||
|
LogPrintf("CChainLocksHandler::UpdatedBlockTip -- InvalidateBlock failed: %s\n", FormatStateMessage(state));
|
||||||
|
// This should not have happened and we are in a state were it's not safe to continue anymore
|
||||||
|
assert(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CValidationState state;
|
||||||
|
if (activateBestChain && !ActivateBestChain(state, params)) {
|
||||||
|
LogPrintf("CChainLocksHandler::UpdatedBlockTip -- ActivateBestChain failed: %s\n", FormatStateMessage(state));
|
||||||
|
// This should not have happened and we are in a state were it's not safe to continue anymore
|
||||||
|
assert(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CChainLocksHandler::HasChainLock(int nHeight, const uint256& blockHash)
|
||||||
|
{
|
||||||
|
LOCK(cs);
|
||||||
|
return InternalHasChainLock(nHeight, blockHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CChainLocksHandler::InternalHasChainLock(int nHeight, const uint256& blockHash)
|
||||||
|
{
|
||||||
|
AssertLockHeld(cs);
|
||||||
|
|
||||||
|
if (!bestChainLockBlockIndex) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nHeight > bestChainLockBlockIndex->nHeight) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nHeight == bestChainLockBlockIndex->nHeight) {
|
||||||
|
return blockHash == bestChainLockBlockIndex->GetBlockHash();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto pAncestor = bestChainLockBlockIndex->GetAncestor(nHeight);
|
||||||
|
return pAncestor && pAncestor->GetBlockHash() == blockHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CChainLocksHandler::HasConflictingChainLock(int nHeight, const uint256& blockHash)
|
||||||
|
{
|
||||||
|
LOCK(cs);
|
||||||
|
return InternalHasConflictingChainLock(nHeight, blockHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CChainLocksHandler::InternalHasConflictingChainLock(int nHeight, const uint256& blockHash)
|
||||||
|
{
|
||||||
|
AssertLockHeld(cs);
|
||||||
|
|
||||||
|
if (!bestChainLockBlockIndex) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nHeight > bestChainLockBlockIndex->nHeight) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nHeight == bestChainLockBlockIndex->nHeight) {
|
||||||
|
return blockHash != bestChainLockBlockIndex->GetBlockHash();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto pAncestor = bestChainLockBlockIndex->GetAncestor(nHeight);
|
||||||
|
assert(pAncestor);
|
||||||
|
return pAncestor->GetBlockHash() != blockHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CChainLocksHandler::Cleanup()
|
||||||
|
{
|
||||||
|
{
|
||||||
|
LOCK(cs);
|
||||||
|
if (GetTimeMillis() - lastCleanupTime < CLEANUP_INTERVAL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOCK2(cs_main, cs);
|
||||||
|
|
||||||
|
for (auto it = seenChainLocks.begin(); it != seenChainLocks.end(); ) {
|
||||||
|
if (GetTimeMillis() - it->second >= CLEANUP_SEEN_TIMEOUT) {
|
||||||
|
it = seenChainLocks.erase(it);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastCleanupTime = GetTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
99
src/llmq/quorums_chainlocks.h
Normal file
99
src/llmq/quorums_chainlocks.h
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
// Copyright (c) 2019 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_CHAINLOCKS_H
|
||||||
|
#define DASH_QUORUMS_CHAINLOCKS_H
|
||||||
|
|
||||||
|
#include "llmq/quorums.h"
|
||||||
|
#include "llmq/quorums_signing.h"
|
||||||
|
|
||||||
|
#include "net.h"
|
||||||
|
#include "chainparams.h"
|
||||||
|
|
||||||
|
class CBlockIndex;
|
||||||
|
class CScheduler;
|
||||||
|
|
||||||
|
namespace llmq
|
||||||
|
{
|
||||||
|
|
||||||
|
class CChainLockSig
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
int32_t nHeight{-1};
|
||||||
|
uint256 blockHash;
|
||||||
|
CBLSSignature sig;
|
||||||
|
|
||||||
|
public:
|
||||||
|
ADD_SERIALIZE_METHODS
|
||||||
|
|
||||||
|
template<typename Stream, typename Operation>
|
||||||
|
inline void SerializationOp(Stream& s, Operation ser_action)
|
||||||
|
{
|
||||||
|
READWRITE(nHeight);
|
||||||
|
READWRITE(blockHash);
|
||||||
|
READWRITE(sig);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ToString() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
class CChainLocksHandler : public CRecoveredSigsListener
|
||||||
|
{
|
||||||
|
static const int64_t CLEANUP_INTERVAL = 1000 * 30;
|
||||||
|
static const int64_t CLEANUP_SEEN_TIMEOUT = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
private:
|
||||||
|
CScheduler* scheduler;
|
||||||
|
CCriticalSection cs;
|
||||||
|
bool inInvalidate{false};
|
||||||
|
|
||||||
|
uint256 bestChainLockHash;
|
||||||
|
CChainLockSig bestChainLock;
|
||||||
|
|
||||||
|
CChainLockSig bestChainLockWithKnownBlock;
|
||||||
|
const CBlockIndex* bestChainLockBlockIndex{nullptr};
|
||||||
|
|
||||||
|
int32_t lastSignedHeight{-1};
|
||||||
|
uint256 lastSignedRequestId;
|
||||||
|
uint256 lastSignedMsgHash;
|
||||||
|
|
||||||
|
std::map<uint256, int64_t> seenChainLocks;
|
||||||
|
|
||||||
|
int64_t lastCleanupTime{0};
|
||||||
|
|
||||||
|
public:
|
||||||
|
CChainLocksHandler(CScheduler* _scheduler);
|
||||||
|
~CChainLocksHandler();
|
||||||
|
|
||||||
|
public:
|
||||||
|
bool AlreadyHave(const CInv& inv);
|
||||||
|
bool GetChainLockByHash(const uint256& hash, CChainLockSig& ret);
|
||||||
|
|
||||||
|
void ProcessMessage(CNode* pfrom, const std::string& strCommand, CDataStream& vRecv, CConnman& connman);
|
||||||
|
void ProcessNewChainLock(NodeId from, const CChainLockSig& clsig, const uint256& hash);
|
||||||
|
void AcceptedBlockHeader(const CBlockIndex* pindexNew);
|
||||||
|
void UpdatedBlockTip(const CBlockIndex* pindexNew, const CBlockIndex* pindexFork);
|
||||||
|
void EnforceBestChainLock();
|
||||||
|
virtual void HandleNewRecoveredSig(const CRecoveredSig& recoveredSig);
|
||||||
|
|
||||||
|
bool HasChainLock(int nHeight, const uint256& blockHash);
|
||||||
|
bool HasConflictingChainLock(int nHeight, const uint256& blockHash);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// these require locks to be held already
|
||||||
|
bool InternalHasChainLock(int nHeight, const uint256& blockHash);
|
||||||
|
bool InternalHasConflictingChainLock(int nHeight, const uint256& blockHash);
|
||||||
|
|
||||||
|
void ScheduleInvalidateBlock(const CBlockIndex* pindex);
|
||||||
|
void DoInvalidateBlock(const CBlockIndex* pindex, bool activateBestChain);
|
||||||
|
|
||||||
|
void Cleanup();
|
||||||
|
};
|
||||||
|
|
||||||
|
extern CChainLocksHandler* chainLocksHandler;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif //DASH_QUORUMS_CHAINLOCKS_H
|
@ -7,6 +7,7 @@
|
|||||||
#include "quorums.h"
|
#include "quorums.h"
|
||||||
#include "quorums_blockprocessor.h"
|
#include "quorums_blockprocessor.h"
|
||||||
#include "quorums_commitment.h"
|
#include "quorums_commitment.h"
|
||||||
|
#include "quorums_chainlocks.h"
|
||||||
#include "quorums_debug.h"
|
#include "quorums_debug.h"
|
||||||
#include "quorums_dkgsessionmgr.h"
|
#include "quorums_dkgsessionmgr.h"
|
||||||
#include "quorums_signing.h"
|
#include "quorums_signing.h"
|
||||||
@ -27,6 +28,7 @@ void InitLLMQSystem(CEvoDB& evoDb, CScheduler* scheduler, bool unitTests)
|
|||||||
quorumManager = new CQuorumManager(evoDb, blsWorker, *quorumDKGSessionManager);
|
quorumManager = new CQuorumManager(evoDb, blsWorker, *quorumDKGSessionManager);
|
||||||
quorumSigSharesManager = new CSigSharesManager();
|
quorumSigSharesManager = new CSigSharesManager();
|
||||||
quorumSigningManager = new CSigningManager(unitTests);
|
quorumSigningManager = new CSigningManager(unitTests);
|
||||||
|
chainLocksHandler = new CChainLocksHandler(scheduler);
|
||||||
|
|
||||||
quorumSigSharesManager->StartWorkerThread();
|
quorumSigSharesManager->StartWorkerThread();
|
||||||
}
|
}
|
||||||
@ -37,6 +39,8 @@ void DestroyLLMQSystem()
|
|||||||
quorumSigSharesManager->StopWorkerThread();
|
quorumSigSharesManager->StopWorkerThread();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
delete chainLocksHandler;
|
||||||
|
chainLocksHandler = nullptr;
|
||||||
delete quorumSigningManager;
|
delete quorumSigningManager;
|
||||||
quorumSigningManager = nullptr;
|
quorumSigningManager = nullptr;
|
||||||
delete quorumSigSharesManager;
|
delete quorumSigSharesManager;
|
||||||
|
@ -588,7 +588,7 @@ CQuorumCPtr CSigningManager::SelectQuorumForSigning(Consensus::LLMQType llmqType
|
|||||||
|
|
||||||
bool CSigningManager::VerifyRecoveredSig(Consensus::LLMQType llmqType, int signedAtHeight, const uint256& id, const uint256& msgHash, const CBLSSignature& sig)
|
bool CSigningManager::VerifyRecoveredSig(Consensus::LLMQType llmqType, int signedAtHeight, const uint256& id, const uint256& msgHash, const CBLSSignature& sig)
|
||||||
{
|
{
|
||||||
auto& llmqParams = Params().GetConsensus().llmqs.at(Params().GetConsensus().llmqTypeForChainLocks);
|
auto& llmqParams = Params().GetConsensus().llmqs.at(Params().GetConsensus().llmqChainLocks);
|
||||||
|
|
||||||
auto quorum = SelectQuorumForSigning(llmqParams.type, signedAtHeight, id);
|
auto quorum = SelectQuorumForSigning(llmqParams.type, signedAtHeight, id);
|
||||||
if (!quorum) {
|
if (!quorum) {
|
||||||
|
@ -46,6 +46,7 @@
|
|||||||
#include "evo/simplifiedmns.h"
|
#include "evo/simplifiedmns.h"
|
||||||
#include "llmq/quorums_blockprocessor.h"
|
#include "llmq/quorums_blockprocessor.h"
|
||||||
#include "llmq/quorums_commitment.h"
|
#include "llmq/quorums_commitment.h"
|
||||||
|
#include "llmq/quorums_chainlocks.h"
|
||||||
#include "llmq/quorums_debug.h"
|
#include "llmq/quorums_debug.h"
|
||||||
#include "llmq/quorums_dkgsessionmgr.h"
|
#include "llmq/quorums_dkgsessionmgr.h"
|
||||||
#include "llmq/quorums_init.h"
|
#include "llmq/quorums_init.h"
|
||||||
@ -973,6 +974,8 @@ bool static AlreadyHave(const CInv& inv) EXCLUSIVE_LOCKS_REQUIRED(cs_main)
|
|||||||
return llmq::quorumDKGDebugManager->AlreadyHave(inv);
|
return llmq::quorumDKGDebugManager->AlreadyHave(inv);
|
||||||
case MSG_QUORUM_RECOVERED_SIG:
|
case MSG_QUORUM_RECOVERED_SIG:
|
||||||
return llmq::quorumSigningManager->AlreadyHave(inv);
|
return llmq::quorumSigningManager->AlreadyHave(inv);
|
||||||
|
case MSG_CLSIG:
|
||||||
|
return llmq::chainLocksHandler->AlreadyHave(inv);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't know what it is, just say we already got one
|
// Don't know what it is, just say we already got one
|
||||||
@ -1285,6 +1288,14 @@ void static ProcessGetData(CNode* pfrom, const Consensus::Params& consensusParam
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!push && (inv.type == MSG_CLSIG)) {
|
||||||
|
llmq::CChainLockSig o;
|
||||||
|
if (llmq::chainLocksHandler->GetChainLockByHash(inv.hash, o)) {
|
||||||
|
connman.PushMessage(pfrom, msgMaker.Make(NetMsgType::CLSIG, o));
|
||||||
|
push = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!push)
|
if (!push)
|
||||||
vNotFound.push_back(inv);
|
vNotFound.push_back(inv);
|
||||||
}
|
}
|
||||||
@ -1787,6 +1798,9 @@ bool static ProcessMessage(CNode* pfrom, const std::string& strCommand, CDataStr
|
|||||||
case MSG_QUORUM_RECOVERED_SIG:
|
case MSG_QUORUM_RECOVERED_SIG:
|
||||||
doubleRequestDelay = 5 * 1000000;
|
doubleRequestDelay = 5 * 1000000;
|
||||||
break;
|
break;
|
||||||
|
case MSG_CLSIG:
|
||||||
|
doubleRequestDelay = 5 * 1000000;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
pfrom->AskFor(inv, doubleRequestDelay);
|
pfrom->AskFor(inv, doubleRequestDelay);
|
||||||
}
|
}
|
||||||
@ -2952,6 +2966,7 @@ bool static ProcessMessage(CNode* pfrom, const std::string& strCommand, CDataStr
|
|||||||
llmq::quorumDKGDebugManager->ProcessMessage(pfrom, strCommand, vRecv, connman);
|
llmq::quorumDKGDebugManager->ProcessMessage(pfrom, strCommand, vRecv, connman);
|
||||||
llmq::quorumSigSharesManager->ProcessMessage(pfrom, strCommand, vRecv, connman);
|
llmq::quorumSigSharesManager->ProcessMessage(pfrom, strCommand, vRecv, connman);
|
||||||
llmq::quorumSigningManager->ProcessMessage(pfrom, strCommand, vRecv, connman);
|
llmq::quorumSigningManager->ProcessMessage(pfrom, strCommand, vRecv, connman);
|
||||||
|
llmq::chainLocksHandler->ProcessMessage(pfrom, strCommand, vRecv, connman);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -69,6 +69,7 @@ const char *QSIGSHARESINV="qsigsinv";
|
|||||||
const char *QGETSIGSHARES="qgetsigs";
|
const char *QGETSIGSHARES="qgetsigs";
|
||||||
const char *QBSIGSHARES="qbsigs";
|
const char *QBSIGSHARES="qbsigs";
|
||||||
const char *QSIGREC="qsigrec";
|
const char *QSIGREC="qsigrec";
|
||||||
|
const char *CLSIG="clsig";
|
||||||
};
|
};
|
||||||
|
|
||||||
static const char* ppszTypeName[] =
|
static const char* ppszTypeName[] =
|
||||||
@ -104,6 +105,7 @@ static const char* ppszTypeName[] =
|
|||||||
NetMsgType::QPCOMMITMENT,
|
NetMsgType::QPCOMMITMENT,
|
||||||
NetMsgType::QDEBUGSTATUS,
|
NetMsgType::QDEBUGSTATUS,
|
||||||
NetMsgType::QSIGREC,
|
NetMsgType::QSIGREC,
|
||||||
|
NetMsgType::CLSIG,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** All known message types. Keep this in the same order as the list of
|
/** All known message types. Keep this in the same order as the list of
|
||||||
@ -167,6 +169,7 @@ const static std::string allNetMessageTypes[] = {
|
|||||||
NetMsgType::QGETSIGSHARES,
|
NetMsgType::QGETSIGSHARES,
|
||||||
NetMsgType::QBSIGSHARES,
|
NetMsgType::QBSIGSHARES,
|
||||||
NetMsgType::QSIGREC,
|
NetMsgType::QSIGREC,
|
||||||
|
NetMsgType::CLSIG,
|
||||||
};
|
};
|
||||||
const static std::vector<std::string> allNetMessageTypesVec(allNetMessageTypes, allNetMessageTypes+ARRAYLEN(allNetMessageTypes));
|
const static std::vector<std::string> allNetMessageTypesVec(allNetMessageTypes, allNetMessageTypes+ARRAYLEN(allNetMessageTypes));
|
||||||
|
|
||||||
|
@ -275,6 +275,7 @@ extern const char *QSIGSHARESINV;
|
|||||||
extern const char *QGETSIGSHARES;
|
extern const char *QGETSIGSHARES;
|
||||||
extern const char *QBSIGSHARES;
|
extern const char *QBSIGSHARES;
|
||||||
extern const char *QSIGREC;
|
extern const char *QSIGREC;
|
||||||
|
extern const char *CLSIG;
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Get a vector of all valid message types (see above) */
|
/* Get a vector of all valid message types (see above) */
|
||||||
@ -376,6 +377,7 @@ enum GetDataMsg {
|
|||||||
MSG_QUORUM_PREMATURE_COMMITMENT = 26,
|
MSG_QUORUM_PREMATURE_COMMITMENT = 26,
|
||||||
MSG_QUORUM_DEBUG_STATUS = 27,
|
MSG_QUORUM_DEBUG_STATUS = 27,
|
||||||
MSG_QUORUM_RECOVERED_SIG = 28,
|
MSG_QUORUM_RECOVERED_SIG = 28,
|
||||||
|
MSG_CLSIG = 29,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** inv message data */
|
/** inv message data */
|
||||||
|
@ -46,6 +46,8 @@
|
|||||||
#include "evo/deterministicmns.h"
|
#include "evo/deterministicmns.h"
|
||||||
#include "evo/cbtx.h"
|
#include "evo/cbtx.h"
|
||||||
|
|
||||||
|
#include "llmq/quorums_chainlocks.h"
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
@ -1897,6 +1899,10 @@ static bool ConnectBlock(const CBlock& block, CValidationState& state, CBlockInd
|
|||||||
if (!CheckBlock(block, state, chainparams.GetConsensus(), !fJustCheck, !fJustCheck))
|
if (!CheckBlock(block, state, chainparams.GetConsensus(), !fJustCheck, !fJustCheck))
|
||||||
return error("%s: Consensus::CheckBlock: %s", __func__, FormatStateMessage(state));
|
return error("%s: Consensus::CheckBlock: %s", __func__, FormatStateMessage(state));
|
||||||
|
|
||||||
|
if (pindex->pprev && llmq::chainLocksHandler->HasConflictingChainLock(pindex->nHeight, pindex->GetBlockHash())) {
|
||||||
|
return state.DoS(10, error("%s: conflicting with chainlock", __func__), REJECT_INVALID, "bad-chainlock");
|
||||||
|
}
|
||||||
|
|
||||||
// verify that the view's current state corresponds to the previous block
|
// verify that the view's current state corresponds to the previous block
|
||||||
uint256 hashPrevBlock = pindex->pprev == NULL ? uint256() : pindex->pprev->GetBlockHash();
|
uint256 hashPrevBlock = pindex->pprev == NULL ? uint256() : pindex->pprev->GetBlockHash();
|
||||||
assert(hashPrevBlock == view.GetBestBlock());
|
assert(hashPrevBlock == view.GetBestBlock());
|
||||||
@ -3442,6 +3448,10 @@ static bool AcceptBlockHeader(const CBlockHeader& block, CValidationState& state
|
|||||||
|
|
||||||
if (!ContextualCheckBlockHeader(block, state, chainparams.GetConsensus(), pindexPrev, GetAdjustedTime()))
|
if (!ContextualCheckBlockHeader(block, state, chainparams.GetConsensus(), pindexPrev, GetAdjustedTime()))
|
||||||
return error("%s: Consensus::ContextualCheckBlockHeader: %s, %s", __func__, hash.ToString(), FormatStateMessage(state));
|
return error("%s: Consensus::ContextualCheckBlockHeader: %s, %s", __func__, hash.ToString(), FormatStateMessage(state));
|
||||||
|
|
||||||
|
if (llmq::chainLocksHandler->HasConflictingChainLock(pindexPrev->nHeight + 1, hash)) {
|
||||||
|
return state.DoS(10, error("%s: conflicting with chainlock", __func__), REJECT_INVALID, "bad-chainlock");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (pindex == NULL)
|
if (pindex == NULL)
|
||||||
pindex = AddToBlockIndex(block);
|
pindex = AddToBlockIndex(block);
|
||||||
@ -3593,13 +3603,20 @@ bool TestBlockValidity(CValidationState& state, const CChainParams& chainparams,
|
|||||||
{
|
{
|
||||||
AssertLockHeld(cs_main);
|
AssertLockHeld(cs_main);
|
||||||
assert(pindexPrev && pindexPrev == chainActive.Tip());
|
assert(pindexPrev && pindexPrev == chainActive.Tip());
|
||||||
if (fCheckpointsEnabled && !CheckIndexAgainstCheckpoint(pindexPrev, state, chainparams, block.GetHash()))
|
|
||||||
|
uint256 hash = block.GetHash();
|
||||||
|
if (fCheckpointsEnabled && !CheckIndexAgainstCheckpoint(pindexPrev, state, chainparams, hash))
|
||||||
return error("%s: CheckIndexAgainstCheckpoint(): %s", __func__, state.GetRejectReason().c_str());
|
return error("%s: CheckIndexAgainstCheckpoint(): %s", __func__, state.GetRejectReason().c_str());
|
||||||
|
|
||||||
|
if (llmq::chainLocksHandler->HasConflictingChainLock(pindexPrev->nHeight + 1, hash)) {
|
||||||
|
return state.DoS(10, error("%s: conflicting with chainlock", __func__), REJECT_INVALID, "bad-chainlock");
|
||||||
|
}
|
||||||
|
|
||||||
CCoinsViewCache viewNew(pcoinsTip);
|
CCoinsViewCache viewNew(pcoinsTip);
|
||||||
CBlockIndex indexDummy(block);
|
CBlockIndex indexDummy(block);
|
||||||
indexDummy.pprev = pindexPrev;
|
indexDummy.pprev = pindexPrev;
|
||||||
indexDummy.nHeight = pindexPrev->nHeight + 1;
|
indexDummy.nHeight = pindexPrev->nHeight + 1;
|
||||||
|
indexDummy.phashBlock = &hash;
|
||||||
|
|
||||||
// begin tx and let it rollback
|
// begin tx and let it rollback
|
||||||
auto dbTx = evoDb->BeginTransaction();
|
auto dbTx = evoDb->BeginTransaction();
|
||||||
|
Loading…
Reference in New Issue
Block a user