aboutsummaryrefslogtreecommitdiff
path: root/qa/rpc-tests/bip68-sequence.py
blob: 1b099f933967524d9826229e270e78fa13b9188c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
#!/usr/bin/env python3
# Copyright (c) 2014-2016 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.

#
# Test BIP68 implementation
#

from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import *
from test_framework.blocktools import *

SEQUENCE_LOCKTIME_DISABLE_FLAG = (1<<31)
SEQUENCE_LOCKTIME_TYPE_FLAG = (1<<22) # this means use time (0 means height)
SEQUENCE_LOCKTIME_GRANULARITY = 9 # this is a bit-shift
SEQUENCE_LOCKTIME_MASK = 0x0000ffff

# RPC error for non-BIP68 final transactions
NOT_FINAL_ERROR = "64: non-BIP68-final"

class BIP68Test(BitcoinTestFramework):
    def __init__(self):
        super().__init__()
        self.num_nodes = 2
        self.setup_clean_chain = False

    def setup_network(self):
        self.nodes = []
        self.nodes.append(start_node(0, self.options.tmpdir, ["-debug", "-blockprioritysize=0"]))
        self.nodes.append(start_node(1, self.options.tmpdir, ["-debug", "-blockprioritysize=0", "-acceptnonstdtxn=0"]))
        self.is_network_split = False
        self.relayfee = self.nodes[0].getnetworkinfo()["relayfee"]
        connect_nodes(self.nodes[0], 1)

    def run_test(self):
        # Generate some coins
        self.nodes[0].generate(110)

        print("Running test disable flag")
        self.test_disable_flag()

        print("Running test sequence-lock-confirmed-inputs")
        self.test_sequence_lock_confirmed_inputs()

        print("Running test sequence-lock-unconfirmed-inputs")
        self.test_sequence_lock_unconfirmed_inputs()

        print("Running test BIP68 not consensus before versionbits activation")
        self.test_bip68_not_consensus()

        print("Verifying nVersion=2 transactions aren't standard")
        self.test_version2_relay(before_activation=True)

        print("Activating BIP68 (and 112/113)")
        self.activateCSV()

        print("Verifying nVersion=2 transactions are now standard")
        self.test_version2_relay(before_activation=False)

        print("Passed\n")

    # Test that BIP68 is not in effect if tx version is 1, or if
    # the first sequence bit is set.
    def test_disable_flag(self):
        # Create some unconfirmed inputs
        new_addr = self.nodes[0].getnewaddress()
        self.nodes[0].sendtoaddress(new_addr, 2) # send 2 BTC

        utxos = self.nodes[0].listunspent(0, 0)
        assert(len(utxos) > 0)

        utxo = utxos[0]

        tx1 = CTransaction()
        value = int(satoshi_round(utxo["amount"] - self.relayfee)*COIN)

        # Check that the disable flag disables relative locktime.
        # If sequence locks were used, this would require 1 block for the
        # input to mature.
        sequence_value = SEQUENCE_LOCKTIME_DISABLE_FLAG | 1
        tx1.vin = [CTxIn(COutPoint(int(utxo["txid"], 16), utxo["vout"]), nSequence=sequence_value)] 
        tx1.vout = [CTxOut(value, CScript([b'a']))]

        tx1_signed = self.nodes[0].signrawtransaction(ToHex(tx1))["hex"]
        tx1_id = self.nodes[0].sendrawtransaction(tx1_signed)
        tx1_id = int(tx1_id, 16)

        # This transaction will enable sequence-locks, so this transaction should
        # fail
        tx2 = CTransaction()
        tx2.nVersion = 2
        sequence_value = sequence_value & 0x7fffffff
        tx2.vin = [CTxIn(COutPoint(tx1_id, 0), nSequence=sequence_value)]
        tx2.vout = [CTxOut(int(value-self.relayfee*COIN), CScript([b'a']))]
        tx2.rehash()

        try:
            self.nodes[0].sendrawtransaction(ToHex(tx2))
        except JSONRPCException as exp:
            assert_equal(exp.error["message"], NOT_FINAL_ERROR)
        else:
            assert(False)

        # Setting the version back down to 1 should disable the sequence lock,
        # so this should be accepted.
        tx2.nVersion = 1

        self.nodes[0].sendrawtransaction(ToHex(tx2))

    # Calculate the median time past of a prior block ("confirmations" before
    # the current tip).
    def get_median_time_past(self, confirmations):
        block_hash = self.nodes[0].getblockhash(self.nodes[0].getblockcount()-confirmations)
        return self.nodes[0].getblockheader(block_hash)["mediantime"]

    # Test that sequence locks are respected for transactions spending confirmed inputs.
    def test_sequence_lock_confirmed_inputs(self):
        # Create lots of confirmed utxos, and use them to generate lots of random
        # transactions.
        max_outputs = 50
        addresses = []
        while len(addresses) < max_outputs:
            addresses.append(self.nodes[0].getnewaddress())
        while len(self.nodes[0].listunspent()) < 200:
            import random
            random.shuffle(addresses)
            num_outputs = random.randint(1, max_outputs)
            outputs = {}
            for i in range(num_outputs):
                outputs[addresses[i]] = random.randint(1, 20)*0.01
            self.nodes[0].sendmany("", outputs)
            self.nodes[0].generate(1)

        utxos = self.nodes[0].listunspent()

        # Try creating a lot of random transactions.
        # Each time, choose a random number of inputs, and randomly set
        # some of those inputs to be sequence locked (and randomly choose
        # between height/time locking). Small random chance of making the locks
        # all pass.
        for i in range(400):
            # Randomly choose up to 10 inputs
            num_inputs = random.randint(1, 10)
            random.shuffle(utxos)

            # Track whether any sequence locks used should fail
            should_pass = True
            
            # Track whether this transaction was built with sequence locks
            using_sequence_locks = False

            tx = CTransaction()
            tx.nVersion = 2
            value = 0
            for j in range(num_inputs):
                sequence_value = 0xfffffffe # this disables sequence locks

                # 50% chance we enable sequence locks
                if random.randint(0,1):
                    using_sequence_locks = True

                    # 10% of the time, make the input sequence value pass
                    input_will_pass = (random.randint(1,10) == 1)
                    sequence_value = utxos[j]["confirmations"]
                    if not input_will_pass:
                        sequence_value += 1
                        should_pass = False

                    # Figure out what the median-time-past was for the confirmed input
                    # Note that if an input has N confirmations, we're going back N blocks
                    # from the tip so that we're looking up MTP of the block
                    # PRIOR to the one the input appears in, as per the BIP68 spec.
                    orig_time = self.get_median_time_past(utxos[j]["confirmations"])
                    cur_time = self.get_median_time_past(0) # MTP of the tip

                    # can only timelock this input if it's not too old -- otherwise use height
                    can_time_lock = True
                    if ((cur_time - orig_time) >> SEQUENCE_LOCKTIME_GRANULARITY) >= SEQUENCE_LOCKTIME_MASK:
                        can_time_lock = False

                    # if time-lockable, then 50% chance we make this a time lock
                    if random.randint(0,1) and can_time_lock:
                        # Find first time-lock value that fails, or latest one that succeeds
                        time_delta = sequence_value << SEQUENCE_LOCKTIME_GRANULARITY
                        if input_will_pass and time_delta > cur_time - orig_time:
                            sequence_value = ((cur_time - orig_time) >> SEQUENCE_LOCKTIME_GRANULARITY)
                        elif (not input_will_pass and time_delta <= cur_time - orig_time):
                            sequence_value = ((cur_time - orig_time) >> SEQUENCE_LOCKTIME_GRANULARITY)+1
                        sequence_value |= SEQUENCE_LOCKTIME_TYPE_FLAG
                tx.vin.append(CTxIn(COutPoint(int(utxos[j]["txid"], 16), utxos[j]["vout"]), nSequence=sequence_value))
                value += utxos[j]["amount"]*COIN
            # Overestimate the size of the tx - signatures should be less than 120 bytes, and leave 50 for the output
            tx_size = len(ToHex(tx))//2 + 120*num_inputs + 50
            tx.vout.append(CTxOut(int(value-self.relayfee*tx_size*COIN/1000), CScript([b'a'])))
            rawtx = self.nodes[0].signrawtransaction(ToHex(tx))["hex"]

            try:
                self.nodes[0].sendrawtransaction(rawtx)
            except JSONRPCException as exp:
                assert(not should_pass and using_sequence_locks)
                assert_equal(exp.error["message"], NOT_FINAL_ERROR)
            else:
                assert(should_pass or not using_sequence_locks)
                # Recalculate utxos if we successfully sent the transaction
                utxos = self.nodes[0].listunspent()

    # Test that sequence locks on unconfirmed inputs must have nSequence
    # height or time of 0 to be accepted.
    # Then test that BIP68-invalid transactions are removed from the mempool
    # after a reorg.
    def test_sequence_lock_unconfirmed_inputs(self):
        # Store height so we can easily reset the chain at the end of the test
        cur_height = self.nodes[0].getblockcount()

        # Create a mempool tx.
        txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 2)
        tx1 = FromHex(CTransaction(), self.nodes[0].getrawtransaction(txid))
        tx1.rehash()

        # Anyone-can-spend mempool tx.
        # Sequence lock of 0 should pass.
        tx2 = CTransaction()
        tx2.nVersion = 2
        tx2.vin = [CTxIn(COutPoint(tx1.sha256, 0), nSequence=0)]
        tx2.vout = [CTxOut(int(tx1.vout[0].nValue - self.relayfee*COIN), CScript([b'a']))]
        tx2_raw = self.nodes[0].signrawtransaction(ToHex(tx2))["hex"]
        tx2 = FromHex(tx2, tx2_raw)
        tx2.rehash()

        self.nodes[0].sendrawtransaction(tx2_raw)

        # Create a spend of the 0th output of orig_tx with a sequence lock
        # of 1, and test what happens when submitting.
        # orig_tx.vout[0] must be an anyone-can-spend output
        def test_nonzero_locks(orig_tx, node, relayfee, use_height_lock):
            sequence_value = 1
            if not use_height_lock:
                sequence_value |= SEQUENCE_LOCKTIME_TYPE_FLAG

            tx = CTransaction()
            tx.nVersion = 2
            tx.vin = [CTxIn(COutPoint(orig_tx.sha256, 0), nSequence=sequence_value)]
            tx.vout = [CTxOut(int(orig_tx.vout[0].nValue - relayfee*COIN), CScript([b'a']))]
            tx.rehash()

            try:
                node.sendrawtransaction(ToHex(tx))
            except JSONRPCException as exp:
                assert_equal(exp.error["message"], NOT_FINAL_ERROR)
                assert(orig_tx.hash in node.getrawmempool())
            else:
                # orig_tx must not be in mempool
                assert(orig_tx.hash not in node.getrawmempool())
            return tx

        test_nonzero_locks(tx2, self.nodes[0], self.relayfee, use_height_lock=True)
        test_nonzero_locks(tx2, self.nodes[0], self.relayfee, use_height_lock=False)

        # Now mine some blocks, but make sure tx2 doesn't get mined.
        # Use prioritisetransaction to lower the effective feerate to 0
        self.nodes[0].prioritisetransaction(tx2.hash, -1e15, int(-self.relayfee*COIN))
        cur_time = int(time.time())
        for i in range(10):
            self.nodes[0].setmocktime(cur_time + 600)
            self.nodes[0].generate(1)
            cur_time += 600

        assert(tx2.hash in self.nodes[0].getrawmempool())

        test_nonzero_locks(tx2, self.nodes[0], self.relayfee, use_height_lock=True)
        test_nonzero_locks(tx2, self.nodes[0], self.relayfee, use_height_lock=False)

        # Mine tx2, and then try again
        self.nodes[0].prioritisetransaction(tx2.hash, 1e15, int(self.relayfee*COIN))

        # Advance the time on the node so that we can test timelocks
        self.nodes[0].setmocktime(cur_time+600)
        self.nodes[0].generate(1)
        assert(tx2.hash not in self.nodes[0].getrawmempool())

        # Now that tx2 is not in the mempool, a sequence locked spend should
        # succeed
        tx3 = test_nonzero_locks(tx2, self.nodes[0], self.relayfee, use_height_lock=False)
        assert(tx3.hash in self.nodes[0].getrawmempool())

        self.nodes[0].generate(1)
        assert(tx3.hash not in self.nodes[0].getrawmempool())

        # One more test, this time using height locks
        tx4 = test_nonzero_locks(tx3, self.nodes[0], self.relayfee, use_height_lock=True)
        assert(tx4.hash in self.nodes[0].getrawmempool())

        # Now try combining confirmed and unconfirmed inputs
        tx5 = test_nonzero_locks(tx4, self.nodes[0], self.relayfee, use_height_lock=True)
        assert(tx5.hash not in self.nodes[0].getrawmempool())

        utxos = self.nodes[0].listunspent()
        tx5.vin.append(CTxIn(COutPoint(int(utxos[0]["txid"], 16), utxos[0]["vout"]), nSequence=1))
        tx5.vout[0].nValue += int(utxos[0]["amount"]*COIN)
        raw_tx5 = self.nodes[0].signrawtransaction(ToHex(tx5))["hex"]

        try:
            self.nodes[0].sendrawtransaction(raw_tx5)
        except JSONRPCException as exp:
            assert_equal(exp.error["message"], NOT_FINAL_ERROR)
        else:
            assert(False)

        # Test mempool-BIP68 consistency after reorg
        #
        # State of the transactions in the last blocks:
        # ... -> [ tx2 ] ->  [ tx3 ]
        #         tip-1        tip
        # And currently tx4 is in the mempool.
        #
        # If we invalidate the tip, tx3 should get added to the mempool, causing
        # tx4 to be removed (fails sequence-lock).
        self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash())
        assert(tx4.hash not in self.nodes[0].getrawmempool())
        assert(tx3.hash in self.nodes[0].getrawmempool())

        # Now mine 2 empty blocks to reorg out the current tip (labeled tip-1 in
        # diagram above).
        # This would cause tx2 to be added back to the mempool, which in turn causes
        # tx3 to be removed.
        tip = int(self.nodes[0].getblockhash(self.nodes[0].getblockcount()-1), 16)
        height = self.nodes[0].getblockcount()
        for i in range(2):
            block = create_block(tip, create_coinbase(height), cur_time)
            block.nVersion = 3
            block.rehash()
            block.solve()
            tip = block.sha256
            height += 1
            self.nodes[0].submitblock(ToHex(block))
            cur_time += 1

        mempool = self.nodes[0].getrawmempool()
        assert(tx3.hash not in mempool)
        assert(tx2.hash in mempool)

        # Reset the chain and get rid of the mocktimed-blocks
        self.nodes[0].setmocktime(0)
        self.nodes[0].invalidateblock(self.nodes[0].getblockhash(cur_height+1))
        self.nodes[0].generate(10)

    # Make sure that BIP68 isn't being used to validate blocks, prior to
    # versionbits activation.  If more blocks are mined prior to this test
    # being run, then it's possible the test has activated the soft fork, and
    # this test should be moved to run earlier, or deleted.
    def test_bip68_not_consensus(self):
        assert(get_bip9_status(self.nodes[0], 'csv')['status'] != 'active')
        txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 2)

        tx1 = FromHex(CTransaction(), self.nodes[0].getrawtransaction(txid))
        tx1.rehash()

        # Make an anyone-can-spend transaction
        tx2 = CTransaction()
        tx2.nVersion = 1
        tx2.vin = [CTxIn(COutPoint(tx1.sha256, 0), nSequence=0)]
        tx2.vout = [CTxOut(int(tx1.vout[0].nValue - self.relayfee*COIN), CScript([b'a']))]

        # sign tx2
        tx2_raw = self.nodes[0].signrawtransaction(ToHex(tx2))["hex"]
        tx2 = FromHex(tx2, tx2_raw)
        tx2.rehash()

        self.nodes[0].sendrawtransaction(ToHex(tx2))
        
        # Now make an invalid spend of tx2 according to BIP68
        sequence_value = 100 # 100 block relative locktime

        tx3 = CTransaction()
        tx3.nVersion = 2
        tx3.vin = [CTxIn(COutPoint(tx2.sha256, 0), nSequence=sequence_value)]
        tx3.vout = [CTxOut(int(tx2.vout[0].nValue - self.relayfee*COIN), CScript([b'a']))]
        tx3.rehash()

        try:
            self.nodes[0].sendrawtransaction(ToHex(tx3))
        except JSONRPCException as exp:
            assert_equal(exp.error["message"], NOT_FINAL_ERROR)
        else:
            assert(False)

        # make a block that violates bip68; ensure that the tip updates
        tip = int(self.nodes[0].getbestblockhash(), 16)
        block = create_block(tip, create_coinbase(self.nodes[0].getblockcount()+1))
        block.nVersion = 3
        block.vtx.extend([tx1, tx2, tx3])
        block.hashMerkleRoot = block.calc_merkle_root()
        block.rehash()
        block.solve()

        self.nodes[0].submitblock(ToHex(block))
        assert_equal(self.nodes[0].getbestblockhash(), block.hash)

    def activateCSV(self):
        # activation should happen at block height 432 (3 periods)
        min_activation_height = 432
        height = self.nodes[0].getblockcount()
        assert(height < 432)
        self.nodes[0].generate(432-height)
        assert(get_bip9_status(self.nodes[0], 'csv')['status'] == 'active')
        sync_blocks(self.nodes)

    # Use self.nodes[1] to test standardness relay policy
    def test_version2_relay(self, before_activation):
        inputs = [ ]
        outputs = { self.nodes[1].getnewaddress() : 1.0 }
        rawtx = self.nodes[1].createrawtransaction(inputs, outputs)
        rawtxfund = self.nodes[1].fundrawtransaction(rawtx)['hex']
        tx = FromHex(CTransaction(), rawtxfund)
        tx.nVersion = 2
        tx_signed = self.nodes[1].signrawtransaction(ToHex(tx))["hex"]
        try:
            tx_id = self.nodes[1].sendrawtransaction(tx_signed)
            assert(before_activation == False)
        except:
            assert(before_activation)


if __name__ == '__main__':
    BIP68Test().main()