Skip to content

Commit 525368a

Browse files
UdjinM6claude
authored andcommitted
test: add functional test for coinbase chainlock recovery
Replace the useless unit test with a meaningful functional test that verifies nodes can learn about chainlocks from coinbase transactions when they miss the P2P broadcast. The new test: - Isolates a node before a chainlock is created - Submits blocks via RPC (not P2P) so the node gets blocks but not the chainlock message - Verifies the chainlock appears in the next block's coinbase - Uses mockscheduler to trigger async processing - Verifies the node learned the chainlock from the coinbase This tests the async chainlock queueing and processing mechanism implemented in the parent commit, ensuring nodes can recover chainlocks from block data during sync/reindex. Removed: coinbase_chainlock_queueing_test (unit test that only verified calls don't crash, provided no real validation) Added: test_coinbase_chainlock_recovery (functional test with actual validation of chainlock recovery behavior) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 39d1d31 commit 525368a

2 files changed

Lines changed: 71 additions & 37 deletions

File tree

src/test/llmq_chainlock_tests.cpp

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,7 @@
88
#include <streams.h>
99
#include <util/strencodings.h>
1010

11-
#include <chainlock/chainlock.h>
1211
#include <chainlock/clsig.h>
13-
#include <llmq/context.h>
14-
#include <validation.h>
1512

1613
#include <boost/test/unit_test.hpp>
1714

@@ -170,37 +167,4 @@ BOOST_AUTO_TEST_CASE(chainlock_malformed_data_test)
170167
}
171168
}
172169

173-
BOOST_AUTO_TEST_CASE(coinbase_chainlock_queueing_test)
174-
{
175-
// Test that coinbase chainlocks can be queued for processing
176-
// This test verifies the queueing mechanism works without requiring full block processing
177-
178-
TestingSetup test_setup(CBaseChainParams::REGTEST);
179-
180-
// Create a chainlock handler
181-
llmq::CChainLocksHandler handler(test_setup.m_node.chainman->ActiveChainstate(), *test_setup.m_node.llmq_ctx->qman,
182-
*test_setup.m_node.sporkman, *test_setup.m_node.mempool, *test_setup.m_node.mn_sync);
183-
184-
// Create a test chainlock
185-
int32_t height = 100;
186-
uint256 blockHash = GetTestBlockHash(100);
187-
ChainLockSig clsig = CreateChainLock(height, blockHash);
188-
189-
// Verify the chainlock is not null
190-
BOOST_CHECK(!clsig.IsNull());
191-
BOOST_CHECK_EQUAL(clsig.getHeight(), height);
192-
193-
// Queue the chainlock (this should not fail even if chainlocks are disabled)
194-
// The handler will check if chainlocks are enabled internally
195-
handler.QueueCoinbaseChainLock(clsig);
196-
197-
// Create a newer chainlock
198-
ChainLockSig clsig2 = CreateChainLock(height + 1, GetTestBlockHash(101));
199-
handler.QueueCoinbaseChainLock(clsig2);
200-
201-
// Queueing should succeed without errors
202-
// Note: Actual processing requires chainlocks to be enabled and the scheduler to run,
203-
// which is tested in functional tests (feature_llmq_chainlocks.py)
204-
}
205-
206170
BOOST_AUTO_TEST_SUITE_END()

test/functional/feature_llmq_chainlocks.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from test_framework.messages import CBlock, CCbTx
1616
from test_framework.test_framework import DashTestFramework
17-
from test_framework.util import assert_equal, assert_raises_rpc_error, force_finish_mnsync
17+
from test_framework.util import assert_equal, assert_greater_than, assert_raises_rpc_error, force_finish_mnsync
1818

1919
import time
2020

@@ -251,6 +251,9 @@ def test_cb(self):
251251
self.log.info("Test bestCLHeightDiff restrictions")
252252
self.test_bestCLHeightDiff()
253253

254+
self.log.info("Test coinbase chainlock recovery")
255+
self.test_coinbase_chainlock_recovery()
256+
254257
def create_chained_txs(self, node, amount):
255258
txid = node.sendtoaddress(node.getnewaddress(), amount)
256259
tx = node.getrawtransaction(txid, 1)
@@ -354,6 +357,73 @@ def test_bestCLHeightDiff(self):
354357
self.reconnect_isolated_node(1, 0)
355358
self.sync_all()
356359

360+
def test_coinbase_chainlock_recovery(self):
361+
"""
362+
Test that nodes can learn about chainlocks from coinbase transactions
363+
when they miss the P2P broadcast.
364+
365+
This verifies the async chainlock queueing and processing mechanism.
366+
"""
367+
self.log.info("Testing coinbase chainlock recovery from submitted blocks...")
368+
369+
# Isolate node4 before creating a chainlock
370+
self.isolate_node(4)
371+
372+
# Mine one block on nodes 0-3 and wait for it to be chainlocked
373+
cl_block_hash = self.generate(self.nodes[0], 1, sync_fun=lambda: self.sync_blocks(self.nodes[0:4]))[0]
374+
self.wait_for_chainlocked_block(self.nodes[0], cl_block_hash, timeout=15)
375+
cl_height = self.nodes[0].getblockcount()
376+
377+
# Mine another block - its coinbase should contain the chainlock
378+
new_block_hash = self.generate(self.nodes[0], 1, sync_fun=lambda: self.sync_blocks(self.nodes[0:4]))[0]
379+
380+
# Verify the new block's coinbase contains the chainlock for cl_block_hash
381+
cbtx = self.nodes[0].getblock(new_block_hash, 2)["cbTx"]
382+
# CbTx should include chainlock fields
383+
assert_greater_than(int(cbtx["version"]), 2)
384+
# CbTx should reference immediately previous block
385+
assert_equal(int(cbtx["bestCLHeightDiff"]), 0)
386+
387+
# Verify the chainlock in coinbase matches our saved block
388+
cb_cl_height = int(cbtx["height"]) - int(cbtx["bestCLHeightDiff"]) - 1
389+
assert_equal(cb_cl_height, cl_height)
390+
cb_cl_block_hash = self.nodes[0].getblockhash(cb_cl_height)
391+
assert_equal(cb_cl_block_hash, cl_block_hash)
392+
393+
# Now submit both blocks to isolated node4 via submitblock (NOT via P2P)
394+
# This way node4 gets the blocks but NOT the chainlock P2P message
395+
cl_block_hex = self.nodes[0].getblock(cl_block_hash, 0)
396+
self.nodes[4].submitblock(cl_block_hex)
397+
398+
new_block_hex = self.nodes[0].getblock(new_block_hash, 0)
399+
result = self.nodes[4].submitblock(new_block_hex)
400+
assert_equal(result, None)
401+
assert_equal(self.nodes[4].getbestblockhash(), new_block_hash)
402+
403+
# Verify node4 has the blocks but NOT the chainlock (missed P2P message)
404+
node4_block = self.nodes[4].getblock(cl_block_hash)
405+
assert not node4_block["chainlock"], "Node4 should not have chainlock yet (no P2P)"
406+
407+
# At this point:
408+
# - Node4 has both blocks
409+
# - Node4 has NOT received chainlock via P2P
410+
# - Node4 HAS seen the chainlock in the coinbase of new_block_hash
411+
# - The chainlock should be queued for async processing
412+
413+
# Trigger scheduler to process pending coinbase chainlocks
414+
# The scheduler runs every 5 seconds, so advancing by 6 seconds ensures it runs
415+
self.log.info("Triggering async chainlock processing from coinbase...")
416+
self.nodes[4].mockscheduler(6)
417+
418+
# Verify node4 learned about the chainlock from the coinbase
419+
self.wait_for_chainlocked_block(self.nodes[4], cl_block_hash, timeout=5)
420+
421+
self.log.info("Node successfully recovered chainlock from coinbase (not P2P)")
422+
423+
# Reconnect and verify everything is consistent
424+
self.reconnect_isolated_node(4, 0)
425+
self.sync_blocks()
426+
357427

358428
if __name__ == '__main__':
359429
LLMQChainLocksTest().main()

0 commit comments

Comments
 (0)