Skip to content

Commit 876e92b

Browse files
committed
Testing: listsinceblock should display all transactions that were affected since the given block, including transactions that were removed due to a reorg.
1 parent f999c46 commit 876e92b

File tree

1 file changed

+180
-10
lines changed

1 file changed

+180
-10
lines changed

test/functional/listsinceblock.py

Lines changed: 180 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,15 @@ def __init__(self):
1414
self.setup_clean_chain = True
1515
self.num_nodes = 4
1616

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):
1826
'''
1927
`listsinceblock` did not behave correctly when handed a block that was
2028
no longer in the main chain:
@@ -43,14 +51,6 @@ def run_test (self):
4351
This test only checks that [tx0] is present.
4452
'''
4553

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-
5454
# Split network into two
5555
self.split_network()
5656

@@ -73,7 +73,177 @@ def run_test (self):
7373
if tx['txid'] == senttx:
7474
found = True
7575
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)
77247

78248
if __name__ == '__main__':
79249
ListSinceBlockTest().main()

0 commit comments

Comments
 (0)