-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat: implement asynchronous processing for coinbase chainlocks #6947
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,7 +14,7 @@ | |
|
|
||
| from test_framework.messages import CBlock, CCbTx | ||
| from test_framework.test_framework import DashTestFramework | ||
| from test_framework.util import assert_equal, assert_raises_rpc_error, force_finish_mnsync | ||
| from test_framework.util import assert_equal, assert_greater_than, assert_raises_rpc_error, force_finish_mnsync | ||
|
|
||
| import time | ||
|
|
||
|
|
@@ -254,6 +254,9 @@ def test_cb(self): | |
| self.log.info("Test bestCLHeightDiff restrictions") | ||
| self.test_bestCLHeightDiff() | ||
|
|
||
| self.log.info("Test coinbase chainlock recovery") | ||
| self.test_coinbase_chainlock_recovery() | ||
|
|
||
| def create_chained_txs(self, node, amount): | ||
| txid = node.sendtoaddress(node.getnewaddress(), amount) | ||
| tx = node.getrawtransaction(txid, 1) | ||
|
|
@@ -357,6 +360,73 @@ def test_bestCLHeightDiff(self): | |
| self.reconnect_isolated_node(1, 0) | ||
| self.sync_all() | ||
|
|
||
| def test_coinbase_chainlock_recovery(self): | ||
| """ | ||
| Test that nodes can learn about chainlocks from coinbase transactions | ||
| when they miss the P2P broadcast. | ||
|
|
||
| This verifies the async chainlock queueing and processing mechanism. | ||
| """ | ||
| self.log.info("Testing coinbase chainlock recovery from submitted blocks...") | ||
|
|
||
| # Isolate node4 before creating a chainlock | ||
| self.isolate_node(4) | ||
|
|
||
| # Mine one block on nodes 0-3 and wait for it to be chainlocked | ||
| cl_block_hash = self.generate(self.nodes[0], 1, sync_fun=lambda: self.sync_blocks(self.nodes[0:4]))[0] | ||
| self.wait_for_chainlocked_block(self.nodes[0], cl_block_hash, timeout=15) | ||
| cl_height = self.nodes[0].getblockcount() | ||
|
|
||
| # Mine another block - its coinbase should contain the chainlock | ||
| new_block_hash = self.generate(self.nodes[0], 1, sync_fun=lambda: self.sync_blocks(self.nodes[0:4]))[0] | ||
|
|
||
| # Verify the new block's coinbase contains the chainlock for cl_block_hash | ||
| cbtx = self.nodes[0].getblock(new_block_hash, 2)["cbTx"] | ||
| # CbTx should include chainlock fields | ||
| assert_greater_than(int(cbtx["version"]), 2) | ||
| # CbTx should reference immediately previous block | ||
| assert_equal(int(cbtx["bestCLHeightDiff"]), 0) | ||
|
|
||
| # Verify the chainlock in coinbase matches our saved block | ||
| cb_cl_height = int(cbtx["height"]) - int(cbtx["bestCLHeightDiff"]) - 1 | ||
| assert_equal(cb_cl_height, cl_height) | ||
| cb_cl_block_hash = self.nodes[0].getblockhash(cb_cl_height) | ||
| assert_equal(cb_cl_block_hash, cl_block_hash) | ||
|
|
||
| # Now submit both blocks to isolated node4 via submitblock (NOT via P2P) | ||
| # This way node4 gets the blocks but NOT the chainlock P2P message | ||
| cl_block_hex = self.nodes[0].getblock(cl_block_hash, 0) | ||
| self.nodes[4].submitblock(cl_block_hex) | ||
|
|
||
| new_block_hex = self.nodes[0].getblock(new_block_hash, 0) | ||
| result = self.nodes[4].submitblock(new_block_hex) | ||
| assert_equal(result, None) | ||
| assert_equal(self.nodes[4].getbestblockhash(), new_block_hash) | ||
|
|
||
| # Verify node4 has the blocks but NOT the chainlock (missed P2P message) | ||
| node4_block = self.nodes[4].getblock(cl_block_hash) | ||
| assert not node4_block["chainlock"], "Node4 should not have chainlock yet (no P2P)" | ||
|
|
||
| # At this point: | ||
| # - Node4 has both blocks | ||
| # - Node4 has NOT received chainlock via P2P | ||
| # - Node4 HAS seen the chainlock in the coinbase of new_block_hash | ||
| # - The chainlock should be queued for async processing | ||
|
|
||
| # Trigger scheduler to process pending coinbase chainlocks | ||
| # The scheduler runs every 5 seconds, so advancing by 6 seconds ensures it runs | ||
| self.log.info("Triggering async chainlock processing from coinbase...") | ||
| self.nodes[4].mockscheduler(6) | ||
|
|
||
| # Verify node4 learned about the chainlock from the coinbase | ||
| self.wait_for_chainlocked_block(self.nodes[4], cl_block_hash, timeout=5) | ||
|
Comment on lines
+416
to
+422
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: mockscheduler does not advance ChainlockHandler's private scheduler This test says mockscheduler(6) triggers async coinbase ChainLock processing, but the RPC advances NodeContext::scheduler only. ChainlockHandler owns a separate private CScheduler and thread, so ProcessPendingCoinbaseChainLocks() only runs when that real 5-second scheduler ticks. The test can therefore pass because the real thread happened to run within the wait, fail if it does not, or even fail the earlier 'should not have chainlock yet' assertion if the private scheduler runs before that check; it needs either a test hook/RPC for the ChainlockHandler scheduler or expectations that explicitly account for the real private scheduler interval. source: ['claude-general', 'codex-general'] 🤖 Fix this with AI agents |
||
|
|
||
| self.log.info("Node successfully recovered chainlock from coinbase (not P2P)") | ||
|
|
||
| # Reconnect and verify everything is consistent | ||
| self.reconnect_isolated_node(4, 0) | ||
| self.sync_blocks() | ||
|
|
||
|
|
||
| if __name__ == '__main__': | ||
| LLMQChainLocksTest().main() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same-height chainlock rejection may skip valid fork chainlocks.
The condition
clsig.getHeight() <= bestChainLock.getHeight()(line 184) rejects chainlocks at the same height as the current best. During a chain fork, competing blocks at the same height can both have valid chainlocks, and the coinbase chainlock for the correct fork should still be queued and processed. Using<=instead of<may cause the node to skip recovery of a valid chainlock when the incoming coinbase chainlock is at the same height but for a different block hash thanbestChainLock.Consider changing the condition to
<or adding a block-hash inequality check.Proposed fix
Alternatively, if same-height entries must be filtered, check the block hash as well:
📝 Committable suggestion
🤖 Prompt for AI Agents