Skip to content

Commit c360237

Browse files
codablockUdjinM6
authored andcommitted
Implement retroactive IS locking of transactions first seen in blocks instead of mempool (#2770)
* Don't rely on UTXO set in CheckCanLock The UTXO set only works for TXs in the mempool and won't work when we try to retroactively lock unlocked TXs from blocks. This is safe as ProcessTx is only called when a TX was accepted into the mempool or connected in a block, which means that all input checks were good. * Rename RetryLockMempoolTxs to RetryLockTxs and let it retry connected TXs * Instead of manually calling ProcessTx, let SyncTransaction handle all cases SyncTransaction is called from AcceptToMemoryPool and when transactions got connected in a block. So this is the time we want to run TXs through ProcessTx. This also enables retroactive signing of TXs that were unknown before a new block appeared. * Test retroactive signing and safe TXs in LLMQ ChainLocks tests * Also test for retroactive signing of chained TXs * Honor lockedParentTx when looking for TXs to retry signing * Stop scanning for TXs to retry after a depth of 6 * Generate 6 block to avoid retroactive signing overloading Travis * Avoid retroactive signing * Don't rely on NewPoWValidBlock and use SyncTransaction to build blockTxs NewPoWValidBlock is not guaranteed to be called when blocks come in fast. When a block is accepted in AcceptBlock, NewPoWValidBlock is only called when the new block is a successor of the currently active tip. This is not the case when after the first block a second block is accepted immediately as the first block is not connected yet. This might be a bug actually in the handling of NewPoWValidBlock, so we might need to check/fix this later, but currently I prefer to not touch that part. Instead, we now use SyncTransaction to gather TXs for blockTxs. This works because SyncTransaction is called for all transactions in a freshly connected block in one go. The call also happens before UpdatedBlockTip is called, so it's fine with the existing logic. * Use tx.IsCoinBase() instead of checking index 0 Also check for empty vin.
1 parent 9df6acd commit c360237

13 files changed

+174
-64
lines changed

qa/rpc-tests/autois-mempool.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ def run_test(self):
5656
self.log.info("Test old InstantSend")
5757
self.test_auto();
5858

59+
# Generate 6 block to avoid retroactive signing overloading Travis
60+
self.nodes[0].generate(6)
61+
sync_blocks(self.nodes)
62+
5963
self.nodes[0].spork("SPORK_20_INSTANTSEND_LLMQ_BASED", 0)
6064
self.wait_for_sporks_same()
6165

@@ -100,9 +104,17 @@ def test_auto(self, new_is = False):
100104
assert(not self.send_regular_instantsend(sender, receiver, False) if new_is else self.send_regular_instantsend(sender, receiver))
101105

102106
# generate one block to clean up mempool and retry auto and regular IS
103-
# generate 2 more blocks to have enough confirmations for IS
104-
self.nodes[0].generate(3)
107+
# generate 5 more blocks to avoid retroactive signing (which would overload Travis)
108+
set_mocktime(get_mocktime() + 1)
109+
set_node_times(self.nodes, get_mocktime())
110+
self.nodes[0].spork("SPORK_2_INSTANTSEND_ENABLED", 4070908800)
111+
self.wait_for_sporks_same()
112+
self.nodes[0].generate(6)
105113
self.sync_all()
114+
set_mocktime(get_mocktime() + 1)
115+
set_node_times(self.nodes, get_mocktime())
116+
self.nodes[0].spork("SPORK_2_INSTANTSEND_ENABLED", 0)
117+
self.wait_for_sporks_same()
106118
assert(self.send_simple_tx(sender, receiver))
107119
assert(self.send_regular_instantsend(sender, receiver, not new_is))
108120

qa/rpc-tests/llmq-chainlocks.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,38 @@ def run_test(self):
8888
self.wait_for_chainlock(self.nodes[0], self.nodes[1].getbestblockhash())
8989
assert(self.nodes[0].getbestblockhash() == self.nodes[1].getbestblockhash())
9090

91+
# Enable LLMQ bases InstantSend, which also enables checks for "safe" transactions
92+
self.nodes[0].spork("SPORK_20_INSTANTSEND_LLMQ_BASED", 0)
93+
self.wait_for_sporks_same()
94+
95+
# Isolate a node and let it create some transactions which won't get IS locked
96+
self.nodes[0].setnetworkactive(False)
97+
txs = []
98+
for i in range(3):
99+
txs.append(self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1))
100+
txs += self.create_chained_txs(self.nodes[0], 1)
101+
# Assert that after block generation these TXs are NOT included (as they are "unsafe")
102+
self.nodes[0].generate(1)
103+
for txid in txs:
104+
tx = self.nodes[0].getrawtransaction(txid, 1)
105+
assert("confirmations" not in tx)
106+
sleep(1)
107+
assert(not self.nodes[0].getblock(self.nodes[0].getbestblockhash())["chainlock"])
108+
# Disable LLMQ based InstantSend for a very short time (this never gets propagated to other nodes)
109+
self.nodes[0].spork("SPORK_20_INSTANTSEND_LLMQ_BASED", 4070908800)
110+
# Now the TXs should be included
111+
self.nodes[0].generate(1)
112+
self.nodes[0].spork("SPORK_20_INSTANTSEND_LLMQ_BASED", 0)
113+
# Assert that TXs got included now
114+
for txid in txs:
115+
tx = self.nodes[0].getrawtransaction(txid, 1)
116+
assert("confirmations" in tx and tx["confirmations"] > 0)
117+
# Enable network on first node again, which will cause the blocks to propagate and IS locks to happen retroactively
118+
# for the mined TXs, which will then allow the network to create a CLSIG
119+
self.nodes[0].setnetworkactive(True)
120+
connect_nodes(self.nodes[0], 1)
121+
self.wait_for_chainlock(self.nodes[0], self.nodes[1].getbestblockhash())
122+
91123
def wait_for_chainlock_tip_all_nodes(self):
92124
for node in self.nodes:
93125
tip = node.getbestblockhash()
@@ -110,6 +142,24 @@ def wait_for_chainlock(self, node, block_hash):
110142
sleep(0.1)
111143
raise AssertionError("wait_for_chainlock timed out")
112144

145+
def create_chained_txs(self, node, amount):
146+
txid = node.sendtoaddress(node.getnewaddress(), amount)
147+
tx = node.getrawtransaction(txid, 1)
148+
inputs = []
149+
valueIn = 0
150+
for txout in tx["vout"]:
151+
inputs.append({"txid": txid, "vout": txout["n"]})
152+
valueIn += txout["value"]
153+
outputs = {
154+
node.getnewaddress(): round(float(valueIn) - 0.0001, 6)
155+
}
156+
157+
rawtx = node.createrawtransaction(inputs, outputs)
158+
rawtx = node.signrawtransaction(rawtx)
159+
rawtxid = node.sendrawtransaction(rawtx["hex"])
160+
161+
return [txid, rawtxid]
162+
113163

114164
if __name__ == '__main__':
115165
LLMQChainLocksTest().main()

qa/rpc-tests/p2p-autoinstantsend.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ def run_test(self):
3939
self.log.info("Test old InstantSend")
4040
self.test_auto();
4141

42+
# Generate 6 block to avoid retroactive signing overloading Travis
43+
self.nodes[0].generate(6)
44+
sync_blocks(self.nodes)
45+
4246
self.nodes[0].spork("SPORK_20_INSTANTSEND_LLMQ_BASED", 0)
4347
self.wait_for_sporks_same()
4448

qa/rpc-tests/p2p-instantsend.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ def run_test(self):
2828
self.log.info("Test old InstantSend")
2929
self.test_doublespend()
3030

31+
# Generate 6 block to avoid retroactive signing overloading Travis
32+
self.nodes[0].generate(6)
33+
sync_blocks(self.nodes)
34+
3135
self.nodes[0].spork("SPORK_20_INSTANTSEND_LLMQ_BASED", 0)
3236
self.wait_for_sporks_same()
3337

src/dsnotificationinterface.cpp

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,6 @@ void CDSNotificationInterface::UpdatedBlockTip(const CBlockIndex *pindexNew, con
7171
llmq::quorumDKGSessionManager->UpdatedBlockTip(pindexNew, pindexFork, fInitialDownload);
7272
}
7373

74-
void CDSNotificationInterface::NewPoWValidBlock(const CBlockIndex* pindex, const std::shared_ptr<const CBlock>& block)
75-
{
76-
llmq::chainLocksHandler->NewPoWValidBlock(pindex, block);
77-
}
78-
7974
void CDSNotificationInterface::SyncTransaction(const CTransaction &tx, const CBlockIndex *pindex, int posInBlock)
8075
{
8176
llmq::quorumInstantSendManager->SyncTransaction(tx, pindex, posInBlock);

src/dsnotificationinterface.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ class CDSNotificationInterface : public CValidationInterface
2121
void AcceptedBlockHeader(const CBlockIndex *pindexNew) override;
2222
void NotifyHeaderTip(const CBlockIndex *pindexNew, bool fInitialDownload) override;
2323
void UpdatedBlockTip(const CBlockIndex *pindexNew, const CBlockIndex *pindexFork, bool fInitialDownload) override;
24-
void NewPoWValidBlock(const CBlockIndex *pindex, const std::shared_ptr<const CBlock>& block) override;
2524
void SyncTransaction(const CTransaction &tx, const CBlockIndex *pindex, int posInBlock) override;
2625
void NotifyMasternodeListChanged(const CDeterministicMNList& newList) override;
2726
void NotifyChainLock(const CBlockIndex* pindex) override;

src/llmq/quorums_chainlocks.cpp

Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -316,41 +316,34 @@ void CChainLocksHandler::TrySignChainTip()
316316
quorumSigningManager->AsyncSignIfMember(Params().GetConsensus().llmqChainLocks, requestId, msgHash);
317317
}
318318

319-
void CChainLocksHandler::NewPoWValidBlock(const CBlockIndex* pindex, const std::shared_ptr<const CBlock>& block)
319+
void CChainLocksHandler::SyncTransaction(const CTransaction& tx, const CBlockIndex* pindex, int posInBlock)
320320
{
321-
LOCK(cs);
322-
if (blockTxs.count(pindex->GetBlockHash())) {
323-
// should actually not happen (blocks are only written once to disk and this is when NewPoWValidBlock is called)
324-
// but be extra safe here in case this behaviour changes.
325-
return;
321+
bool handleTx = true;
322+
if (tx.IsCoinBase() || tx.vin.empty()) {
323+
handleTx = false;
326324
}
327325

328-
int64_t curTime = GetAdjustedTime();
326+
LOCK(cs);
329327

330-
// We listen for NewPoWValidBlock so that we can collect all TX ids of all included TXs of newly received blocks
328+
if (handleTx) {
329+
int64_t curTime = GetAdjustedTime();
330+
txFirstSeenTime.emplace(tx.GetHash(), curTime);
331+
}
332+
333+
// We listen for SyncTransaction so that we can collect all TX ids of all included TXs of newly received blocks
331334
// We need this information later when we try to sign a new tip, so that we can determine if all included TXs are
332335
// safe.
333-
334-
auto txs = std::make_shared<std::unordered_set<uint256, StaticSaltedHasher>>();
335-
for (const auto& tx : block->vtx) {
336-
if (tx->IsCoinBase() || tx->vin.empty()) {
337-
continue;
336+
if (pindex && posInBlock != CMainSignals::SYNC_TRANSACTION_NOT_IN_BLOCK) {
337+
auto it = blockTxs.find(pindex->GetBlockHash());
338+
if (it == blockTxs.end()) {
339+
// we want this to be run even if handleTx == false, so that the coinbase TX triggers creation of an empty entry
340+
it = blockTxs.emplace(pindex->GetBlockHash(), std::make_shared<std::unordered_set<uint256, StaticSaltedHasher>>()).first;
341+
}
342+
if (handleTx) {
343+
auto& txs = *it->second;
344+
txs.emplace(tx.GetHash());
338345
}
339-
txs->emplace(tx->GetHash());
340-
txFirstSeenTime.emplace(tx->GetHash(), curTime);
341-
}
342-
blockTxs[pindex->GetBlockHash()] = txs;
343-
}
344-
345-
void CChainLocksHandler::SyncTransaction(const CTransaction& tx, const CBlockIndex* pindex, int posInBlock)
346-
{
347-
if (tx.IsCoinBase() || tx.vin.empty()) {
348-
return;
349346
}
350-
351-
LOCK(cs);
352-
int64_t curTime = GetAdjustedTime();
353-
txFirstSeenTime.emplace(tx.GetHash(), curTime);
354347
}
355348

356349
bool CChainLocksHandler::IsTxSafeForMining(const uint256& txid)

src/llmq/quorums_chainlocks.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,6 @@ class CChainLocksHandler : public CRecoveredSigsListener
8787
void ProcessNewChainLock(NodeId from, const CChainLockSig& clsig, const uint256& hash);
8888
void AcceptedBlockHeader(const CBlockIndex* pindexNew);
8989
void UpdatedBlockTip(const CBlockIndex* pindexNew, const CBlockIndex* pindexFork);
90-
void NewPoWValidBlock(const CBlockIndex* pindex, const std::shared_ptr<const CBlock>& block);
9190
void SyncTransaction(const CTransaction &tx, const CBlockIndex *pindex, int posInBlock);
9291
void TrySignChainTip();
9392
void EnforceBestChainLock();

src/llmq/quorums_instantsend.cpp

Lines changed: 81 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -294,20 +294,23 @@ bool CInstantSendManager::CheckCanLock(const COutPoint& outpoint, bool printDebu
294294
return false;
295295
}
296296

297-
Coin coin;
298-
const CBlockIndex* pindexMined = nullptr;
297+
CTransactionRef tx;
298+
uint256 hashBlock;
299+
// this relies on enabled txindex and won't work if we ever try to remove the requirement for txindex for masternodes
300+
if (!GetTransaction(outpoint.hash, tx, params, hashBlock, false)) {
301+
if (printDebug) {
302+
LogPrint("instantsend", "CInstantSendManager::%s -- txid=%s: failed to find parent TX %s\n", __func__,
303+
txHash.ToString(), outpoint.hash.ToString());
304+
}
305+
return false;
306+
}
307+
308+
const CBlockIndex* pindexMined;
299309
int nTxAge;
300310
{
301311
LOCK(cs_main);
302-
if (!pcoinsTip->GetCoin(outpoint, coin) || coin.IsSpent()) {
303-
if (printDebug) {
304-
LogPrint("instantsend", "CInstantSendManager::%s -- txid=%s: failed to find UTXO %s\n", __func__,
305-
txHash.ToString(), outpoint.ToStringShort());
306-
}
307-
return false;
308-
}
309-
pindexMined = chainActive[coin.nHeight];
310-
nTxAge = chainActive.Height() - coin.nHeight + 1;
312+
pindexMined = mapBlockIndex.at(hashBlock);
313+
nTxAge = chainActive.Height() - pindexMined->nHeight + 1;
311314
}
312315

313316
if (nTxAge < nInstantSendConfirmationsRequired) {
@@ -321,7 +324,7 @@ bool CInstantSendManager::CheckCanLock(const COutPoint& outpoint, bool printDebu
321324
}
322325

323326
if (retValue) {
324-
*retValue = coin.out.nValue;
327+
*retValue = tx->vout[outpoint.n].nValue;
325328
}
326329

327330
return true;
@@ -673,7 +676,7 @@ void CInstantSendManager::ProcessInstantSendLock(NodeId from, const uint256& has
673676
g_connman->RelayInv(inv);
674677

675678
RemoveMempoolConflictsForLock(hash, islock);
676-
RetryLockMempoolTxs(islock.txid);
679+
RetryLockTxs(islock.txid);
677680

678681
UpdateWalletTransaction(islock.txid, tx);
679682
}
@@ -708,8 +711,17 @@ void CInstantSendManager::SyncTransaction(const CTransaction& tx, const CBlockIn
708711
return;
709712
}
710713

711-
if (IsLocked(tx.GetHash())) {
712-
RetryLockMempoolTxs(tx.GetHash());
714+
if (tx.IsCoinBase() || tx.vin.empty()) {
715+
// coinbase can't and TXs with no inputs be locked
716+
return;
717+
}
718+
719+
bool locked = IsLocked(tx.GetHash());
720+
bool chainlocked = pindex && chainLocksHandler->HasChainLock(pindex->nHeight, pindex->GetBlockHash());
721+
if (locked || chainlocked) {
722+
RetryLockTxs(tx.GetHash());
723+
} else {
724+
ProcessTx(tx, Params().GetConsensus());
713725
}
714726
}
715727

@@ -756,7 +768,7 @@ void CInstantSendManager::NotifyChainLock(const CBlockIndex* pindexChainLock)
756768
db.WriteLastChainLockBlock(pindexChainLock->GetBlockHash());
757769
}
758770

759-
RetryLockMempoolTxs(uint256());
771+
RetryLockTxs(uint256());
760772
}
761773

762774
void CInstantSendManager::RemoveFinalISLock(const uint256& hash, const CInstantSendLockPtr& islock)
@@ -795,9 +807,9 @@ void CInstantSendManager::RemoveMempoolConflictsForLock(const uint256& hash, con
795807
}
796808
}
797809

798-
void CInstantSendManager::RetryLockMempoolTxs(const uint256& lockedParentTx)
810+
void CInstantSendManager::RetryLockTxs(const uint256& lockedParentTx)
799811
{
800-
// Let's retry all mempool TXs which don't have an islock yet and where the parents got ChainLocked now
812+
// Let's retry all unlocked TXs from mempool and and recently connected blocks
801813

802814
std::unordered_map<uint256, CTransactionRef> txs;
803815

@@ -817,6 +829,57 @@ void CInstantSendManager::RetryLockMempoolTxs(const uint256& lockedParentTx)
817829
}
818830
}
819831
}
832+
833+
uint256 lastChainLockBlock;
834+
const CBlockIndex* pindexLastChainLockBlock = nullptr;
835+
const CBlockIndex* pindexWalk = nullptr;
836+
{
837+
LOCK(cs);
838+
lastChainLockBlock = db.GetLastChainLockBlock();
839+
}
840+
{
841+
LOCK(cs_main);
842+
if (!lastChainLockBlock.IsNull()) {
843+
pindexLastChainLockBlock = mapBlockIndex.at(lastChainLockBlock);
844+
pindexWalk = chainActive.Tip();
845+
}
846+
}
847+
848+
// scan blocks until we hit the last chainlocked block we know of. Also stop scanning after a depth of 6 to avoid
849+
// signing thousands of TXs at once. Also, after a depth of 6, blocks get eligible for ChainLocking even if unsafe
850+
// TXs are included, so there is no need to retroactively sign these.
851+
int depth = 0;
852+
while (pindexWalk && pindexWalk != pindexLastChainLockBlock && depth < 6) {
853+
CBlock block;
854+
{
855+
LOCK(cs_main);
856+
if (!ReadBlockFromDisk(block, pindexWalk, Params().GetConsensus())) {
857+
pindexWalk = pindexWalk->pprev;
858+
continue;
859+
}
860+
}
861+
862+
for (const auto& tx : block.vtx) {
863+
if (lockedParentTx.IsNull()) {
864+
txs.emplace(tx->GetHash(), tx);
865+
} else {
866+
bool isChild = false;
867+
for (auto& in : tx->vin) {
868+
if (in.prevout.hash == lockedParentTx) {
869+
isChild = true;
870+
break;
871+
}
872+
}
873+
if (isChild) {
874+
txs.emplace(tx->GetHash(), tx);
875+
}
876+
}
877+
}
878+
879+
pindexWalk = pindexWalk->pprev;
880+
depth++;
881+
}
882+
820883
for (auto& p : txs) {
821884
auto& tx = p.second;
822885
{

src/llmq/quorums_instantsend.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ class CInstantSendManager : public CRecoveredSigsListener
124124
void RemoveFinalISLock(const uint256& hash, const CInstantSendLockPtr& islock);
125125

126126
void RemoveMempoolConflictsForLock(const uint256& hash, const CInstantSendLock& islock);
127-
void RetryLockMempoolTxs(const uint256& lockedParentTx);
127+
void RetryLockTxs(const uint256& lockedParentTx);
128128

129129
bool AlreadyHave(const CInv& inv);
130130
bool GetInstantSendLockByHash(const uint256& hash, CInstantSendLock& ret);

0 commit comments

Comments
 (0)