From 062c31b72e1fab5ed2516ef7c5700ed9340dfe11 Mon Sep 17 00:00:00 2001 From: Chris Stewart Date: Sun, 6 Apr 2025 19:23:39 -0500 Subject: [PATCH 1/3] tests: Add unix timestamp tests for OP_CLTV --- test/functional/feature_cltv.py | 73 ++++++++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 5 deletions(-) diff --git a/test/functional/feature_cltv.py b/test/functional/feature_cltv.py index 60b3fb4e20b..36da7e82db0 100755 --- a/test/functional/feature_cltv.py +++ b/test/functional/feature_cltv.py @@ -7,6 +7,7 @@ Test that the CHECKLOCKTIMEVERIFY soft-fork activates. """ +import time from test_framework.blocktools import ( TIME_GENESIS_BLOCK, create_block, @@ -26,7 +27,7 @@ from test_framework.script import ( OP_DROP, ) from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import assert_equal +from test_framework.util import assert_equal, assert_not_equal from test_framework.wallet import ( MiniWallet, MiniWalletMode, @@ -72,12 +73,12 @@ def cltv_invalidate(tx, failure_reason): def cltv_validate(tx, height): # Modify the signature in vin 0 and nSequence/nLockTime of the tx to pass CLTV - scheme = [[CScriptNum(height), OP_CHECKLOCKTIMEVERIFY, OP_DROP], 0, height] + scheme = [[CScriptNum.encode(CScriptNum(height))[1:], OP_CHECKLOCKTIMEVERIFY, OP_DROP], 0, height] cltv_modify_tx(tx, prepend_scriptsig=scheme[0], nsequence=scheme[1], nlocktime=scheme[2]) -CLTV_HEIGHT = 111 +CLTV_HEIGHT = 112 class BIP65Test(BitcoinTestFramework): @@ -107,8 +108,9 @@ class BIP65Test(BitcoinTestFramework): self.test_cltv_info(is_active=False) self.log.info("Mining %d blocks", CLTV_HEIGHT - 2) - self.generate(wallet, 10) - self.generate(self.nodes[0], CLTV_HEIGHT - 2 - 10) + num_utxos = 13 + self.generate(wallet, num_utxos) + self.generate(self.nodes[0], CLTV_HEIGHT - 2 - num_utxos) assert_equal(self.nodes[0].getblockcount(), CLTV_HEIGHT - 2) self.log.info("Test that invalid-according-to-CLTV transactions can still appear in a block") @@ -199,6 +201,67 @@ class BIP65Test(BitcoinTestFramework): self.test_cltv_info(is_active=True) # Active as of current tip assert_equal(int(self.nodes[0].getbestblockhash(), 16), block.sha256) + # test wall clock time + tip = block.sha256 + + current_time = int(time.time()) + locktime = current_time + (60 * 60) # 1 hour + + self.log.info("Test that one block mined with correct wall clock time does not satisfy median-time-past") + cltv_validate(spendtx,locktime) + block2 = create_block(tip, create_coinbase(CLTV_HEIGHT + 1), ntime=locktime, version=4) + block2.vtx.append(spendtx) + block2.hashMerkleRoot = block2.calc_merkle_root() + block2.solve() + peer.send_and_ping(msg_block(block2)) + # expect failure as we haven't satisfied wall clock time yet + assert_not_equal(int(self.nodes[0].getbestblockhash(), 16), block2.sha256) + + self.log.info("Test 7 blocks mined with current wall clock time can satisfy the median-time-past") + num = 7 + for i in range(num): + b = create_block(tip, create_coinbase(CLTV_HEIGHT + i + 1), ntime=locktime + i, version=4) + b.solve() + peer.send_and_ping(msg_block(b)) + tip = b.sha256 + + # now our median-past-time should allow for a transaction with + # a locktime of +1 hour to be confirmed + spendtx = wallet.create_self_transfer()['tx'] + cltv_validate(spendtx,locktime) + block2.vtx.append(spendtx) + block2 = create_block(tip, create_coinbase(CLTV_HEIGHT + num + 1), ntime=locktime + num, version=4) + block2.vtx.append(spendtx) + block2.vtx.append(wallet.create_self_transfer()['tx']) + block2.hashMerkleRoot = block2.calc_merkle_root() + block2.solve() + peer.send_and_ping(msg_block(block2)) + assert_equal(int(self.nodes[0].getbestblockhash(), 16), block2.sha256) + + self.log.info("Test that we cannot mine locktimes that are UINT32_MAX") + UINT32_MAX = 2**32 - 1 + self.nodes[0].setmocktime(UINT32_MAX) + # need to re-add connection as setting mocktime times out the p2p connection + peer = self.nodes[0].add_p2p_connection(P2PInterface()) + tip = block2.sha256 + # 6 blocks are allowed to have UINT32_MAX block time + for i in range(6): + b = create_block(tip, create_coinbase(CLTV_HEIGHT + num + i + 2), ntime=UINT32_MAX, version=4) + b.solve() + peer.send_and_ping(msg_block(b)) + tip = b.sha256 + assert_equal(int(self.nodes[0].getbestblockhash(), 16), tip) + + spendtx = wallet.create_self_transfer()['tx'] + # the 7th block means this block is invalid according to BIP113 (median-time-past) + block3 = create_block(tip, create_coinbase(CLTV_HEIGHT + num + 6 + 2), ntime=UINT32_MAX, version=4) + # cannot satisfy this locktime, need to be strictly greater than the median-past-time + cltv_validate(spendtx,UINT32_MAX) + block3.vtx.append(spendtx) + block3.hashMerkleRoot = block3.calc_merkle_root() + block3.solve() + peer.send_and_ping(msg_block(block3)) + self.nodes[0].assert_debug_log("time-too-old, block's timestamp is too early") if __name__ == '__main__': BIP65Test(__file__).main() From 01d2068fb66634bbb0851f042dc18d7fc89649dd Mon Sep 17 00:00:00 2001 From: Chris Stewart Date: Tue, 8 Apr 2025 11:48:00 -0500 Subject: [PATCH 2/3] Add assertion to check that the max locktime satisfiable under BIP113 is UINT32_MAX - 2 --- test/functional/feature_cltv.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/functional/feature_cltv.py b/test/functional/feature_cltv.py index 36da7e82db0..6aefb9e0c4b 100755 --- a/test/functional/feature_cltv.py +++ b/test/functional/feature_cltv.py @@ -108,7 +108,7 @@ class BIP65Test(BitcoinTestFramework): self.test_cltv_info(is_active=False) self.log.info("Mining %d blocks", CLTV_HEIGHT - 2) - num_utxos = 13 + num_utxos = 14 self.generate(wallet, num_utxos) self.generate(self.nodes[0], CLTV_HEIGHT - 2 - num_utxos) assert_equal(self.nodes[0].getblockcount(), CLTV_HEIGHT - 2) @@ -246,7 +246,7 @@ class BIP65Test(BitcoinTestFramework): tip = block2.sha256 # 6 blocks are allowed to have UINT32_MAX block time for i in range(6): - b = create_block(tip, create_coinbase(CLTV_HEIGHT + num + i + 2), ntime=UINT32_MAX, version=4) + b = create_block(tip, create_coinbase(CLTV_HEIGHT + num + i + 2), ntime=UINT32_MAX - 1, version=4) b.solve() peer.send_and_ping(msg_block(b)) tip = b.sha256 @@ -256,12 +256,12 @@ class BIP65Test(BitcoinTestFramework): # the 7th block means this block is invalid according to BIP113 (median-time-past) block3 = create_block(tip, create_coinbase(CLTV_HEIGHT + num + 6 + 2), ntime=UINT32_MAX, version=4) # cannot satisfy this locktime, need to be strictly greater than the median-past-time - cltv_validate(spendtx,UINT32_MAX) + cltv_validate(spendtx,UINT32_MAX - 2) block3.vtx.append(spendtx) block3.hashMerkleRoot = block3.calc_merkle_root() block3.solve() peer.send_and_ping(msg_block(block3)) - self.nodes[0].assert_debug_log("time-too-old, block's timestamp is too early") + assert_equal(int(self.nodes[0].getbestblockhash(), 16), block3.sha256) if __name__ == '__main__': BIP65Test(__file__).main() From 024c3bf3c7be6b3b262e674ac77ecbd68395b34c Mon Sep 17 00:00:00 2001 From: Chris Stewart Date: Thu, 10 Apr 2025 11:57:23 -0500 Subject: [PATCH 3/3] Address codreview from mabu44 --- test/functional/feature_cltv.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/test/functional/feature_cltv.py b/test/functional/feature_cltv.py index 6aefb9e0c4b..d06d155fd1d 100755 --- a/test/functional/feature_cltv.py +++ b/test/functional/feature_cltv.py @@ -108,7 +108,7 @@ class BIP65Test(BitcoinTestFramework): self.test_cltv_info(is_active=False) self.log.info("Mining %d blocks", CLTV_HEIGHT - 2) - num_utxos = 14 + num_utxos = 12 self.generate(wallet, num_utxos) self.generate(self.nodes[0], CLTV_HEIGHT - 2 - num_utxos) assert_equal(self.nodes[0].getblockcount(), CLTV_HEIGHT - 2) @@ -206,7 +206,7 @@ class BIP65Test(BitcoinTestFramework): current_time = int(time.time()) locktime = current_time + (60 * 60) # 1 hour - + spendtx = wallet.create_self_transfer()['tx'] self.log.info("Test that one block mined with correct wall clock time does not satisfy median-time-past") cltv_validate(spendtx,locktime) block2 = create_block(tip, create_coinbase(CLTV_HEIGHT + 1), ntime=locktime, version=4) @@ -227,12 +227,9 @@ class BIP65Test(BitcoinTestFramework): # now our median-past-time should allow for a transaction with # a locktime of +1 hour to be confirmed - spendtx = wallet.create_self_transfer()['tx'] cltv_validate(spendtx,locktime) - block2.vtx.append(spendtx) block2 = create_block(tip, create_coinbase(CLTV_HEIGHT + num + 1), ntime=locktime + num, version=4) block2.vtx.append(spendtx) - block2.vtx.append(wallet.create_self_transfer()['tx']) block2.hashMerkleRoot = block2.calc_merkle_root() block2.solve() peer.send_and_ping(msg_block(block2)) @@ -244,7 +241,7 @@ class BIP65Test(BitcoinTestFramework): # need to re-add connection as setting mocktime times out the p2p connection peer = self.nodes[0].add_p2p_connection(P2PInterface()) tip = block2.sha256 - # 6 blocks are allowed to have UINT32_MAX block time + # mine 6 blocks with UINT32_MAX -1 ntime for i in range(6): b = create_block(tip, create_coinbase(CLTV_HEIGHT + num + i + 2), ntime=UINT32_MAX - 1, version=4) b.solve() @@ -253,9 +250,10 @@ class BIP65Test(BitcoinTestFramework): assert_equal(int(self.nodes[0].getbestblockhash(), 16), tip) spendtx = wallet.create_self_transfer()['tx'] - # the 7th block means this block is invalid according to BIP113 (median-time-past) + # create the next block with ntime=UINT32_MAX so we monotonically increase block time block3 = create_block(tip, create_coinbase(CLTV_HEIGHT + num + 6 + 2), ntime=UINT32_MAX, version=4) - # cannot satisfy this locktime, need to be strictly greater than the median-past-time + # the max locktime we can include in this block is UINT32_MAX - 2 due to the fact + # our current median-time-past is UINT32_MAX - 1 cltv_validate(spendtx,UINT32_MAX - 2) block3.vtx.append(spendtx) block3.hashMerkleRoot = block3.calc_merkle_root()