Skip to content

Commit 4b5659c

Browse files
author
MarcoFalke
committed
Merge bitcoin#19801: test: check for all possible OP_CLTV fail reasons in feature_cltv.py (BIP 65)
b01cd94 test: check that _all_ invalid-CLTV txs are rejected after BIP65 activation (Sebastian Falbesoner) dbc1981 test: check that _all_ invalid-CLTV txs are allowed in a block pre-BIP65 (Sebastian Falbesoner) 8d0ce50 test: prepare cltv_invalidate to test all failure reasons in feature_cltv.py (Sebastian Falbesoner) ce994e1 test: add tx modfication helper function in feature_cltv.py (Sebastian Falbesoner) Pull request description: The functional test for [BIP65](https://github.com/bitcoin/bips/blob/master/bip-0065.mediawiki) / `OP_CHECKLOCKTIMEVERIFY` (`feature_cltv.py`) currently only tests one out of five conditions that lead to failure of the op-code -- by prepending the script `OP_1NEGATE OP_CHECKLOCKTIMEVERIFY OP_DROP` to a tx's first input's scriptSig, the case of "_the top item on the stack is less than 0_" is checked: https://github.com/bitcoin/bitcoin/blob/f8462a6d2794be728cf8550f45d19a354aae59cf/test/functional/feature_cltv.py#L26-L35 This PR adds the other cases (5 in total) by taking an integer argument to the function `cltv_invalidate` that is called in a loop instead of only once per testing scenario. Here is the full list of failure conditions and how they are tested (note that the scriptSig should still be valid before activation of BIP65, when `OP_CLTV` is simply a no-op): * _the stack is empty_ ➡️ prepending `OP_CHECKLOCKTIMEVERIFY` to scriptSig * _the top item on the stack is less than 0_ ➡️ prepending `OP_1NEGATE OP_CHECKLOCKTIMEVERIFY OP_DROP` to scriptSig * _the lock-time type (height vs. timestamp) of the top stack item and the nLockTime field are not the same_ ➡️ prepending `OPNum(1000) OP_CHECKLOCKTIMEVERIFY OP_DROP` to scriptSig ➡️ setting tx.vin[0].nSequence=0 and tx.nCheckTimeLock=1296688602 (genesis block timestamp) * _the top stack item is greater than the transaction's nLockTime field_ ➡️ prepending `OPNum(1000) OP_CHECKLOCKTIMEVERIFY OP_DROP` to scriptSig ➡️ setting tx.vin[0].nSequence=0 and tx.nCheckTimeLock=500 * _the nSequence field of the txin is 0xffffffff_ ➡️ prepending `OPNum(500) OP_CHECKLOCKTIMEVERIFY OP_DROP` to scriptSig ➡️ setting tx.vin[0].nSequence=0xffffffff and tx.nCheckTimeLock=500 The first commit creates a helper function for the tx modification and also includes some tidying up like turning single-line to multi-line Python imports where necessary and cleaning up some PEP8 warnings. The second commit prepares the invalidation function `cltv_invalidate` and the third and the fourth use it and check for the expected reject reason strings ("Operation not valid with the current stack size", "Negative locktime" and "Locktime requirement not satisfied"). ACKs for top commit: MarcoFalke: review ACK b01cd94 🐣 Tree-SHA512: dd82ae86e2bc4f3ab9bb1cfc9f04e4431b2b59c8aaf2a9f4b28654a1577e003fb43c500f99d76ff57e96262168e1cad7c1a0d71158e4b01063737e8f4be1e07d
2 parents aaf6641 + b01cd94 commit 4b5659c

File tree

1 file changed

+111
-61
lines changed

1 file changed

+111
-61
lines changed

test/functional/feature_cltv.py

Lines changed: 111 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,24 @@
88
1351.
99
"""
1010

11-
from test_framework.blocktools import create_coinbase, create_block, create_transaction
12-
from test_framework.messages import CTransaction, msg_block, ToHex
11+
from test_framework.blocktools import (
12+
create_block,
13+
create_coinbase,
14+
create_transaction,
15+
)
16+
from test_framework.messages import (
17+
CTransaction,
18+
ToHex,
19+
msg_block,
20+
)
1321
from test_framework.p2p import P2PInterface
14-
from test_framework.script import CScript, OP_1NEGATE, OP_CHECKLOCKTIMEVERIFY, OP_DROP, CScriptNum
22+
from test_framework.script import (
23+
CScript,
24+
CScriptNum,
25+
OP_1NEGATE,
26+
OP_CHECKLOCKTIMEVERIFY,
27+
OP_DROP,
28+
)
1529
from test_framework.test_framework import BitcoinTestFramework
1630
from test_framework.util import (
1731
assert_equal,
@@ -23,32 +37,54 @@
2337
CLTV_HEIGHT = 1351
2438

2539

26-
def cltv_invalidate(tx):
27-
'''Modify the signature in vin 0 of the tx to fail CLTV
40+
# Helper function to modify a transaction by
41+
# 1) prepending a given script to the scriptSig of vin 0 and
42+
# 2) (optionally) modify the nSequence of vin 0 and the tx's nLockTime
43+
def cltv_modify_tx(node, tx, prepend_scriptsig, nsequence=None, nlocktime=None):
44+
if nsequence is not None:
45+
tx.vin[0].nSequence = nsequence
46+
tx.nLockTime = nlocktime
47+
48+
# Need to re-sign, since nSequence and nLockTime changed
49+
signed_result = node.signrawtransactionwithwallet(ToHex(tx))
50+
new_tx = CTransaction()
51+
new_tx.deserialize(BytesIO(hex_str_to_bytes(signed_result['hex'])))
52+
else:
53+
new_tx = tx
54+
55+
new_tx.vin[0].scriptSig = CScript(prepend_scriptsig + list(CScript(new_tx.vin[0].scriptSig)))
56+
return new_tx
57+
2858

29-
Prepends -1 CLTV DROP in the scriptSig itself.
59+
def cltv_invalidate(node, tx, failure_reason):
60+
# Modify the signature in vin 0 and nSequence/nLockTime of the tx to fail CLTV
61+
#
62+
# According to BIP65, OP_CHECKLOCKTIMEVERIFY can fail due the following reasons:
63+
# 1) the stack is empty
64+
# 2) the top item on the stack is less than 0
65+
# 3) the lock-time type (height vs. timestamp) of the top stack item and the
66+
# nLockTime field are not the same
67+
# 4) the top stack item is greater than the transaction's nLockTime field
68+
# 5) the nSequence field of the txin is 0xffffffff
69+
assert failure_reason in range(5)
70+
scheme = [
71+
# | Script to prepend to scriptSig | nSequence | nLockTime |
72+
# +-------------------------------------------------+------------+--------------+
73+
[[OP_CHECKLOCKTIMEVERIFY], None, None],
74+
[[OP_1NEGATE, OP_CHECKLOCKTIMEVERIFY, OP_DROP], None, None],
75+
[[CScriptNum(1000), OP_CHECKLOCKTIMEVERIFY, OP_DROP], 0, 1296688602], # timestamp of genesis block
76+
[[CScriptNum(1000), OP_CHECKLOCKTIMEVERIFY, OP_DROP], 0, 500],
77+
[[CScriptNum(500), OP_CHECKLOCKTIMEVERIFY, OP_DROP], 0xffffffff, 500],
78+
][failure_reason]
79+
80+
return cltv_modify_tx(node, tx, prepend_scriptsig=scheme[0], nsequence=scheme[1], nlocktime=scheme[2])
3081

31-
TODO: test more ways that transactions using CLTV could be invalid (eg
32-
locktime requirements fail, sequence time requirements fail, etc).
33-
'''
34-
tx.vin[0].scriptSig = CScript([OP_1NEGATE, OP_CHECKLOCKTIMEVERIFY, OP_DROP] +
35-
list(CScript(tx.vin[0].scriptSig)))
3682

3783
def cltv_validate(node, tx, height):
38-
'''Modify the signature in vin 0 of the tx to pass CLTV
39-
Prepends <height> CLTV DROP in the scriptSig, and sets
40-
the locktime to height'''
41-
tx.vin[0].nSequence = 0
42-
tx.nLockTime = height
43-
44-
# Need to re-sign, since nSequence and nLockTime changed
45-
signed_result = node.signrawtransactionwithwallet(ToHex(tx))
46-
new_tx = CTransaction()
47-
new_tx.deserialize(BytesIO(hex_str_to_bytes(signed_result['hex'])))
48-
49-
new_tx.vin[0].scriptSig = CScript([CScriptNum(height), OP_CHECKLOCKTIMEVERIFY, OP_DROP] +
50-
list(CScript(new_tx.vin[0].scriptSig)))
51-
return new_tx
84+
# Modify the signature in vin 0 and nSequence/nLockTime of the tx to pass CLTV
85+
scheme = [[CScriptNum(height), OP_CHECKLOCKTIMEVERIFY, OP_DROP], 0, height]
86+
87+
return cltv_modify_tx(node, tx, prepend_scriptsig=scheme[0], nsequence=scheme[1], nlocktime=scheme[2])
5288

5389

5490
class BIP65Test(BitcoinTestFramework):
@@ -66,8 +102,7 @@ def skip_test_if_missing_module(self):
66102
self.skip_if_no_wallet()
67103

68104
def test_cltv_info(self, *, is_active):
69-
assert_equal(self.nodes[0].getblockchaininfo()['softforks']['bip65'],
70-
{
105+
assert_equal(self.nodes[0].getblockchaininfo()['softforks']['bip65'], {
71106
"active": is_active,
72107
"height": CLTV_HEIGHT,
73108
"type": "buried",
@@ -83,18 +118,22 @@ def run_test(self):
83118
self.coinbase_txids = [self.nodes[0].getblock(b)['tx'][0] for b in self.nodes[0].generate(CLTV_HEIGHT - 2)]
84119
self.nodeaddress = self.nodes[0].getnewaddress()
85120

86-
self.log.info("Test that an invalid-according-to-CLTV transaction can still appear in a block")
121+
self.log.info("Test that invalid-according-to-CLTV transactions can still appear in a block")
87122

88-
spendtx = create_transaction(self.nodes[0], self.coinbase_txids[0],
89-
self.nodeaddress, amount=1.0)
90-
cltv_invalidate(spendtx)
91-
spendtx.rehash()
123+
# create one invalid tx per CLTV failure reason (5 in total) and collect them
124+
invalid_ctlv_txs = []
125+
for i in range(5):
126+
spendtx = create_transaction(self.nodes[0], self.coinbase_txids[i],
127+
self.nodeaddress, amount=1.0)
128+
spendtx = cltv_invalidate(self.nodes[0], spendtx, i)
129+
spendtx.rehash()
130+
invalid_ctlv_txs.append(spendtx)
92131

93132
tip = self.nodes[0].getbestblockhash()
94133
block_time = self.nodes[0].getblockheader(tip)['mediantime'] + 1
95134
block = create_block(int(tip, 16), create_coinbase(CLTV_HEIGHT - 1), block_time)
96135
block.nVersion = 3
97-
block.vtx.append(spendtx)
136+
block.vtx.extend(invalid_ctlv_txs)
98137
block.hashMerkleRoot = block.calc_merkle_root()
99138
block.solve()
100139

@@ -115,35 +154,46 @@ def run_test(self):
115154
assert_equal(int(self.nodes[0].getbestblockhash(), 16), tip)
116155
peer.sync_with_ping()
117156

118-
self.log.info("Test that invalid-according-to-cltv transactions cannot appear in a block")
157+
self.log.info("Test that invalid-according-to-CLTV transactions cannot appear in a block")
119158
block.nVersion = 4
120-
121-
spendtx = create_transaction(self.nodes[0], self.coinbase_txids[1],
122-
self.nodeaddress, amount=1.0)
123-
cltv_invalidate(spendtx)
124-
spendtx.rehash()
125-
126-
# First we show that this tx is valid except for CLTV by getting it
127-
# rejected from the mempool for exactly that reason.
128-
assert_equal(
129-
[{
130-
'txid': spendtx.hash,
131-
'wtxid': spendtx.getwtxid(),
132-
'allowed': False,
133-
'reject-reason': 'non-mandatory-script-verify-flag (Negative locktime)',
134-
}],
135-
self.nodes[0].testmempoolaccept(rawtxs=[spendtx.serialize().hex()], maxfeerate=0),
136-
)
137-
138-
# Now we verify that a block with this transaction is also invalid.
139-
block.vtx.append(spendtx)
140-
block.hashMerkleRoot = block.calc_merkle_root()
141-
block.solve()
142-
143-
with self.nodes[0].assert_debug_log(expected_msgs=['CheckInputScripts on {} failed with non-mandatory-script-verify-flag (Negative locktime)'.format(block.vtx[-1].hash)]):
144-
peer.send_and_ping(msg_block(block))
145-
assert_equal(int(self.nodes[0].getbestblockhash(), 16), tip)
146-
peer.sync_with_ping()
159+
block.vtx.append(CTransaction()) # dummy tx after coinbase that will be replaced later
160+
161+
# create and test one invalid tx per CLTV failure reason (5 in total)
162+
for i in range(5):
163+
spendtx = create_transaction(self.nodes[0], self.coinbase_txids[10+i],
164+
self.nodeaddress, amount=1.0)
165+
spendtx = cltv_invalidate(self.nodes[0], spendtx, i)
166+
spendtx.rehash()
167+
168+
expected_cltv_reject_reason = [
169+
"non-mandatory-script-verify-flag (Operation not valid with the current stack size)",
170+
"non-mandatory-script-verify-flag (Negative locktime)",
171+
"non-mandatory-script-verify-flag (Locktime requirement not satisfied)",
172+
"non-mandatory-script-verify-flag (Locktime requirement not satisfied)",
173+
"non-mandatory-script-verify-flag (Locktime requirement not satisfied)",
174+
][i]
175+
# First we show that this tx is valid except for CLTV by getting it
176+
# rejected from the mempool for exactly that reason.
177+
assert_equal(
178+
[{
179+
'txid': spendtx.hash,
180+
'wtxid': spendtx.getwtxid(),
181+
'allowed': False,
182+
'reject-reason': expected_cltv_reject_reason,
183+
}],
184+
self.nodes[0].testmempoolaccept(rawtxs=[spendtx.serialize().hex()], maxfeerate=0),
185+
)
186+
187+
# Now we verify that a block with this transaction is also invalid.
188+
block.vtx[1] = spendtx
189+
block.hashMerkleRoot = block.calc_merkle_root()
190+
block.solve()
191+
192+
with self.nodes[0].assert_debug_log(expected_msgs=['CheckInputScripts on {} failed with {}'.format(
193+
block.vtx[-1].hash, expected_cltv_reject_reason)]):
194+
peer.send_and_ping(msg_block(block))
195+
assert_equal(int(self.nodes[0].getbestblockhash(), 16), tip)
196+
peer.sync_with_ping()
147197

148198
self.log.info("Test that a version 4 block with a valid-according-to-CLTV transaction is accepted")
149199
spendtx = cltv_validate(self.nodes[0], spendtx, CLTV_HEIGHT - 1)

0 commit comments

Comments
 (0)