@@ -14,7 +14,15 @@ def __init__(self):
14
14
self .setup_clean_chain = True
15
15
self .num_nodes = 4
16
16
17
- def run_test (self ):
17
+ def run_test (self ):
18
+ self .nodes [2 ].generate (101 )
19
+ self .sync_all ()
20
+
21
+ self .test_reorg ()
22
+ self .test_double_spend ()
23
+ self .test_double_send ()
24
+
25
+ def test_reorg (self ):
18
26
'''
19
27
`listsinceblock` did not behave correctly when handed a block that was
20
28
no longer in the main chain:
@@ -43,14 +51,6 @@ def run_test (self):
43
51
This test only checks that [tx0] is present.
44
52
'''
45
53
46
- self .nodes [2 ].generate (101 )
47
- self .sync_all ()
48
-
49
- assert_equal (self .nodes [0 ].getbalance (), 0 )
50
- assert_equal (self .nodes [1 ].getbalance (), 0 )
51
- assert_equal (self .nodes [2 ].getbalance (), 50 )
52
- assert_equal (self .nodes [3 ].getbalance (), 0 )
53
-
54
54
# Split network into two
55
55
self .split_network ()
56
56
@@ -73,7 +73,177 @@ def run_test (self):
73
73
if tx ['txid' ] == senttx :
74
74
found = True
75
75
break
76
- assert_equal (found , True )
76
+ assert found
77
+
78
+ def test_double_spend (self ):
79
+ '''
80
+ This tests the case where the same UTXO is spent twice on two separate
81
+ blocks as part of a reorg.
82
+
83
+ ab0
84
+ / \
85
+ aa1 [tx1] bb1 [tx2]
86
+ | |
87
+ aa2 bb2
88
+ | |
89
+ aa3 bb3
90
+ |
91
+ bb4
92
+
93
+ Problematic case:
94
+
95
+ 1. User 1 receives BTC in tx1 from utxo1 in block aa1.
96
+ 2. User 2 receives BTC in tx2 from utxo1 (same) in block bb1
97
+ 3. User 1 sees 2 confirmations at block aa3.
98
+ 4. Reorg into bb chain.
99
+ 5. User 1 asks `listsinceblock aa3` and does not see that tx1 is now
100
+ invalidated.
101
+
102
+ Currently the solution to this is to detect that a reorg'd block is
103
+ asked for in listsinceblock, and to iterate back over existing blocks up
104
+ until the fork point, and to include all transactions that relate to the
105
+ node wallet.
106
+ '''
107
+
108
+ self .sync_all ()
109
+
110
+ # Split network into two
111
+ self .split_network ()
112
+
113
+ # share utxo between nodes[1] and nodes[2]
114
+ utxos = self .nodes [2 ].listunspent ()
115
+ utxo = utxos [0 ]
116
+ privkey = self .nodes [2 ].dumpprivkey (utxo ['address' ])
117
+ self .nodes [1 ].importprivkey (privkey )
118
+
119
+ # send from nodes[1] using utxo to nodes[0]
120
+ change = '%.8f' % (float (utxo ['amount' ]) - 1.0003 )
121
+ recipientDict = {
122
+ self .nodes [0 ].getnewaddress (): 1 ,
123
+ self .nodes [1 ].getnewaddress (): change ,
124
+ }
125
+ utxoDicts = [{
126
+ 'txid' : utxo ['txid' ],
127
+ 'vout' : utxo ['vout' ],
128
+ }]
129
+ txid1 = self .nodes [1 ].sendrawtransaction (
130
+ self .nodes [1 ].signrawtransaction (
131
+ self .nodes [1 ].createrawtransaction (utxoDicts , recipientDict ))['hex' ])
132
+
133
+ # send from nodes[2] using utxo to nodes[3]
134
+ recipientDict2 = {
135
+ self .nodes [3 ].getnewaddress (): 1 ,
136
+ self .nodes [2 ].getnewaddress (): change ,
137
+ }
138
+ self .nodes [2 ].sendrawtransaction (
139
+ self .nodes [2 ].signrawtransaction (
140
+ self .nodes [2 ].createrawtransaction (utxoDicts , recipientDict2 ))['hex' ])
141
+
142
+ # generate on both sides
143
+ lastblockhash = self .nodes [1 ].generate (3 )[2 ]
144
+ self .nodes [2 ].generate (4 )
145
+
146
+ self .join_network ()
147
+
148
+ self .sync_all ()
149
+
150
+ # gettransaction should work for txid1
151
+ assert self .nodes [0 ].gettransaction (txid1 )['txid' ] == txid1 , "gettransaction failed to find txid1"
152
+
153
+ # listsinceblock(lastblockhash) should now include txid1, as seen from nodes[0]
154
+ lsbres = self .nodes [0 ].listsinceblock (lastblockhash )
155
+ assert any (tx ['txid' ] == txid1 for tx in lsbres ['removed' ])
156
+
157
+ # but it should not include 'removed' if include_removed=false
158
+ lsbres2 = self .nodes [0 ].listsinceblock (blockhash = lastblockhash , include_removed = False )
159
+ assert 'removed' not in lsbres2
160
+
161
+ def test_double_send (self ):
162
+ '''
163
+ This tests the case where the same transaction is submitted twice on two
164
+ separate blocks as part of a reorg. The former will vanish and the
165
+ latter will appear as the true transaction (with confirmations dropping
166
+ as a result).
167
+
168
+ ab0
169
+ / \
170
+ aa1 [tx1] bb1
171
+ | |
172
+ aa2 bb2
173
+ | |
174
+ aa3 bb3 [tx1]
175
+ |
176
+ bb4
177
+
178
+ Asserted:
179
+
180
+ 1. tx1 is listed in listsinceblock.
181
+ 2. It is included in 'removed' as it was removed, even though it is now
182
+ present in a different block.
183
+ 3. It is listed with a confirmations count of 2 (bb3, bb4), not
184
+ 3 (aa1, aa2, aa3).
185
+ '''
186
+
187
+ self .sync_all ()
188
+
189
+ # Split network into two
190
+ self .split_network ()
191
+
192
+ # create and sign a transaction
193
+ utxos = self .nodes [2 ].listunspent ()
194
+ utxo = utxos [0 ]
195
+ change = '%.8f' % (float (utxo ['amount' ]) - 1.0003 )
196
+ recipientDict = {
197
+ self .nodes [0 ].getnewaddress (): 1 ,
198
+ self .nodes [2 ].getnewaddress (): change ,
199
+ }
200
+ utxoDicts = [{
201
+ 'txid' : utxo ['txid' ],
202
+ 'vout' : utxo ['vout' ],
203
+ }]
204
+ signedtxres = self .nodes [2 ].signrawtransaction (
205
+ self .nodes [2 ].createrawtransaction (utxoDicts , recipientDict ))
206
+ assert signedtxres ['complete' ]
207
+
208
+ signedtx = signedtxres ['hex' ]
209
+
210
+ # send from nodes[1]; this will end up in aa1
211
+ txid1 = self .nodes [1 ].sendrawtransaction (signedtx )
212
+
213
+ # generate bb1-bb2 on right side
214
+ self .nodes [2 ].generate (2 )
215
+
216
+ # send from nodes[2]; this will end up in bb3
217
+ txid2 = self .nodes [2 ].sendrawtransaction (signedtx )
218
+
219
+ assert_equal (txid1 , txid2 )
220
+
221
+ # generate on both sides
222
+ lastblockhash = self .nodes [1 ].generate (3 )[2 ]
223
+ self .nodes [2 ].generate (2 )
224
+
225
+ self .join_network ()
226
+
227
+ self .sync_all ()
228
+
229
+ # gettransaction should work for txid1
230
+ self .nodes [0 ].gettransaction (txid1 )
231
+
232
+ # listsinceblock(lastblockhash) should now include txid1 in transactions
233
+ # as well as in removed
234
+ lsbres = self .nodes [0 ].listsinceblock (lastblockhash )
235
+ assert any (tx ['txid' ] == txid1 for tx in lsbres ['transactions' ])
236
+ assert any (tx ['txid' ] == txid1 for tx in lsbres ['removed' ])
237
+
238
+ # find transaction and ensure confirmations is valid
239
+ for tx in lsbres ['transactions' ]:
240
+ if tx ['txid' ] == txid1 :
241
+ assert_equal (tx ['confirmations' ], 2 )
242
+
243
+ # the same check for the removed array; confirmations should STILL be 2
244
+ for tx in lsbres ['removed' ]:
245
+ if tx ['txid' ] == txid1 :
246
+ assert_equal (tx ['confirmations' ], 2 )
77
247
78
248
if __name__ == '__main__' :
79
249
ListSinceBlockTest ().main ()
0 commit comments