Changeset View
Changeset View
Standalone View
Standalone View
test/functional/wallet_listsinceblock.py
#!/usr/bin/env python3 | #!/usr/bin/env python3 | ||||
# Copyright (c) 2017 The Bitcoin Core developers | # Copyright (c) 2017 The Bitcoin Core developers | ||||
# Distributed under the MIT software license, see the accompanying | # Distributed under the MIT software license, see the accompanying | ||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php. | # file COPYING or http://www.opensource.org/licenses/mit-license.php. | ||||
from test_framework.test_framework import BitcoinTestFramework | from test_framework.test_framework import BitcoinTestFramework | ||||
from test_framework.util import assert_equal | from test_framework.util import assert_equal | ||||
class ListSinceBlockTest (BitcoinTestFramework): | class ListSinceBlockTest (BitcoinTestFramework): | ||||
def set_test_params(self): | def set_test_params(self): | ||||
self.num_nodes = 4 | self.num_nodes = 4 | ||||
self.setup_clean_chain = True | self.setup_clean_chain = True | ||||
self.extra_args = [["-noparkdeepreorg"], ["-noparkdeepreorg"], [], []] | self.extra_args = [["-noparkdeepreorg"], ["-noparkdeepreorg"], [], []] | ||||
def run_test(self): | def run_test(self): | ||||
self.nodes[2].generate(101) | |||||
self.sync_all() | |||||
self.test_reorg() | |||||
self.test_double_spend() | |||||
self.test_double_send() | |||||
def test_reorg(self): | |||||
''' | ''' | ||||
`listsinceblock` did not behave correctly when handed a block that was | `listsinceblock` did not behave correctly when handed a block that was | ||||
no longer in the main chain: | no longer in the main chain: | ||||
ab0 | ab0 | ||||
/ \ | / \ | ||||
aa1 [tx0] bb1 | aa1 [tx0] bb1 | ||||
| | | | | | ||||
Show All 12 Lines | def test_reorg(self): | ||||
results restricted to bb3-bb4. | results restricted to bb3-bb4. | ||||
Now: listsinceblock finds the fork at ab0 and returns results in the | Now: listsinceblock finds the fork at ab0 and returns results in the | ||||
range bb1-bb4. | range bb1-bb4. | ||||
This test only checks that [tx0] is present. | This test only checks that [tx0] is present. | ||||
''' | ''' | ||||
self.nodes[2].generate(101) | |||||
self.sync_all() | |||||
assert_equal(self.nodes[0].getbalance(), 0) | |||||
assert_equal(self.nodes[1].getbalance(), 0) | |||||
assert_equal(self.nodes[2].getbalance(), 50) | |||||
assert_equal(self.nodes[3].getbalance(), 0) | |||||
# Split network into two | # Split network into two | ||||
self.split_network() | self.split_network() | ||||
# send to nodes[0] from nodes[2] | # send to nodes[0] from nodes[2] | ||||
senttx = self.nodes[2].sendtoaddress(self.nodes[0].getnewaddress(), 1) | senttx = self.nodes[2].sendtoaddress(self.nodes[0].getnewaddress(), 1) | ||||
# generate on both sides | # generate on both sides | ||||
lastblockhash = self.nodes[1].generate(6)[5] | lastblockhash = self.nodes[1].generate(6)[5] | ||||
self.nodes[2].generate(7) | self.nodes[2].generate(7) | ||||
self.log.info('lastblockhash={}'.format(lastblockhash)) | self.log.info('lastblockhash={}'.format(lastblockhash)) | ||||
self.sync_all([self.nodes[:2], self.nodes[2:]]) | self.sync_all([self.nodes[:2], self.nodes[2:]]) | ||||
self.join_network() | self.join_network() | ||||
# listsinceblock(lastblockhash) should now include tx, as seen from | # listsinceblock(lastblockhash) should now include tx, as seen from | ||||
# nodes[0] | # nodes[0] | ||||
lsbres = self.nodes[0].listsinceblock(lastblockhash) | lsbres = self.nodes[0].listsinceblock(lastblockhash) | ||||
found = False | found = False | ||||
for tx in lsbres['transactions']: | for tx in lsbres['transactions']: | ||||
if tx['txid'] == senttx: | if tx['txid'] == senttx: | ||||
found = True | found = True | ||||
break | break | ||||
assert_equal(found, True) | assert found | ||||
def test_double_spend(self): | |||||
''' | |||||
This tests the case where the same UTXO is spent twice on two separate | |||||
blocks as part of a reorg. | |||||
ab0 | |||||
/ \ | |||||
aa1 [tx1] bb1 [tx2] | |||||
| | | |||||
aa2 bb2 | |||||
| | | |||||
aa3 bb3 | |||||
| | |||||
bb4 | |||||
Problematic case: | |||||
1. User 1 receives BCH in tx1 from utxo1 in block aa1. | |||||
2. User 2 receives BCH in tx2 from utxo1 (same) in block bb1 | |||||
3. User 1 sees 2 confirmations at block aa3. | |||||
4. Reorg into bb chain. | |||||
5. User 1 asks `listsinceblock aa3` and does not see that tx1 is now | |||||
invalidated. | |||||
Currently the solution to this is to detect that a reorg'd block is | |||||
asked for in listsinceblock, and to iterate back over existing blocks up | |||||
until the fork point, and to include all transactions that relate to the | |||||
node wallet. | |||||
''' | |||||
self.sync_all() | |||||
# Split network into two | |||||
self.split_network() | |||||
# share utxo between nodes[1] and nodes[2] | |||||
utxos = self.nodes[2].listunspent() | |||||
utxo = utxos[0] | |||||
privkey = self.nodes[2].dumpprivkey(utxo['address']) | |||||
self.nodes[1].importprivkey(privkey) | |||||
# send from nodes[1] using utxo to nodes[0] | |||||
change = '{:.8f}'.format(float(utxo['amount']) - 1.0003) | |||||
recipientDict = { | |||||
self.nodes[0].getnewaddress(): 1, | |||||
self.nodes[1].getnewaddress(): change, | |||||
} | |||||
utxoDicts = [{ | |||||
'txid': utxo['txid'], | |||||
'vout': utxo['vout'], | |||||
}] | |||||
txid1 = self.nodes[1].sendrawtransaction( | |||||
self.nodes[1].signrawtransaction( | |||||
self.nodes[1].createrawtransaction(utxoDicts, recipientDict))['hex']) | |||||
# send from nodes[2] using utxo to nodes[3] | |||||
recipientDict2 = { | |||||
self.nodes[3].getnewaddress(): 1, | |||||
self.nodes[2].getnewaddress(): change, | |||||
} | |||||
self.nodes[2].sendrawtransaction( | |||||
self.nodes[2].signrawtransaction( | |||||
self.nodes[2].createrawtransaction(utxoDicts, recipientDict2))['hex']) | |||||
# generate on both sides | |||||
lastblockhash = self.nodes[1].generate(3)[2] | |||||
self.nodes[2].generate(4) | |||||
self.join_network() | |||||
self.sync_all() | |||||
# gettransaction should work for txid1 | |||||
assert self.nodes[0].gettransaction( | |||||
txid1)['txid'] == txid1, "gettransaction failed to find txid1" | |||||
# listsinceblock(lastblockhash) should now include txid1, as seen from nodes[0] | |||||
lsbres = self.nodes[0].listsinceblock(lastblockhash) | |||||
assert any(tx['txid'] == txid1 for tx in lsbres['removed']) | |||||
# but it should not include 'removed' if include_removed=false | |||||
lsbres2 = self.nodes[0].listsinceblock( | |||||
blockhash=lastblockhash, include_removed=False) | |||||
assert 'removed' not in lsbres2 | |||||
def test_double_send(self): | |||||
''' | |||||
This tests the case where the same transaction is submitted twice on two | |||||
separate blocks as part of a reorg. The former will vanish and the | |||||
latter will appear as the true transaction (with confirmations dropping | |||||
as a result). | |||||
ab0 | |||||
/ \ | |||||
aa1 [tx1] bb1 | |||||
| | | |||||
aa2 bb2 | |||||
| | | |||||
aa3 bb3 [tx1] | |||||
| | |||||
bb4 | |||||
Asserted: | |||||
1. tx1 is listed in listsinceblock. | |||||
2. It is included in 'removed' as it was removed, even though it is now | |||||
present in a different block. | |||||
3. It is listed with a confirmations count of 2 (bb3, bb4), not | |||||
3 (aa1, aa2, aa3). | |||||
''' | |||||
self.sync_all() | |||||
# Split network into two | |||||
self.split_network() | |||||
# create and sign a transaction | |||||
utxos = self.nodes[2].listunspent() | |||||
utxo = utxos[0] | |||||
change = '{:.8f}'.format(float(utxo['amount']) - 1.0003) | |||||
recipientDict = { | |||||
self.nodes[0].getnewaddress(): 1, | |||||
self.nodes[2].getnewaddress(): change, | |||||
} | |||||
utxoDicts = [{ | |||||
'txid': utxo['txid'], | |||||
'vout': utxo['vout'], | |||||
}] | |||||
signedtxres = self.nodes[2].signrawtransaction( | |||||
self.nodes[2].createrawtransaction(utxoDicts, recipientDict)) | |||||
assert signedtxres['complete'] | |||||
signedtx = signedtxres['hex'] | |||||
# send from nodes[1]; this will end up in aa1 | |||||
txid1 = self.nodes[1].sendrawtransaction(signedtx) | |||||
# generate bb1-bb2 on right side | |||||
self.nodes[2].generate(2) | |||||
# send from nodes[2]; this will end up in bb3 | |||||
txid2 = self.nodes[2].sendrawtransaction(signedtx) | |||||
assert_equal(txid1, txid2) | |||||
# generate on both sides | |||||
lastblockhash = self.nodes[1].generate(3)[2] | |||||
self.nodes[2].generate(2) | |||||
self.join_network() | |||||
self.sync_all() | |||||
# gettransaction should work for txid1 | |||||
self.nodes[0].gettransaction(txid1) | |||||
# listsinceblock(lastblockhash) should now include txid1 in transactions | |||||
# as well as in removed | |||||
lsbres = self.nodes[0].listsinceblock(lastblockhash) | |||||
assert any(tx['txid'] == txid1 for tx in lsbres['transactions']) | |||||
assert any(tx['txid'] == txid1 for tx in lsbres['removed']) | |||||
# find transaction and ensure confirmations is valid | |||||
for tx in lsbres['transactions']: | |||||
if tx['txid'] == txid1: | |||||
assert_equal(tx['confirmations'], 2) | |||||
# the same check for the removed array; confirmations should STILL be 2 | |||||
for tx in lsbres['removed']: | |||||
if tx['txid'] == txid1: | |||||
assert_equal(tx['confirmations'], 2) | |||||
if __name__ == '__main__': | if __name__ == '__main__': | ||||
ListSinceBlockTest().main() | ListSinceBlockTest().main() |