mirror of
https://github.com/dashpay/dash.git
synced 2024-12-29 05:49:11 +01:00
1117 lines
34 KiB
C++
1117 lines
34 KiB
C++
// Copyright (c) 2019 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_chainlocks.h"
|
|
#include "quorums_instantsend.h"
|
|
#include "quorums_utils.h"
|
|
|
|
#include "bls/bls_batchverifier.h"
|
|
#include "chainparams.h"
|
|
#include "coins.h"
|
|
#include "txmempool.h"
|
|
#include "masternode-sync.h"
|
|
#include "net_processing.h"
|
|
#include "spork.h"
|
|
#include "validation.h"
|
|
|
|
#ifdef ENABLE_WALLET
|
|
#include "wallet/wallet.h"
|
|
#endif
|
|
|
|
// needed for nCompleteTXLocks
|
|
#include "instantx.h"
|
|
|
|
#include <boost/algorithm/string/replace.hpp>
|
|
#include <boost/thread.hpp>
|
|
|
|
namespace llmq
|
|
{
|
|
|
|
static const std::string INPUTLOCK_REQUESTID_PREFIX = "inlock";
|
|
static const std::string ISLOCK_REQUESTID_PREFIX = "islock";
|
|
|
|
CInstantSendManager* quorumInstantSendManager;
|
|
|
|
uint256 CInstantSendLock::GetRequestId() const
|
|
{
|
|
CHashWriter hw(SER_GETHASH, 0);
|
|
hw << ISLOCK_REQUESTID_PREFIX;
|
|
hw << inputs;
|
|
return hw.GetHash();
|
|
}
|
|
|
|
////////////////
|
|
|
|
void CInstantSendDb::WriteNewInstantSendLock(const uint256& hash, const CInstantSendLock& islock)
|
|
{
|
|
CDBBatch batch(db);
|
|
batch.Write(std::make_tuple(std::string("is_i"), hash), islock);
|
|
batch.Write(std::make_tuple(std::string("is_tx"), islock.txid), hash);
|
|
for (auto& in : islock.inputs) {
|
|
batch.Write(std::make_tuple(std::string("is_in"), in), hash);
|
|
}
|
|
db.WriteBatch(batch);
|
|
|
|
auto p = std::make_shared<CInstantSendLock>(islock);
|
|
islockCache.insert(hash, p);
|
|
txidCache.insert(islock.txid, hash);
|
|
for (auto& in : islock.inputs) {
|
|
outpointCache.insert(in, hash);
|
|
}
|
|
}
|
|
|
|
void CInstantSendDb::RemoveInstantSendLock(const uint256& hash, CInstantSendLockPtr islock)
|
|
{
|
|
CDBBatch batch(db);
|
|
RemoveInstantSendLock(batch, hash, islock);
|
|
db.WriteBatch(batch);
|
|
}
|
|
|
|
void CInstantSendDb::RemoveInstantSendLock(CDBBatch& batch, const uint256& hash, CInstantSendLockPtr islock)
|
|
{
|
|
if (!islock) {
|
|
islock = GetInstantSendLockByHash(hash);
|
|
if (!islock) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
batch.Erase(std::make_tuple(std::string("is_i"), hash));
|
|
batch.Erase(std::make_tuple(std::string("is_tx"), islock->txid));
|
|
for (auto& in : islock->inputs) {
|
|
batch.Erase(std::make_tuple(std::string("is_in"), in));
|
|
}
|
|
|
|
islockCache.erase(hash);
|
|
txidCache.erase(islock->txid);
|
|
for (auto& in : islock->inputs) {
|
|
outpointCache.erase(in);
|
|
}
|
|
}
|
|
|
|
static std::tuple<std::string, uint32_t, uint256> BuildInversedISLockMinedKey(int nHeight, const uint256& islockHash)
|
|
{
|
|
return std::make_tuple(std::string("is_m"), htobe32(std::numeric_limits<uint32_t>::max() - nHeight), islockHash);
|
|
}
|
|
|
|
void CInstantSendDb::WriteInstantSendLockMined(const uint256& hash, int nHeight)
|
|
{
|
|
db.Write(BuildInversedISLockMinedKey(nHeight, hash), true);
|
|
}
|
|
|
|
void CInstantSendDb::RemoveInstantSendLockMined(const uint256& hash, int nHeight)
|
|
{
|
|
db.Erase(BuildInversedISLockMinedKey(nHeight, hash));
|
|
}
|
|
|
|
std::unordered_map<uint256, CInstantSendLockPtr> CInstantSendDb::RemoveConfirmedInstantSendLocks(int nUntilHeight)
|
|
{
|
|
auto it = std::unique_ptr<CDBIterator>(db.NewIterator());
|
|
|
|
auto firstKey = BuildInversedISLockMinedKey(nUntilHeight, uint256());
|
|
|
|
it->Seek(firstKey);
|
|
|
|
CDBBatch deleteBatch(db);
|
|
std::unordered_map<uint256, CInstantSendLockPtr> ret;
|
|
while (it->Valid()) {
|
|
decltype(firstKey) curKey;
|
|
if (!it->GetKey(curKey) || std::get<0>(curKey) != "is_m") {
|
|
break;
|
|
}
|
|
uint32_t nHeight = std::numeric_limits<uint32_t>::max() - be32toh(std::get<1>(curKey));
|
|
if (nHeight > nUntilHeight) {
|
|
break;
|
|
}
|
|
|
|
auto& islockHash = std::get<2>(curKey);
|
|
auto islock = GetInstantSendLockByHash(islockHash);
|
|
if (islock) {
|
|
RemoveInstantSendLock(deleteBatch, islockHash, islock);
|
|
ret.emplace(islockHash, islock);
|
|
}
|
|
|
|
deleteBatch.Erase(curKey);
|
|
|
|
it->Next();
|
|
}
|
|
|
|
db.WriteBatch(deleteBatch);
|
|
|
|
return ret;
|
|
}
|
|
|
|
CInstantSendLockPtr CInstantSendDb::GetInstantSendLockByHash(const uint256& hash)
|
|
{
|
|
CInstantSendLockPtr ret;
|
|
if (islockCache.get(hash, ret)) {
|
|
return ret;
|
|
}
|
|
|
|
ret = std::make_shared<CInstantSendLock>();
|
|
bool exists = db.Read(std::make_tuple(std::string("is_i"), hash), *ret);
|
|
if (!exists) {
|
|
ret = nullptr;
|
|
}
|
|
islockCache.insert(hash, ret);
|
|
return ret;
|
|
}
|
|
|
|
uint256 CInstantSendDb::GetInstantSendLockHashByTxid(const uint256& txid)
|
|
{
|
|
uint256 islockHash;
|
|
|
|
bool found = txidCache.get(txid, islockHash);
|
|
if (found && islockHash.IsNull()) {
|
|
return uint256();
|
|
}
|
|
|
|
if (!found) {
|
|
found = db.Read(std::make_tuple(std::string("is_tx"), txid), islockHash);
|
|
txidCache.insert(txid, islockHash);
|
|
}
|
|
|
|
if (!found) {
|
|
return uint256();
|
|
}
|
|
return islockHash;
|
|
}
|
|
|
|
CInstantSendLockPtr CInstantSendDb::GetInstantSendLockByTxid(const uint256& txid)
|
|
{
|
|
uint256 islockHash = GetInstantSendLockHashByTxid(txid);
|
|
if (islockHash.IsNull()) {
|
|
return nullptr;
|
|
}
|
|
return GetInstantSendLockByHash(islockHash);
|
|
}
|
|
|
|
CInstantSendLockPtr CInstantSendDb::GetInstantSendLockByInput(const COutPoint& outpoint)
|
|
{
|
|
uint256 islockHash;
|
|
bool found = outpointCache.get(outpoint, islockHash);
|
|
if (found && islockHash.IsNull()) {
|
|
return nullptr;
|
|
}
|
|
|
|
if (!found) {
|
|
found = db.Read(std::make_tuple(std::string("is_in"), outpoint), islockHash);
|
|
outpointCache.insert(outpoint, islockHash);
|
|
}
|
|
|
|
if (!found) {
|
|
return nullptr;
|
|
}
|
|
return GetInstantSendLockByHash(islockHash);
|
|
}
|
|
|
|
////////////////
|
|
|
|
CInstantSendManager::CInstantSendManager(CDBWrapper& _llmqDb) :
|
|
db(_llmqDb)
|
|
{
|
|
workInterrupt.reset();
|
|
}
|
|
|
|
CInstantSendManager::~CInstantSendManager()
|
|
{
|
|
}
|
|
|
|
void CInstantSendManager::Start()
|
|
{
|
|
// can't start new thread if we have one running already
|
|
if (workThread.joinable()) {
|
|
assert(false);
|
|
}
|
|
|
|
workThread = std::thread(&TraceThread<std::function<void()> >, "instantsend", std::function<void()>(std::bind(&CInstantSendManager::WorkThreadMain, this)));
|
|
|
|
quorumSigningManager->RegisterRecoveredSigsListener(this);
|
|
}
|
|
|
|
void CInstantSendManager::Stop()
|
|
{
|
|
quorumSigningManager->UnregisterRecoveredSigsListener(this);
|
|
|
|
// make sure to call InterruptWorkerThread() first
|
|
if (!workInterrupt) {
|
|
assert(false);
|
|
}
|
|
|
|
if (workThread.joinable()) {
|
|
workThread.join();
|
|
}
|
|
}
|
|
|
|
void CInstantSendManager::InterruptWorkerThread()
|
|
{
|
|
workInterrupt();
|
|
}
|
|
|
|
bool CInstantSendManager::ProcessTx(const CTransaction& tx, const Consensus::Params& params)
|
|
{
|
|
if (!IsNewInstantSendEnabled()) {
|
|
return true;
|
|
}
|
|
|
|
auto llmqType = params.llmqForInstantSend;
|
|
if (llmqType == Consensus::LLMQ_NONE) {
|
|
return true;
|
|
}
|
|
if (!fMasternodeMode) {
|
|
return true;
|
|
}
|
|
|
|
// Ignore any InstantSend messages until blockchain is synced
|
|
if (!masternodeSync.IsBlockchainSynced()) {
|
|
return true;
|
|
}
|
|
|
|
// In case the islock was received before the TX, filtered announcement might have missed this islock because
|
|
// we were unable to check for filter matches deep inside the TX. Now we have the TX, so we should retry.
|
|
uint256 islockHash;
|
|
{
|
|
LOCK(cs);
|
|
islockHash = db.GetInstantSendLockHashByTxid(tx.GetHash());
|
|
}
|
|
if (!islockHash.IsNull()) {
|
|
CInv inv(MSG_ISLOCK, islockHash);
|
|
g_connman->RelayInvFiltered(inv, tx, LLMQS_PROTO_VERSION);
|
|
}
|
|
|
|
if (IsConflicted(tx)) {
|
|
return false;
|
|
}
|
|
|
|
if (!CheckCanLock(tx, true, params)) {
|
|
return false;
|
|
}
|
|
|
|
std::vector<uint256> ids;
|
|
ids.reserve(tx.vin.size());
|
|
|
|
size_t alreadyVotedCount = 0;
|
|
for (const auto& in : tx.vin) {
|
|
auto id = ::SerializeHash(std::make_pair(INPUTLOCK_REQUESTID_PREFIX, in.prevout));
|
|
ids.emplace_back(id);
|
|
|
|
uint256 otherTxHash;
|
|
if (quorumSigningManager->GetVoteForId(llmqType, id, otherTxHash)) {
|
|
if (otherTxHash != tx.GetHash()) {
|
|
LogPrintf("CInstantSendManager::%s -- txid=%s: input %s is conflicting with islock %s\n", __func__,
|
|
tx.GetHash().ToString(), in.prevout.ToStringShort(), otherTxHash.ToString());
|
|
return false;
|
|
}
|
|
alreadyVotedCount++;
|
|
}
|
|
|
|
// don't even try the actual signing if any input is conflicting
|
|
if (quorumSigningManager->IsConflicting(llmqType, id, tx.GetHash())) {
|
|
return false;
|
|
}
|
|
}
|
|
if (alreadyVotedCount == ids.size()) {
|
|
return true;
|
|
}
|
|
|
|
for (auto& id : ids) {
|
|
inputRequestIds.emplace(id);
|
|
quorumSigningManager->AsyncSignIfMember(llmqType, id, tx.GetHash());
|
|
}
|
|
|
|
// We might have received all input locks before we got the corresponding TX. In this case, we have to sign the
|
|
// islock now instead of waiting for the input locks.
|
|
TrySignInstantSendLock(tx);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CInstantSendManager::CheckCanLock(const CTransaction& tx, bool printDebug, const Consensus::Params& params)
|
|
{
|
|
if (sporkManager.IsSporkActive(SPORK_16_INSTANTSEND_AUTOLOCKS) && (mempool.UsedMemoryShare() > CInstantSend::AUTO_IX_MEMPOOL_THRESHOLD)) {
|
|
return false;
|
|
}
|
|
|
|
if (tx.vin.empty()) {
|
|
// can't lock TXs without inputs (e.g. quorum commitments)
|
|
return false;
|
|
}
|
|
|
|
CAmount nValueIn = 0;
|
|
for (const auto& in : tx.vin) {
|
|
CAmount v = 0;
|
|
if (!CheckCanLock(in.prevout, printDebug, tx.GetHash(), &v, params)) {
|
|
return false;
|
|
}
|
|
|
|
nValueIn += v;
|
|
}
|
|
|
|
// TODO decide if we should limit max input values. This was ok to do in the old system, but in the new system
|
|
// where we want to have all TXs locked at some point, this is counterproductive (especially when ChainLocks later
|
|
// depend on all TXs being locked first)
|
|
// CAmount maxValueIn = sporkManager.GetSporkValue(SPORK_5_INSTANTSEND_MAX_VALUE);
|
|
// if (nValueIn > maxValueIn * COIN) {
|
|
// if (printDebug) {
|
|
// LogPrint("instantsend", "CInstantSendManager::%s -- txid=%s: TX input value too high. nValueIn=%f, maxValueIn=%d", __func__,
|
|
// tx.GetHash().ToString(), nValueIn / (double)COIN, maxValueIn);
|
|
// }
|
|
// return false;
|
|
// }
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CInstantSendManager::CheckCanLock(const COutPoint& outpoint, bool printDebug, const uint256& txHash, CAmount* retValue, const Consensus::Params& params)
|
|
{
|
|
int nInstantSendConfirmationsRequired = params.nInstantSendConfirmationsRequired;
|
|
|
|
if (IsLocked(outpoint.hash)) {
|
|
// if prevout was ix locked, allow locking of descendants (no matter if prevout is in mempool or already mined)
|
|
return true;
|
|
}
|
|
|
|
auto mempoolTx = mempool.get(outpoint.hash);
|
|
if (mempoolTx) {
|
|
if (printDebug) {
|
|
LogPrint("instantsend", "CInstantSendManager::%s -- txid=%s: parent mempool TX %s is not locked\n", __func__,
|
|
txHash.ToString(), outpoint.hash.ToString());
|
|
}
|
|
return false;
|
|
}
|
|
|
|
CTransactionRef tx;
|
|
uint256 hashBlock;
|
|
// this relies on enabled txindex and won't work if we ever try to remove the requirement for txindex for masternodes
|
|
if (!GetTransaction(outpoint.hash, tx, params, hashBlock, false)) {
|
|
if (printDebug) {
|
|
LogPrint("instantsend", "CInstantSendManager::%s -- txid=%s: failed to find parent TX %s\n", __func__,
|
|
txHash.ToString(), outpoint.hash.ToString());
|
|
}
|
|
return false;
|
|
}
|
|
|
|
const CBlockIndex* pindexMined;
|
|
int nTxAge;
|
|
{
|
|
LOCK(cs_main);
|
|
pindexMined = mapBlockIndex.at(hashBlock);
|
|
nTxAge = chainActive.Height() - pindexMined->nHeight + 1;
|
|
}
|
|
|
|
if (nTxAge < nInstantSendConfirmationsRequired) {
|
|
if (!llmq::chainLocksHandler->HasChainLock(pindexMined->nHeight, pindexMined->GetBlockHash())) {
|
|
if (printDebug) {
|
|
LogPrint("instantsend", "CInstantSendManager::%s -- txid=%s: outpoint %s too new and not ChainLocked. nTxAge=%d, nInstantSendConfirmationsRequired=%d\n", __func__,
|
|
txHash.ToString(), outpoint.ToStringShort(), nTxAge, nInstantSendConfirmationsRequired);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (retValue) {
|
|
*retValue = tx->vout[outpoint.n].nValue;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void CInstantSendManager::HandleNewRecoveredSig(const CRecoveredSig& recoveredSig)
|
|
{
|
|
if (!IsNewInstantSendEnabled()) {
|
|
return;
|
|
}
|
|
|
|
auto llmqType = Params().GetConsensus().llmqForInstantSend;
|
|
if (llmqType == Consensus::LLMQ_NONE) {
|
|
return;
|
|
}
|
|
auto& params = Params().GetConsensus().llmqs.at(llmqType);
|
|
|
|
uint256 txid;
|
|
bool isInstantSendLock = false;
|
|
{
|
|
LOCK(cs);
|
|
if (inputRequestIds.count(recoveredSig.id)) {
|
|
txid = recoveredSig.msgHash;
|
|
}
|
|
if (creatingInstantSendLocks.count(recoveredSig.id)) {
|
|
isInstantSendLock = true;
|
|
}
|
|
}
|
|
if (!txid.IsNull()) {
|
|
HandleNewInputLockRecoveredSig(recoveredSig, txid);
|
|
} else if (isInstantSendLock) {
|
|
HandleNewInstantSendLockRecoveredSig(recoveredSig);
|
|
}
|
|
}
|
|
|
|
void CInstantSendManager::HandleNewInputLockRecoveredSig(const CRecoveredSig& recoveredSig, const uint256& txid)
|
|
{
|
|
auto llmqType = Params().GetConsensus().llmqForInstantSend;
|
|
|
|
CTransactionRef tx;
|
|
uint256 hashBlock;
|
|
if (!GetTransaction(txid, tx, Params().GetConsensus(), hashBlock, true)) {
|
|
return;
|
|
}
|
|
|
|
if (LogAcceptCategory("instantsend")) {
|
|
for (auto& in : tx->vin) {
|
|
auto id = ::SerializeHash(std::make_pair(INPUTLOCK_REQUESTID_PREFIX, in.prevout));
|
|
if (id == recoveredSig.id) {
|
|
LogPrint("instantsend", "CInstantSendManager::%s -- txid=%s: got recovered sig for input %s\n", __func__,
|
|
txid.ToString(), in.prevout.ToStringShort());
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
TrySignInstantSendLock(*tx);
|
|
}
|
|
|
|
void CInstantSendManager::TrySignInstantSendLock(const CTransaction& tx)
|
|
{
|
|
auto llmqType = Params().GetConsensus().llmqForInstantSend;
|
|
|
|
for (auto& in : tx.vin) {
|
|
auto id = ::SerializeHash(std::make_pair(INPUTLOCK_REQUESTID_PREFIX, in.prevout));
|
|
if (!quorumSigningManager->HasRecoveredSig(llmqType, id, tx.GetHash())) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
LogPrint("instantsend", "CInstantSendManager::%s -- txid=%s: got all recovered sigs, creating CInstantSendLock\n", __func__,
|
|
tx.GetHash().ToString());
|
|
|
|
CInstantSendLock islock;
|
|
islock.txid = tx.GetHash();
|
|
for (auto& in : tx.vin) {
|
|
islock.inputs.emplace_back(in.prevout);
|
|
}
|
|
|
|
auto id = islock.GetRequestId();
|
|
|
|
if (quorumSigningManager->HasRecoveredSigForId(llmqType, id)) {
|
|
return;
|
|
}
|
|
|
|
{
|
|
LOCK(cs);
|
|
auto e = creatingInstantSendLocks.emplace(id, std::move(islock));
|
|
if (!e.second) {
|
|
return;
|
|
}
|
|
txToCreatingInstantSendLocks.emplace(tx.GetHash(), &e.first->second);
|
|
}
|
|
|
|
quorumSigningManager->AsyncSignIfMember(llmqType, id, tx.GetHash());
|
|
}
|
|
|
|
void CInstantSendManager::HandleNewInstantSendLockRecoveredSig(const llmq::CRecoveredSig& recoveredSig)
|
|
{
|
|
CInstantSendLock islock;
|
|
|
|
{
|
|
LOCK(cs);
|
|
auto it = creatingInstantSendLocks.find(recoveredSig.id);
|
|
if (it == creatingInstantSendLocks.end()) {
|
|
return;
|
|
}
|
|
|
|
islock = std::move(it->second);
|
|
creatingInstantSendLocks.erase(it);
|
|
txToCreatingInstantSendLocks.erase(islock.txid);
|
|
}
|
|
|
|
if (islock.txid != recoveredSig.msgHash) {
|
|
LogPrintf("CInstantSendManager::%s -- txid=%s: islock conflicts with %s, dropping own version", __func__,
|
|
islock.txid.ToString(), recoveredSig.msgHash.ToString());
|
|
return;
|
|
}
|
|
|
|
islock.sig = recoveredSig.sig;
|
|
ProcessInstantSendLock(-1, ::SerializeHash(islock), islock);
|
|
}
|
|
|
|
void CInstantSendManager::ProcessMessage(CNode* pfrom, const std::string& strCommand, CDataStream& vRecv, CConnman& connman)
|
|
{
|
|
if (!IsNewInstantSendEnabled()) {
|
|
return;
|
|
}
|
|
|
|
if (strCommand == NetMsgType::ISLOCK) {
|
|
CInstantSendLock islock;
|
|
vRecv >> islock;
|
|
ProcessMessageInstantSendLock(pfrom, islock, connman);
|
|
}
|
|
}
|
|
|
|
void CInstantSendManager::ProcessMessageInstantSendLock(CNode* pfrom, const llmq::CInstantSendLock& islock, CConnman& connman)
|
|
{
|
|
bool ban = false;
|
|
if (!PreVerifyInstantSendLock(pfrom->id, islock, ban)) {
|
|
if (ban) {
|
|
LOCK(cs_main);
|
|
Misbehaving(pfrom->id, 100);
|
|
}
|
|
return;
|
|
}
|
|
|
|
auto hash = ::SerializeHash(islock);
|
|
|
|
LOCK(cs);
|
|
if (db.GetInstantSendLockByHash(hash) != nullptr) {
|
|
return;
|
|
}
|
|
if (pendingInstantSendLocks.count(hash)) {
|
|
return;
|
|
}
|
|
|
|
LogPrint("instantsend", "CInstantSendManager::%s -- txid=%s, islock=%s: received islock, peer=%d\n", __func__,
|
|
islock.txid.ToString(), hash.ToString(), pfrom->id);
|
|
|
|
pendingInstantSendLocks.emplace(hash, std::make_pair(pfrom->id, std::move(islock)));
|
|
}
|
|
|
|
bool CInstantSendManager::PreVerifyInstantSendLock(NodeId nodeId, const llmq::CInstantSendLock& islock, bool& retBan)
|
|
{
|
|
retBan = false;
|
|
|
|
if (islock.txid.IsNull() || islock.inputs.empty()) {
|
|
retBan = true;
|
|
return false;
|
|
}
|
|
|
|
std::set<COutPoint> dups;
|
|
for (auto& o : islock.inputs) {
|
|
if (!dups.emplace(o).second) {
|
|
retBan = true;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CInstantSendManager::ProcessPendingInstantSendLocks()
|
|
{
|
|
auto llmqType = Params().GetConsensus().llmqForInstantSend;
|
|
|
|
decltype(pendingInstantSendLocks) pend;
|
|
|
|
{
|
|
LOCK(cs);
|
|
pend = std::move(pendingInstantSendLocks);
|
|
}
|
|
|
|
if (pend.empty()) {
|
|
return false;
|
|
}
|
|
|
|
if (!IsNewInstantSendEnabled()) {
|
|
return false;
|
|
}
|
|
|
|
int tipHeight;
|
|
{
|
|
LOCK(cs_main);
|
|
tipHeight = chainActive.Height();
|
|
}
|
|
|
|
CBLSBatchVerifier<NodeId, uint256> batchVerifier(false, true, 8);
|
|
std::unordered_map<uint256, std::pair<CQuorumCPtr, CRecoveredSig>> recSigs;
|
|
|
|
for (const auto& p : pend) {
|
|
auto& hash = p.first;
|
|
auto nodeId = p.second.first;
|
|
auto& islock = p.second.second;
|
|
|
|
if (batchVerifier.badSources.count(nodeId)) {
|
|
continue;
|
|
}
|
|
|
|
if (!islock.sig.GetSig().IsValid()) {
|
|
batchVerifier.badSources.emplace(nodeId);
|
|
continue;
|
|
}
|
|
|
|
auto id = islock.GetRequestId();
|
|
|
|
// no need to verify an ISLOCK if we already have verified the recovered sig that belongs to it
|
|
if (quorumSigningManager->HasRecoveredSig(llmqType, id, islock.txid)) {
|
|
continue;
|
|
}
|
|
|
|
auto quorum = quorumSigningManager->SelectQuorumForSigning(llmqType, tipHeight, id);
|
|
if (!quorum) {
|
|
// should not happen, but if one fails to select, all others will also fail to select
|
|
return false;
|
|
}
|
|
uint256 signHash = CLLMQUtils::BuildSignHash(llmqType, quorum->qc.quorumHash, id, islock.txid);
|
|
batchVerifier.PushMessage(nodeId, hash, signHash, islock.sig.GetSig(), quorum->qc.quorumPublicKey);
|
|
|
|
// We can reconstruct the CRecoveredSig objects from the islock and pass it to the signing manager, which
|
|
// avoids unnecessary double-verification of the signature. We however only do this when verification here
|
|
// turns out to be good (which is checked further down)
|
|
if (!quorumSigningManager->HasRecoveredSigForId(llmqType, id)) {
|
|
CRecoveredSig recSig;
|
|
recSig.llmqType = llmqType;
|
|
recSig.quorumHash = quorum->qc.quorumHash;
|
|
recSig.id = id;
|
|
recSig.msgHash = islock.txid;
|
|
recSig.sig = islock.sig;
|
|
recSigs.emplace(std::piecewise_construct,
|
|
std::forward_as_tuple(hash),
|
|
std::forward_as_tuple(std::move(quorum), std::move(recSig)));
|
|
}
|
|
}
|
|
|
|
batchVerifier.Verify();
|
|
|
|
if (!batchVerifier.badSources.empty()) {
|
|
LOCK(cs_main);
|
|
for (auto& nodeId : batchVerifier.badSources) {
|
|
// Let's not be too harsh, as the peer might simply be unlucky and might have sent us an old lock which
|
|
// does not validate anymore due to changed quorums
|
|
Misbehaving(nodeId, 20);
|
|
}
|
|
}
|
|
for (const auto& p : pend) {
|
|
auto& hash = p.first;
|
|
auto nodeId = p.second.first;
|
|
auto& islock = p.second.second;
|
|
|
|
if (batchVerifier.badMessages.count(hash)) {
|
|
LogPrintf("CInstantSendManager::%s -- txid=%s, islock=%s: invalid sig in islock, peer=%d\n", __func__,
|
|
islock.txid.ToString(), hash.ToString(), nodeId);
|
|
continue;
|
|
}
|
|
|
|
ProcessInstantSendLock(nodeId, hash, islock);
|
|
|
|
// See comment further on top. We pass a reconstructed recovered sig to the signing manager to avoid
|
|
// double-verification of the sig.
|
|
auto it = recSigs.find(hash);
|
|
if (it != recSigs.end()) {
|
|
auto& quorum = it->second.first;
|
|
auto& recSig = it->second.second;
|
|
if (!quorumSigningManager->HasRecoveredSigForId(llmqType, recSig.id)) {
|
|
recSig.UpdateHash();
|
|
LogPrint("instantsend", "CInstantSendManager::%s -- txid=%s, islock=%s: passing reconstructed recSig to signing mgr, peer=%d\n", __func__,
|
|
islock.txid.ToString(), hash.ToString(), nodeId);
|
|
quorumSigningManager->PushReconstructedRecoveredSig(recSig, quorum);
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void CInstantSendManager::ProcessInstantSendLock(NodeId from, const uint256& hash, const CInstantSendLock& islock)
|
|
{
|
|
{
|
|
LOCK(cs_main);
|
|
g_connman->RemoveAskFor(hash);
|
|
}
|
|
|
|
CTransactionRef tx;
|
|
uint256 hashBlock;
|
|
const CBlockIndex* pindexMined = nullptr;
|
|
// we ignore failure here as we must be able to propagate the lock even if we don't have the TX locally
|
|
if (GetTransaction(islock.txid, tx, Params().GetConsensus(), hashBlock)) {
|
|
if (!hashBlock.IsNull()) {
|
|
{
|
|
LOCK(cs_main);
|
|
pindexMined = mapBlockIndex.at(hashBlock);
|
|
}
|
|
|
|
// Let's see if the TX that was locked by this islock is already mined in a ChainLocked block. If yes,
|
|
// we can simply ignore the islock, as the ChainLock implies locking of all TXs in that chain
|
|
if (llmq::chainLocksHandler->HasChainLock(pindexMined->nHeight, pindexMined->GetBlockHash())) {
|
|
LogPrint("instantsend", "CInstantSendManager::%s -- txlock=%s, islock=%s: dropping islock as it already got a ChainLock in block %s, peer=%d\n", __func__,
|
|
islock.txid.ToString(), hash.ToString(), hashBlock.ToString(), from);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
LOCK(cs);
|
|
|
|
LogPrint("instantsend", "CInstantSendManager::%s -- txid=%s, islock=%s: processsing islock, peer=%d\n", __func__,
|
|
islock.txid.ToString(), hash.ToString(), from);
|
|
|
|
creatingInstantSendLocks.erase(islock.GetRequestId());
|
|
txToCreatingInstantSendLocks.erase(islock.txid);
|
|
|
|
CInstantSendLockPtr otherIsLock;
|
|
if (db.GetInstantSendLockByHash(hash)) {
|
|
return;
|
|
}
|
|
otherIsLock = db.GetInstantSendLockByTxid(islock.txid);
|
|
if (otherIsLock != nullptr) {
|
|
LogPrintf("CInstantSendManager::%s -- txid=%s, islock=%s: duplicate islock, other islock=%s, peer=%d\n", __func__,
|
|
islock.txid.ToString(), hash.ToString(), ::SerializeHash(*otherIsLock).ToString(), from);
|
|
}
|
|
for (auto& in : islock.inputs) {
|
|
otherIsLock = db.GetInstantSendLockByInput(in);
|
|
if (otherIsLock != nullptr) {
|
|
LogPrintf("CInstantSendManager::%s -- txid=%s, islock=%s: conflicting input in islock. input=%s, other islock=%s, peer=%d\n", __func__,
|
|
islock.txid.ToString(), hash.ToString(), in.ToStringShort(), ::SerializeHash(*otherIsLock).ToString(), from);
|
|
}
|
|
}
|
|
|
|
db.WriteNewInstantSendLock(hash, islock);
|
|
if (pindexMined) {
|
|
db.WriteInstantSendLockMined(hash, pindexMined->nHeight);
|
|
}
|
|
}
|
|
|
|
CInv inv(MSG_ISLOCK, hash);
|
|
if (tx != nullptr) {
|
|
g_connman->RelayInvFiltered(inv, *tx, LLMQS_PROTO_VERSION);
|
|
} else {
|
|
// we don't have the TX yet, so we only filter based on txid. Later when that TX arrives, we will re-announce
|
|
// with the TX taken into account.
|
|
g_connman->RelayInvFiltered(inv, islock.txid, LLMQS_PROTO_VERSION);
|
|
}
|
|
|
|
RemoveMempoolConflictsForLock(hash, islock);
|
|
RetryLockTxs(islock.txid);
|
|
|
|
UpdateWalletTransaction(islock.txid, tx);
|
|
}
|
|
|
|
void CInstantSendManager::UpdateWalletTransaction(const uint256& txid, const CTransactionRef& tx)
|
|
{
|
|
#ifdef ENABLE_WALLET
|
|
if (!pwalletMain) {
|
|
return;
|
|
}
|
|
|
|
if (pwalletMain->UpdatedTransaction(txid)) {
|
|
// bumping this to update UI
|
|
nCompleteTXLocks++;
|
|
// notify an external script once threshold is reached
|
|
std::string strCmd = GetArg("-instantsendnotify", "");
|
|
if (!strCmd.empty()) {
|
|
boost::replace_all(strCmd, "%s", txid.GetHex());
|
|
boost::thread t(runCommand, strCmd); // thread runs free
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (tx) {
|
|
GetMainSignals().NotifyTransactionLock(*tx);
|
|
}
|
|
}
|
|
|
|
void CInstantSendManager::SyncTransaction(const CTransaction& tx, const CBlockIndex* pindex, int posInBlock)
|
|
{
|
|
if (!IsNewInstantSendEnabled()) {
|
|
return;
|
|
}
|
|
|
|
if (tx.IsCoinBase() || tx.vin.empty()) {
|
|
// coinbase can't and TXs with no inputs be locked
|
|
return;
|
|
}
|
|
|
|
uint256 islockHash;
|
|
{
|
|
LOCK(cs);
|
|
islockHash = db.GetInstantSendLockHashByTxid(tx.GetHash());
|
|
|
|
// update DB about when an IS lock was mined
|
|
if (!islockHash.IsNull() && pindex) {
|
|
if (posInBlock == CMainSignals::SYNC_TRANSACTION_NOT_IN_BLOCK) {
|
|
db.RemoveInstantSendLockMined(islockHash, pindex->nHeight);
|
|
} else {
|
|
db.WriteInstantSendLockMined(islockHash, pindex->nHeight);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool chainlocked = pindex && chainLocksHandler->HasChainLock(pindex->nHeight, pindex->GetBlockHash());
|
|
if (!islockHash.IsNull() || chainlocked) {
|
|
RetryLockTxs(tx.GetHash());
|
|
} else {
|
|
ProcessTx(tx, Params().GetConsensus());
|
|
}
|
|
}
|
|
|
|
void CInstantSendManager::NotifyChainLock(const CBlockIndex* pindexChainLock)
|
|
{
|
|
HandleFullyConfirmedBlock(pindexChainLock);
|
|
}
|
|
|
|
void CInstantSendManager::UpdatedBlockTip(const CBlockIndex* pindexNew)
|
|
{
|
|
// TODO remove this after DIP8 has activated
|
|
bool fDIP0008Active = VersionBitsState(pindexNew->pprev, Params().GetConsensus(), Consensus::DEPLOYMENT_DIP0008, versionbitscache) == THRESHOLD_ACTIVE;
|
|
|
|
if (sporkManager.IsSporkActive(SPORK_19_CHAINLOCKS_ENABLED) && fDIP0008Active) {
|
|
// Nothing to do here. We should keep all islocks and let chainlocks handle them.
|
|
return;
|
|
}
|
|
|
|
int nConfirmedHeight = pindexNew->nHeight - Params().GetConsensus().nInstantSendKeepLock;
|
|
const CBlockIndex* pindex = pindexNew->GetAncestor(nConfirmedHeight);
|
|
|
|
if (pindex) {
|
|
HandleFullyConfirmedBlock(pindex);
|
|
}
|
|
}
|
|
|
|
void CInstantSendManager::HandleFullyConfirmedBlock(const CBlockIndex* pindex)
|
|
{
|
|
std::unordered_map<uint256, CInstantSendLockPtr> removeISLocks;
|
|
{
|
|
LOCK(cs);
|
|
|
|
removeISLocks = db.RemoveConfirmedInstantSendLocks(pindex->nHeight);
|
|
for (auto& p : removeISLocks) {
|
|
auto& islockHash = p.first;
|
|
auto& islock = p.second;
|
|
LogPrint("instantsend", "CInstantSendManager::%s -- txid=%s, islock=%s: removed islock as it got fully confirmed\n", __func__,
|
|
islock->txid.ToString(), islockHash.ToString());
|
|
|
|
for (auto& in : islock->inputs) {
|
|
auto inputRequestId = ::SerializeHash(std::make_pair(INPUTLOCK_REQUESTID_PREFIX, in));
|
|
inputRequestIds.erase(inputRequestId);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (auto& p : removeISLocks) {
|
|
UpdateWalletTransaction(p.second->txid, nullptr);
|
|
}
|
|
|
|
// Retry all not yet locked mempool TXs and TX which where mined after the fully confirmed block
|
|
RetryLockTxs(uint256());
|
|
}
|
|
|
|
void CInstantSendManager::RemoveMempoolConflictsForLock(const uint256& hash, const CInstantSendLock& islock)
|
|
{
|
|
LOCK(mempool.cs);
|
|
|
|
std::unordered_map<uint256, CTransactionRef> toDelete;
|
|
|
|
for (auto& in : islock.inputs) {
|
|
auto it = mempool.mapNextTx.find(in);
|
|
if (it == mempool.mapNextTx.end()) {
|
|
continue;
|
|
}
|
|
if (it->second->GetHash() != islock.txid) {
|
|
toDelete.emplace(it->second->GetHash(), mempool.get(it->second->GetHash()));
|
|
|
|
LogPrintf("CInstantSendManager::%s -- txid=%s, islock=%s: mempool TX %s with input %s conflicts with islock\n", __func__,
|
|
islock.txid.ToString(), hash.ToString(), it->second->GetHash().ToString(), in.ToStringShort());
|
|
}
|
|
}
|
|
|
|
for (auto& p : toDelete) {
|
|
mempool.removeRecursive(*p.second, MemPoolRemovalReason::CONFLICT);
|
|
}
|
|
}
|
|
|
|
void CInstantSendManager::RetryLockTxs(const uint256& lockedParentTx)
|
|
{
|
|
if (!IsNewInstantSendEnabled()) {
|
|
return;
|
|
}
|
|
|
|
// Let's retry all unlocked TXs from mempool and and recently connected blocks
|
|
|
|
std::unordered_map<uint256, CTransactionRef> txs;
|
|
|
|
{
|
|
LOCK(mempool.cs);
|
|
|
|
if (lockedParentTx.IsNull()) {
|
|
txs.reserve(mempool.mapTx.size());
|
|
for (auto it = mempool.mapTx.begin(); it != mempool.mapTx.end(); ++it) {
|
|
txs.emplace(it->GetTx().GetHash(), it->GetSharedTx());
|
|
}
|
|
} else {
|
|
auto it = mempool.mapNextTx.lower_bound(COutPoint(lockedParentTx, 0));
|
|
while (it != mempool.mapNextTx.end() && it->first->hash == lockedParentTx) {
|
|
txs.emplace(it->second->GetHash(), mempool.get(it->second->GetHash()));
|
|
++it;
|
|
}
|
|
}
|
|
}
|
|
|
|
const CBlockIndex* pindexWalk = nullptr;
|
|
{
|
|
LOCK(cs_main);
|
|
pindexWalk = chainActive.Tip();
|
|
}
|
|
|
|
// scan blocks until we hit the last chainlocked block we know of. Also stop scanning after a depth of 6 to avoid
|
|
// signing thousands of TXs at once. Also, after a depth of 6, blocks get eligible for ChainLocking even if unsafe
|
|
// TXs are included, so there is no need to retroactively sign these.
|
|
int depth = 0;
|
|
while (pindexWalk && depth < 6) {
|
|
if (chainLocksHandler->HasChainLock(pindexWalk->nHeight, pindexWalk->GetBlockHash())) {
|
|
break;
|
|
}
|
|
|
|
CBlock block;
|
|
{
|
|
LOCK(cs_main);
|
|
if (!ReadBlockFromDisk(block, pindexWalk, Params().GetConsensus())) {
|
|
pindexWalk = pindexWalk->pprev;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
for (const auto& tx : block.vtx) {
|
|
if (lockedParentTx.IsNull()) {
|
|
txs.emplace(tx->GetHash(), tx);
|
|
} else {
|
|
bool isChild = false;
|
|
for (auto& in : tx->vin) {
|
|
if (in.prevout.hash == lockedParentTx) {
|
|
isChild = true;
|
|
break;
|
|
}
|
|
}
|
|
if (isChild) {
|
|
txs.emplace(tx->GetHash(), tx);
|
|
}
|
|
}
|
|
}
|
|
|
|
pindexWalk = pindexWalk->pprev;
|
|
depth++;
|
|
}
|
|
|
|
for (auto& p : txs) {
|
|
auto& tx = p.second;
|
|
{
|
|
LOCK(cs);
|
|
if (txToCreatingInstantSendLocks.count(tx->GetHash())) {
|
|
// we're already in the middle of locking this one
|
|
continue;
|
|
}
|
|
if (IsLocked(tx->GetHash())) {
|
|
continue;
|
|
}
|
|
if (IsConflicted(*tx)) {
|
|
// should not really happen as we have already filtered these out
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// CheckCanLock is already called by ProcessTx, so we should avoid calling it twice. But we also shouldn't spam
|
|
// the logs when retrying TXs that are not ready yet.
|
|
if (LogAcceptCategory("instantsend")) {
|
|
if (!CheckCanLock(*tx, false, Params().GetConsensus())) {
|
|
continue;
|
|
}
|
|
LogPrint("instantsend", "CInstantSendManager::%s -- txid=%s: retrying to lock\n", __func__,
|
|
tx->GetHash().ToString());
|
|
}
|
|
|
|
ProcessTx(*tx, Params().GetConsensus());
|
|
}
|
|
}
|
|
|
|
bool CInstantSendManager::AlreadyHave(const CInv& inv)
|
|
{
|
|
if (!IsNewInstantSendEnabled()) {
|
|
return true;
|
|
}
|
|
|
|
LOCK(cs);
|
|
return db.GetInstantSendLockByHash(inv.hash) != nullptr || pendingInstantSendLocks.count(inv.hash) != 0;
|
|
}
|
|
|
|
bool CInstantSendManager::GetInstantSendLockByHash(const uint256& hash, llmq::CInstantSendLock& ret)
|
|
{
|
|
if (!IsNewInstantSendEnabled()) {
|
|
return false;
|
|
}
|
|
|
|
LOCK(cs);
|
|
auto islock = db.GetInstantSendLockByHash(hash);
|
|
if (!islock) {
|
|
return false;
|
|
}
|
|
ret = *islock;
|
|
return true;
|
|
}
|
|
|
|
bool CInstantSendManager::IsLocked(const uint256& txHash)
|
|
{
|
|
if (!IsNewInstantSendEnabled()) {
|
|
return false;
|
|
}
|
|
|
|
LOCK(cs);
|
|
return db.GetInstantSendLockByTxid(txHash) != nullptr;
|
|
}
|
|
|
|
bool CInstantSendManager::IsConflicted(const CTransaction& tx)
|
|
{
|
|
LOCK(cs);
|
|
uint256 dummy;
|
|
return GetConflictingTx(tx, dummy);
|
|
}
|
|
|
|
bool CInstantSendManager::GetConflictingTx(const CTransaction& tx, uint256& retConflictTxHash)
|
|
{
|
|
if (!IsNewInstantSendEnabled()) {
|
|
return false;
|
|
}
|
|
|
|
LOCK(cs);
|
|
for (const auto& in : tx.vin) {
|
|
auto otherIsLock = db.GetInstantSendLockByInput(in.prevout);
|
|
if (!otherIsLock) {
|
|
continue;
|
|
}
|
|
|
|
if (otherIsLock->txid != tx.GetHash()) {
|
|
retConflictTxHash = otherIsLock->txid;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void CInstantSendManager::WorkThreadMain()
|
|
{
|
|
while (!workInterrupt) {
|
|
bool didWork = false;
|
|
|
|
didWork |= ProcessPendingInstantSendLocks();
|
|
|
|
if (!didWork) {
|
|
if (!workInterrupt.sleep_for(std::chrono::milliseconds(100))) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool IsOldInstantSendEnabled()
|
|
{
|
|
return sporkManager.IsSporkActive(SPORK_2_INSTANTSEND_ENABLED) && !sporkManager.IsSporkActive(SPORK_20_INSTANTSEND_LLMQ_BASED);
|
|
}
|
|
|
|
bool IsNewInstantSendEnabled()
|
|
{
|
|
return sporkManager.IsSporkActive(SPORK_2_INSTANTSEND_ENABLED) && sporkManager.IsSporkActive(SPORK_20_INSTANTSEND_LLMQ_BASED);
|
|
}
|
|
|
|
bool IsInstantSendEnabled()
|
|
{
|
|
return sporkManager.IsSporkActive(SPORK_2_INSTANTSEND_ENABLED);
|
|
}
|
|
|
|
}
|