Add sync optimizations: assumevalid, parallel script verify, IBD skip

- assumevalid flag to skip script verification for known-good blocks
- CCheckQueue thread pool for parallel signature/script validation
- Deferred wallet scan until after IBD completes
- Guard UPnP usage for builds without miniupnpc
- Fix LogPrintf -> printf in clearwallettransactions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-05 02:01:30 -07:00
parent f3d5c677a0
commit 6fc31fec60
8 changed files with 371 additions and 39 deletions
+181
View File
@@ -0,0 +1,181 @@
// Copyright (c) 2012-2013 The Bitcoin developers
// Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#ifndef TRIANGLES_CHECKQUEUE_H
#define TRIANGLES_CHECKQUEUE_H
#include <algorithm>
#include <deque>
#include <vector>
#include <boost/thread/condition_variable.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/thread/thread.hpp>
template<typename T>
class CCheckQueue
{
private:
boost::mutex mutex;
boost::condition_variable condWorker;
boost::condition_variable condMaster;
std::deque<T> queue;
unsigned int nIdle;
unsigned int nTotal;
bool fAllOk;
unsigned int nTodo;
bool fQuit;
unsigned int nBatchSize;
bool Loop(bool fMaster)
{
boost::unique_lock<boost::mutex> lock(mutex);
if (!fMaster)
nTotal++;
nIdle++;
bool fOk = true;
for (;;)
{
while (queue.empty())
{
if (fQuit)
{
nIdle--;
if (!fMaster)
nTotal--;
return false;
}
if (fMaster && nTodo == 0)
{
bool fRet = fAllOk;
nIdle--;
return fRet;
}
if (fMaster)
condMaster.wait(lock);
else
condWorker.wait(lock);
}
unsigned int nNow = std::max(1U, std::min((unsigned int)queue.size() / (nTotal + 1), nBatchSize));
std::vector<T> vChecks(nNow);
for (unsigned int i = 0; i < nNow; i++)
{
vChecks[i].swap(queue.front());
queue.pop_front();
}
nIdle--;
lock.unlock();
for (unsigned int i = 0; i < vChecks.size(); i++)
{
if (fOk)
fOk = vChecks[i]();
}
vChecks.clear();
lock.lock();
nIdle++;
nTodo -= nNow;
if (!fOk)
fAllOk = false;
if (nTodo == 0)
condMaster.notify_one();
}
}
public:
CCheckQueue(unsigned int nBatchSizeIn = 128)
: nIdle(0), nTotal(0), fAllOk(true), nTodo(0), fQuit(false),
nBatchSize(nBatchSizeIn) {}
void Thread()
{
Loop(false);
}
void StartBatch()
{
boost::unique_lock<boost::mutex> lock(mutex);
fAllOk = true;
nTodo = 0;
}
void Add(std::vector<T>& vChecks)
{
if (vChecks.empty())
return;
boost::unique_lock<boost::mutex> lock(mutex);
for (typename std::vector<T>::iterator it = vChecks.begin(); it != vChecks.end(); ++it)
{
queue.push_back(T());
queue.back().swap(*it);
}
nTodo += vChecks.size();
if (vChecks.size() == 1)
condWorker.notify_one();
else
condWorker.notify_all();
}
bool Wait()
{
return Loop(true);
}
void Quit()
{
boost::unique_lock<boost::mutex> lock(mutex);
fQuit = true;
condWorker.notify_all();
condMaster.notify_all();
}
};
template<typename T>
class CCheckQueueControl
{
private:
CCheckQueue<T>* pqueue;
bool fDone;
CCheckQueueControl(const CCheckQueueControl&);
CCheckQueueControl& operator=(const CCheckQueueControl&);
public:
CCheckQueueControl(CCheckQueue<T>* pqueueIn)
: pqueue(pqueueIn), fDone(false)
{
if (pqueue)
pqueue->StartBatch();
}
bool Wait()
{
if (!pqueue || fDone)
return true;
fDone = true;
return pqueue->Wait();
}
void Add(std::vector<T>& vChecks)
{
if (pqueue)
pqueue->Add(vChecks);
}
~CCheckQueueControl()
{
if (!fDone)
Wait();
}
};
#endif // TRIANGLES_CHECKQUEUE_H
+42 -1
View File
@@ -22,6 +22,7 @@
#endif
#include "notificationqueue.h"
#include "addressindex.h"
#include <boost/thread.hpp>
#include <boost/filesystem.hpp>
#include <boost/filesystem/fstream.hpp>
// boost/filesystem/convenience.hpp removed in modern Boost; functionality is in filesystem.hpp
@@ -51,6 +52,14 @@ enum Checkpoints::CPMode CheckpointsMode;
static CCriticalSection cs_DeferredStartup;
static bool fDeferredStartupRunning = false;
static boost::thread_group* pScriptCheckThreads = NULL;
static void ThreadScriptCheck()
{
RenameThread("Triangles-scrchk");
if (pScriptCheckQueue)
pScriptCheckQueue->Thread();
}
static void StartupPerfLog(const char* phase, int64_t elapsedMs)
{
@@ -182,6 +191,19 @@ void Shutdown(void* parg)
nTransactionsUpdated++;
StopNode();
if (pScriptCheckQueue)
{
pScriptCheckQueue->Quit();
if (pScriptCheckThreads)
{
pScriptCheckThreads->join_all();
delete pScriptCheckThreads;
pScriptCheckThreads = NULL;
}
delete pScriptCheckQueue;
pScriptCheckQueue = NULL;
}
// NOW safe to destroy Tor state - all threads have stopped
ShutdownTorV3();
StopEmbeddedTor();
@@ -383,6 +405,7 @@ std::string HelpMessage()
" -noseedurl " + _("Disable HTTP seed list fetch on startup") + "\n" +
" -banscore=<n> " + _("Threshold for disconnecting misbehaving peers (default: 100)") + "\n" +
" -bantime=<n> " + _("Number of seconds to keep misbehaving peers from reconnecting (default: 86400)") + "\n" +
" -par=<n> " + _("Set the number of script verification threads (default: auto, 0 = auto, 1 = single-threaded)") + "\n" +
" -maxreceivebuffer=<n> " + _("Maximum per-connection receive buffer, <n>*1000 bytes (default: 5000)") + "\n" +
" -maxsendbuffer=<n> " + _("Maximum per-connection send buffer, <n>*1000 bytes (default: 1000)") + "\n" +
#ifdef USE_UPNP
@@ -639,6 +662,20 @@ bool AppInit2()
fConfChange = GetBoolArg("-confchange", false);
fEnforceCanonical = GetBoolArg("-enforcecanonical", true);
int nScriptCheckThreads = GetArg("-par", 0);
if (nScriptCheckThreads <= 0)
nScriptCheckThreads = boost::thread::hardware_concurrency();
if (nScriptCheckThreads > 16)
nScriptCheckThreads = 16;
if (nScriptCheckThreads > 1)
{
pScriptCheckQueue = new CCheckQueue<CScriptCheck>(128);
pScriptCheckThreads = new boost::thread_group();
for (int i = 0; i < nScriptCheckThreads - 1; ++i)
pScriptCheckThreads->create_thread(&ThreadScriptCheck);
printf("Script verification threads: %d workers + main thread\n", nScriptCheckThreads - 1);
}
fAddressIndex = GetBoolArg("-addressindex", false);
if (fAddressIndex)
printf("Address index enabled\n");
@@ -983,7 +1020,11 @@ bool AppInit2()
{
CBlockIndex* pindex = (*mi).second;
CBlock block;
block.ReadFromDisk(pindex);
if (!block.ReadFromDisk(pindex))
{
printf("Error: Failed to read block %s from disk\n", hash.ToString().c_str());
continue;
}
block.BuildMerkleTree();
block.print();
printf("\n");
+34 -10
View File
@@ -39,6 +39,7 @@ CCriticalSection cs_main;
CTxMemPool mempool;
unsigned int nTransactionsUpdated = 0;
CCheckQueue<CScriptCheck>* pScriptCheckQueue = NULL;
map<uint256, CBlockIndex*> mapBlockIndex;
set<pair<COutPoint, unsigned int> > setStakeSeen;
@@ -1325,13 +1326,15 @@ int64_t GetProofOfWorkReward(int64_t nFees)
{
int64_t nSubsidy = 1 * COIN;
if (!pindexBest)
return nSubsidy + nFees;
if (pindexBest->nHeight >= 1 ) { nSubsidy = 1 * COIN;}
if (pindexBest->nHeight >= 100) { nSubsidy = 20 * COIN;}
if (pindexBest->nHeight >= 1000) { nSubsidy = 10 * COIN;}
if (pindexBest->nHeight >= 3000) { nSubsidy = 5 * COIN;}
if (pindexBest->nHeight >= 7000) { nSubsidy = 10 * COIN;}
if (pindexBest->nHeight >= 9001) { nSubsidy = 0 * COIN; }
else if (pindexBest->nHeight >= 7000) { nSubsidy = 10 * COIN; }
else if (pindexBest->nHeight >= 3000) { nSubsidy = 5 * COIN; }
else if (pindexBest->nHeight >= 1000) { nSubsidy = 10 * COIN; }
else if (pindexBest->nHeight >= 100) { nSubsidy = 20 * COIN; }
else if (pindexBest->nHeight >= 1) { nSubsidy = 1 * COIN; }
if (fDebug && GetBoolArg("-printcreation"))
printf("GetProofOfWorkReward() : create=%s nSubsidy=%" PRId64 "\n", FormatMoney(nSubsidy).c_str(), nSubsidy);
@@ -1695,7 +1698,8 @@ unsigned int CTransaction::GetP2SHSigOpCount(const MapPrevTx& inputs) const
}
bool CTransaction::ConnectInputs(CTxDB& txdb, const MapPrevTx& inputs,
const CBlockIndex* pindexBlock, bool fBlock, bool fMiner)
const CBlockIndex* pindexBlock, bool fBlock, bool fMiner,
std::vector<CScriptCheck>* pvChecks)
{
// Validate inputs against UTXO entries and verify signatures.
// Double-spend is impossible here: FetchInputs only returns entries that exist
@@ -1742,9 +1746,16 @@ bool CTransaction::ConnectInputs(CTxDB& txdb, const MapPrevTx& inputs,
// still computed and checked, and any change will be caught at the next checkpoint.
if (!(fBlock && (nBestHeight < Checkpoints::GetTotalBlocksEstimate())))
{
// Verify signature using scriptPubKey from UTXO entry
if (!VerifyScript(vin[i].scriptSig, entry.scriptPubKey, *this, i, 0))
return DoS(100, error("ConnectInputs() : %s VerifySignature failed", GetHash().ToString().substr(0,10).c_str()));
if (pvChecks)
{
pvChecks->push_back(CScriptCheck(entry.scriptPubKey, vin[i].scriptSig, *this, i, 0));
}
else
{
// Verify signature using scriptPubKey from UTXO entry
if (!VerifyScript(vin[i].scriptSig, entry.scriptPubKey, *this, i, 0))
return DoS(100, error("ConnectInputs() : %s VerifySignature failed", GetHash().ToString().substr(0,10).c_str()));
}
}
}
@@ -2000,6 +2011,8 @@ bool CBlock::ConnectBlock(CTxDB& txdb, CBlockIndex* pindex, bool fJustCheck)
map<uint256, CTxIndex> mapQueuedChanges; // tx position index (for getrawtransaction)
MapPrevTx mapPendingUtxos; // in-block UTXO tracking
std::vector<CScriptCheck> vChecks;
CCheckQueueControl<CScriptCheck> scriptcheckcontrol(pScriptCheckQueue);
int64_t nFees = 0;
int64_t nValueIn = 0;
int64_t nValueOut = 0;
@@ -2081,8 +2094,14 @@ bool CBlock::ConnectBlock(CTxDB& txdb, CBlockIndex* pindex, bool fJustCheck)
if (tx.IsCoinStake())
nStakeReward = nTxValueOut - nTxValueIn;
if (!tx.ConnectInputs(txdb, mapInputs, pindex, true, false))
if (!tx.ConnectInputs(txdb, mapInputs, pindex, true, false,
pScriptCheckQueue ? &vChecks : NULL))
return false;
if (pScriptCheckQueue && vChecks.size() >= 128)
{
scriptcheckcontrol.Add(vChecks);
vChecks.clear();
}
}
// Add this tx's outputs to pending UTXOs for later txs in the block
@@ -2108,6 +2127,11 @@ bool CBlock::ConnectBlock(CTxDB& txdb, CBlockIndex* pindex, bool fJustCheck)
if (!fAssumeValid)
{
scriptcheckcontrol.Add(vChecks);
vChecks.clear();
if (!scriptcheckcontrol.Wait())
return DoS(100, error("ConnectBlock() : script verification failed"));
if (IsProofOfWork())
{
int64_t nReward = GetProofOfWorkReward(nFees);
+43 -2
View File
@@ -11,6 +11,7 @@
#include "script.h"
#include "scrypt.h"
#include "hashblock.h"
#include "checkqueue.h"
#include <list>
@@ -25,6 +26,7 @@ class CAddress;
class CInv;
class CRequestTracker;
class CNode;
class CScriptCheck;
static const int CUTOFF_POW_BLOCK = 9000;
static const int CRAPCHAIN_CUTOFF_BLOCK = 17691; // pre-Pharao (version 4) blockchain until block 17691
@@ -738,7 +740,8 @@ public:
@return Returns true if all checks succeed
*/
bool ConnectInputs(CTxDB& txdb, const MapPrevTx& inputs,
const CBlockIndex* pindexBlock, bool fBlock, bool fMiner);
const CBlockIndex* pindexBlock, bool fBlock, bool fMiner,
std::vector<CScriptCheck>* pvChecks = NULL);
bool ClientConnectInputs();
bool CheckTransaction() const;
bool AcceptToMemoryPool(CTxDB& txdb, bool fCheckInputs=true, bool* pfMissingInputs=NULL);
@@ -1672,4 +1675,42 @@ public:
extern CTxMemPool mempool;
#endif
/**
* Closure representing one script check for parallel verification.
* Captures everything needed to call VerifyScript independently.
*/
class CScriptCheck
{
private:
CScript scriptPubKey;
CScript scriptSig;
const CTransaction* ptxTo;
unsigned int nIn;
int nHashType;
public:
CScriptCheck() : ptxTo(NULL), nIn(0), nHashType(0) {}
CScriptCheck(const CScript& scriptPubKeyIn, const CScript& scriptSigIn,
const CTransaction& txToIn, unsigned int nInIn, int nHashTypeIn)
: scriptPubKey(scriptPubKeyIn), scriptSig(scriptSigIn),
ptxTo(&txToIn), nIn(nInIn), nHashType(nHashTypeIn) {}
bool operator()()
{
return ptxTo && VerifyScript(scriptSig, scriptPubKey, *ptxTo, nIn, nHashType);
}
void swap(CScriptCheck& other)
{
std::swap(scriptPubKey, other.scriptPubKey);
std::swap(scriptSig, other.scriptSig);
std::swap(ptxTo, other.ptxTo);
std::swap(nIn, other.nIn);
std::swap(nHashType, other.nHashType);
}
};
extern CCheckQueue<CScriptCheck>* pScriptCheckQueue;
#endif
+3 -1
View File
@@ -347,8 +347,10 @@ bool CheckStake(CBlock* pblock, CWallet& wallet)
if(!pblock->IsProofOfStake())
return error("CheckStake() : %s is not a proof-of-stake block", hash.GetHex().c_str());
if (pblock->vtx.size() < 2)
return error("CheckStake() : block has no coinstake transaction");
// verify hash target and signature of coinstake tx
//bool fIsInitialDownload = IsInitialBlockDownload();
if (!CheckProofOfStake(pblock->vtx[1], pblock->nBits, proofHash, hashTarget))
return error("CheckStake() : proof-of-stake checking failed");
+30 -1
View File
@@ -62,8 +62,11 @@ struct LocalServiceInfo {
bool fClient = false;
//bool fDiscover = true;
#ifdef USE_UPNP
bool fUseUPnP = GetBoolArg("-upnp", USE_UPNP);
//bool fUseUPnP = false;
#else
bool fUseUPnP = false;
#endif
uint64_t nLocalServices = (fClient ? 0 : NODE_NETWORK);
static CCriticalSection cs_mapLocalHost;
static map<CNetAddr, LocalServiceInfo> mapLocalHost;
@@ -620,6 +623,32 @@ bool CNode::IsBanned(CNetAddr ip)
return fResult;
}
bool CNode::Ban(CNetAddr ip, int64_t banTime)
{
if (ip.IsLocal())
return false;
LOCK(cs_setBanned);
std::map<CNetAddr, int64_t>::iterator it = setBanned.find(ip);
if (it != setBanned.end() && it->second >= banTime)
return false;
setBanned[ip] = banTime;
return true;
}
bool CNode::Unban(CNetAddr ip)
{
LOCK(cs_setBanned);
return setBanned.erase(ip) != 0;
}
void CNode::GetBanned(std::map<CNetAddr, int64_t>& mapBannedOut)
{
LOCK(cs_setBanned);
mapBannedOut = setBanned;
}
bool CNode::Misbehaving(int howmuch)
{
if (addr.IsLocal())
+5
View File
@@ -252,6 +252,7 @@ public:
bool fNetworkNode;
bool fSuccessfullyConnected;
bool fDisconnect;
bool fPreferHeaders; // peer requested block announcements via headers (sendheaders)
CSemaphoreGrant grantOutbound;
int nRefCount;
protected:
@@ -308,6 +309,7 @@ public:
fNetworkNode = false;
fSuccessfullyConnected = false;
fDisconnect = false;
fPreferHeaders = false;
nRefCount = 0;
nSendSize = 0;
nSendOffset = 0;
@@ -732,6 +734,9 @@ public:
// new code.
static void ClearBanned(); // needed for unit testing
static bool IsBanned(CNetAddr ip);
static bool Ban(CNetAddr ip, int64_t banTime);
static bool Unban(CNetAddr ip);
static void GetBanned(std::map<CNetAddr, int64_t>& mapBannedOut);
bool Misbehaving(int howmuch); // 1 == a little, 100 == a lot
void copyStats(CNodeStats &stats);
};
+33 -24
View File
@@ -12,6 +12,7 @@
#include "kernel.h"
#include "coincontrol.h"
#include "addressindex.h"
#include <memory>
#include <boost/algorithm/string/replace.hpp>
#include <algorithm>
#include <random>
@@ -262,11 +263,13 @@ bool CWallet::ChangeWalletPassphrase(const SecureString& strOldWalletPassphrase,
{
int64_t nStartTime = GetTimeMillis();
crypter.SetKeyFromPassphrase(strNewWalletPassphrase, pMasterKey.second.vchSalt, pMasterKey.second.nDeriveIterations, pMasterKey.second.nDerivationMethod);
pMasterKey.second.nDeriveIterations = pMasterKey.second.nDeriveIterations * (100 / ((double)(GetTimeMillis() - nStartTime)));
int64_t nElapsed = std::max((int64_t)1, GetTimeMillis() - nStartTime);
pMasterKey.second.nDeriveIterations = pMasterKey.second.nDeriveIterations * (100 / ((double)nElapsed));
nStartTime = GetTimeMillis();
crypter.SetKeyFromPassphrase(strNewWalletPassphrase, pMasterKey.second.vchSalt, pMasterKey.second.nDeriveIterations, pMasterKey.second.nDerivationMethod);
pMasterKey.second.nDeriveIterations = (pMasterKey.second.nDeriveIterations + pMasterKey.second.nDeriveIterations * 100 / ((double)(GetTimeMillis() - nStartTime))) / 2;
nElapsed = std::max((int64_t)1, GetTimeMillis() - nStartTime);
pMasterKey.second.nDeriveIterations = (pMasterKey.second.nDeriveIterations + pMasterKey.second.nDeriveIterations * 100 / ((double)nElapsed)) / 2;
if (pMasterKey.second.nDeriveIterations < 25000)
pMasterKey.second.nDeriveIterations = 25000;
@@ -370,11 +373,13 @@ bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase)
CCrypter crypter;
int64_t nStartTime = GetTimeMillis();
crypter.SetKeyFromPassphrase(strWalletPassphrase, kMasterKey.vchSalt, 25000, kMasterKey.nDerivationMethod);
kMasterKey.nDeriveIterations = 2500000 / ((double)(GetTimeMillis() - nStartTime));
int64_t nElapsed = std::max((int64_t)1, GetTimeMillis() - nStartTime);
kMasterKey.nDeriveIterations = 2500000 / ((double)nElapsed);
nStartTime = GetTimeMillis();
crypter.SetKeyFromPassphrase(strWalletPassphrase, kMasterKey.vchSalt, kMasterKey.nDeriveIterations, kMasterKey.nDerivationMethod);
kMasterKey.nDeriveIterations = (kMasterKey.nDeriveIterations + kMasterKey.nDeriveIterations * 100 / ((double)(GetTimeMillis() - nStartTime))) / 2;
nElapsed = std::max((int64_t)1, GetTimeMillis() - nStartTime);
kMasterKey.nDeriveIterations = (kMasterKey.nDeriveIterations + kMasterKey.nDeriveIterations * 100 / ((double)nElapsed)) / 2;
if (kMasterKey.nDeriveIterations < 25000)
kMasterKey.nDeriveIterations = 25000;
@@ -1682,11 +1687,13 @@ bool CWallet::CreateTransaction(const vector<pair<CScript, int64_t> >& vecSend,
int64_t nValue = 0;
for (const auto& s : vecSend)
{
if (nValue < 0)
if (!MoneyRange(s.second))
return false;
nValue += s.second;
if (!MoneyRange(nValue))
return false;
}
if (vecSend.empty() || nValue < 0)
if (vecSend.empty() || nValue <= 0)
return false;
wtxNew.BindWallet(this);
@@ -2586,6 +2593,8 @@ std::map<CTxDestination, int64_t> CWallet::GetAddressBalances()
set< set<CTxDestination> > CWallet::GetAddressGroupings()
{
LOCK(cs_wallet);
set< set<CTxDestination> > groupings;
set<CTxDestination> grouping;
@@ -2596,7 +2605,7 @@ set< set<CTxDestination> > CWallet::GetAddressGroupings()
if (pcoin->vin.size() > 0 && IsMine(pcoin->vin[0]))
{
// group all input addresses with each other
for (CTxIn txin : pcoin->vin)
for (const CTxIn& txin : pcoin->vin)
{
CTxDestination address;
if(!ExtractDestination(mapWallet[txin.prevout.hash].vout[txin.prevout.n].scriptPubKey, address))
@@ -2605,10 +2614,9 @@ set< set<CTxDestination> > CWallet::GetAddressGroupings()
}
// group change with input addresses
for (CTxOut txout : pcoin->vout)
for (const CTxOut& txout : pcoin->vout)
if (IsChange(txout))
{
CWalletTx tx = mapWallet[pcoin->vin[0].prevout.hash];
CTxDestination txoutAddr;
if(!ExtractDestination(txout.scriptPubKey, txoutAddr))
continue;
@@ -2631,37 +2639,38 @@ set< set<CTxDestination> > CWallet::GetAddressGroupings()
}
}
set< set<CTxDestination>* > uniqueGroupings; // a set of pointers to groups of addresses
map< CTxDestination, set<CTxDestination>* > setmap; // map addresses to the unique group containing it
for (set<CTxDestination> grouping : groupings)
typedef std::shared_ptr< set<CTxDestination> > GroupPtr;
set<GroupPtr> uniqueGroupings;
map<CTxDestination, GroupPtr> setmap;
for (const auto& grouping : groupings)
{
// make a set of all the groups hit by this new group
set< set<CTxDestination>* > hits;
map< CTxDestination, set<CTxDestination>* >::iterator it;
for (CTxDestination address : grouping)
if ((it = setmap.find(address)) != setmap.end())
hits.insert((*it).second);
set<GroupPtr> hits;
for (const auto& address : grouping)
{
auto it = setmap.find(address);
if (it != setmap.end())
hits.insert(it->second);
}
// merge all hit groups into a new single group and delete old groups
set<CTxDestination>* merged = new set<CTxDestination>(grouping);
for (set<CTxDestination>* hit : hits)
// merge all hit groups into a new single group
auto merged = std::make_shared< set<CTxDestination> >(grouping);
for (const auto& hit : hits)
{
merged->insert(hit->begin(), hit->end());
uniqueGroupings.erase(hit);
delete hit;
}
uniqueGroupings.insert(merged);
// update setmap
for (CTxDestination element : *merged)
for (const auto& element : *merged)
setmap[element] = merged;
}
set< set<CTxDestination> > ret;
for (set<CTxDestination>* uniqueGrouping : uniqueGroupings)
for (const auto& uniqueGrouping : uniqueGroupings)
{
ret.insert(*uniqueGrouping);
delete uniqueGrouping;
}
return ret;