Changeset View
Changeset View
Standalone View
Standalone View
test/functional/wallet_listsinceblock.py
Show All 37 Lines | def run_test(self): | ||||
self.test_invalid_blockhash() | self.test_invalid_blockhash() | ||||
self.test_reorg() | self.test_reorg() | ||||
self.test_double_spend() | self.test_double_spend() | ||||
self.test_double_send() | self.test_double_send() | ||||
self.test_targetconfirmations() | self.test_targetconfirmations() | ||||
def test_no_blockhash(self): | def test_no_blockhash(self): | ||||
self.log.info("Test no blockhash") | self.log.info("Test no blockhash") | ||||
txid = self.nodes[2].sendtoaddress( | txid = self.nodes[2].sendtoaddress(self.nodes[0].getnewaddress(), 1000000) | ||||
self.nodes[0].getnewaddress(), 1000000) | (blockhash,) = self.generate(self.nodes[2], 1) | ||||
blockhash, = self.generate(self.nodes[2], 1) | blockheight = self.nodes[2].getblockheader(blockhash)["height"] | ||||
blockheight = self.nodes[2].getblockheader(blockhash)['height'] | |||||
txs = self.nodes[0].listtransactions() | txs = self.nodes[0].listtransactions() | ||||
assert_array_result(txs, {"txid": txid}, { | assert_array_result( | ||||
txs, | |||||
{"txid": txid}, | |||||
{ | |||||
"category": "receive", | "category": "receive", | ||||
"amount": 1000000, | "amount": 1000000, | ||||
"blockhash": blockhash, | "blockhash": blockhash, | ||||
"blockheight": blockheight, | "blockheight": blockheight, | ||||
"confirmations": 1, | "confirmations": 1, | ||||
}) | }, | ||||
) | |||||
assert_equal( | assert_equal( | ||||
self.nodes[0].listsinceblock(), | self.nodes[0].listsinceblock(), | ||||
{"lastblock": blockhash, | {"lastblock": blockhash, "removed": [], "transactions": txs}, | ||||
"removed": [], | ) | ||||
"transactions": txs}) | |||||
assert_equal( | assert_equal( | ||||
self.nodes[0].listsinceblock(""), | self.nodes[0].listsinceblock(""), | ||||
{"lastblock": blockhash, | {"lastblock": blockhash, "removed": [], "transactions": txs}, | ||||
"removed": [], | ) | ||||
"transactions": txs}) | |||||
def test_invalid_blockhash(self): | def test_invalid_blockhash(self): | ||||
self.log.info("Test invalid blockhash") | self.log.info("Test invalid blockhash") | ||||
assert_raises_rpc_error(-5, "Block not found", self.nodes[0].listsinceblock, | assert_raises_rpc_error( | ||||
"42759cde25462784395a337460bde75f58e73d3f08bd31fdc3507cbac856a2c4") | -5, | ||||
assert_raises_rpc_error(-5, "Block not found", self.nodes[0].listsinceblock, | "Block not found", | ||||
"0000000000000000000000000000000000000000000000000000000000000000") | self.nodes[0].listsinceblock, | ||||
assert_raises_rpc_error(-8, "blockhash must be of length 64 (not 11, for 'invalid-hex')", self.nodes[0].listsinceblock, | "42759cde25462784395a337460bde75f58e73d3f08bd31fdc3507cbac856a2c4", | ||||
"invalid-hex") | ) | ||||
assert_raises_rpc_error(-8, "blockhash must be hexadecimal string (not 'Z000000000000000000000000000000000000000000000000000000000000000')", self.nodes[0].listsinceblock, | assert_raises_rpc_error( | ||||
"Z000000000000000000000000000000000000000000000000000000000000000") | -5, | ||||
"Block not found", | |||||
self.nodes[0].listsinceblock, | |||||
"0000000000000000000000000000000000000000000000000000000000000000", | |||||
) | |||||
assert_raises_rpc_error( | |||||
-8, | |||||
"blockhash must be of length 64 (not 11, for 'invalid-hex')", | |||||
self.nodes[0].listsinceblock, | |||||
"invalid-hex", | |||||
) | |||||
assert_raises_rpc_error( | |||||
-8, | |||||
( | |||||
"blockhash must be hexadecimal string (not" | |||||
" 'Z000000000000000000000000000000000000000000000000000000000000000')" | |||||
), | |||||
self.nodes[0].listsinceblock, | |||||
"Z000000000000000000000000000000000000000000000000000000000000000", | |||||
) | |||||
def test_targetconfirmations(self): | def test_targetconfirmations(self): | ||||
''' | """ | ||||
This tests when the value of target_confirmations exceeds the number of | This tests when the value of target_confirmations exceeds the number of | ||||
blocks in the main chain. In this case, the genesis block hash should be | blocks in the main chain. In this case, the genesis block hash should be | ||||
given for the `lastblock` property. If target_confirmations is < 1, then | given for the `lastblock` property. If target_confirmations is < 1, then | ||||
a -8 invalid parameter error is thrown. | a -8 invalid parameter error is thrown. | ||||
''' | """ | ||||
self.log.info("Test target_confirmations") | self.log.info("Test target_confirmations") | ||||
blockhash, = self.generate(self.nodes[2], 1) | (blockhash,) = self.generate(self.nodes[2], 1) | ||||
blockheight = self.nodes[2].getblockheader(blockhash)['height'] | blockheight = self.nodes[2].getblockheader(blockhash)["height"] | ||||
assert_equal( | assert_equal( | ||||
self.nodes[0].getblockhash(0), | self.nodes[0].getblockhash(0), | ||||
self.nodes[0].listsinceblock(blockhash, blockheight + 1)['lastblock']) | self.nodes[0].listsinceblock(blockhash, blockheight + 1)["lastblock"], | ||||
) | |||||
assert_equal( | assert_equal( | ||||
self.nodes[0].getblockhash(0), | self.nodes[0].getblockhash(0), | ||||
self.nodes[0].listsinceblock(blockhash, blockheight + 1000)['lastblock']) | self.nodes[0].listsinceblock(blockhash, blockheight + 1000)["lastblock"], | ||||
assert_raises_rpc_error(-8, "Invalid parameter", | ) | ||||
self.nodes[0].listsinceblock, blockhash, 0) | assert_raises_rpc_error( | ||||
-8, "Invalid parameter", self.nodes[0].listsinceblock, blockhash, 0 | |||||
) | |||||
def test_reorg(self): | 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 | ||||
| | | | | | ||||
aa2 bb2 | aa2 bb2 | ||||
Show All 9 Lines | def test_reorg(self): | ||||
Previously: listsinceblock would find height=4 for block aa3 and compare | Previously: listsinceblock would find height=4 for block aa3 and compare | ||||
this to height=5 for the tip of the chain (bb4). It would then return | this to height=5 for the tip of the chain (bb4). It would then return | ||||
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.log.info("Test reorg") | self.log.info("Test reorg") | ||||
# 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( | senttx = self.nodes[2].sendtoaddress(self.nodes[0].getnewaddress(), 1000000) | ||||
self.nodes[0].getnewaddress(), 1000000) | |||||
# generate on both sides | # generate on both sides | ||||
nodes1_last_blockhash = self.generate( | nodes1_last_blockhash = self.generate( | ||||
self.nodes[1], 6, sync_fun=lambda: self.sync_all(self.nodes[:2]))[-1] | self.nodes[1], 6, sync_fun=lambda: self.sync_all(self.nodes[:2]) | ||||
)[-1] | |||||
nodes2_first_blockhash = self.generate( | nodes2_first_blockhash = self.generate( | ||||
self.nodes[2], 7, sync_fun=lambda: self.sync_all(self.nodes[2:]))[0] | self.nodes[2], 7, sync_fun=lambda: self.sync_all(self.nodes[2:]) | ||||
self.log.debug( | )[0] | ||||
f"nodes[1] last blockhash = {nodes1_last_blockhash}") | self.log.debug(f"nodes[1] last blockhash = {nodes1_last_blockhash}") | ||||
self.log.debug( | self.log.debug(f"nodes[2] first blockhash = {nodes2_first_blockhash}") | ||||
f"nodes[2] first blockhash = {nodes2_first_blockhash}") | |||||
self.join_network() | self.join_network() | ||||
# listsinceblock(nodes1_last_blockhash) should now include tx as seen from nodes[0] | # listsinceblock(nodes1_last_blockhash) should now include tx as seen from nodes[0] | ||||
# and return the block height which listsinceblock now exposes since | # and return the block height which listsinceblock now exposes since | ||||
# rABC6098a1cb2b25. | # rABC6098a1cb2b25. | ||||
transactions = self.nodes[0].listsinceblock( | transactions = self.nodes[0].listsinceblock(nodes1_last_blockhash)[ | ||||
nodes1_last_blockhash)['transactions'] | "transactions" | ||||
found = next(tx for tx in transactions if tx['txid'] == senttx) | ] | ||||
found = next(tx for tx in transactions if tx["txid"] == senttx) | |||||
assert_equal( | assert_equal( | ||||
found['blockheight'], | found["blockheight"], | ||||
self.nodes[0].getblockheader(nodes2_first_blockhash)['height']) | self.nodes[0].getblockheader(nodes2_first_blockhash)["height"], | ||||
) | |||||
def test_double_spend(self): | def test_double_spend(self): | ||||
''' | """ | ||||
This tests the case where the same UTXO is spent twice on two separate | This tests the case where the same UTXO is spent twice on two separate | ||||
blocks as part of a reorg. | blocks as part of a reorg. | ||||
ab0 | ab0 | ||||
/ \ | / \ | ||||
aa1 [tx1] bb1 [tx2] | aa1 [tx1] bb1 [tx2] | ||||
| | | | | | ||||
aa2 bb2 | aa2 bb2 | ||||
Show All 10 Lines | def test_double_spend(self): | ||||
4. Reorg into bb chain. | 4. Reorg into bb chain. | ||||
5. User 1 asks `listsinceblock aa3` and does not see that tx1 is now | 5. User 1 asks `listsinceblock aa3` and does not see that tx1 is now | ||||
invalidated. | invalidated. | ||||
Currently the solution to this is to detect that a reorg'd block is | 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 | 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 | until the fork point, and to include all transactions that relate to the | ||||
node wallet. | node wallet. | ||||
''' | """ | ||||
self.log.info("Test double spend") | self.log.info("Test double spend") | ||||
self.sync_all() | self.sync_all() | ||||
# share utxo between nodes[1] and nodes[2] | # share utxo between nodes[1] and nodes[2] | ||||
eckey = ECKey() | eckey = ECKey() | ||||
eckey.generate() | eckey.generate() | ||||
privkey = bytes_to_wif(eckey.get_bytes()) | privkey = bytes_to_wif(eckey.get_bytes()) | ||||
address = cashaddr.encode_full("ecregtest", cashaddr.PUBKEY_TYPE, | address = cashaddr.encode_full( | ||||
hash160(eckey.get_pubkey().get_bytes())) | "ecregtest", cashaddr.PUBKEY_TYPE, hash160(eckey.get_pubkey().get_bytes()) | ||||
) | |||||
self.nodes[2].sendtoaddress(address, 10_000_000) | self.nodes[2].sendtoaddress(address, 10_000_000) | ||||
self.generate(self.nodes[2], 6) | self.generate(self.nodes[2], 6) | ||||
self.nodes[2].importprivkey(privkey) | self.nodes[2].importprivkey(privkey) | ||||
utxos = self.nodes[2].listunspent() | utxos = self.nodes[2].listunspent() | ||||
utxo = [u for u in utxos if u["address"] == address][0] | utxo = [u for u in utxos if u["address"] == address][0] | ||||
self.nodes[1].importprivkey(privkey) | self.nodes[1].importprivkey(privkey) | ||||
# Split network into two | # Split network into two | ||||
self.split_network() | self.split_network() | ||||
# send from nodes[1] using utxo to nodes[0] | # send from nodes[1] using utxo to nodes[0] | ||||
change = f"{float(utxo['amount']) - 1000300.0:.2f}" | change = f"{float(utxo['amount']) - 1000300.0:.2f}" | ||||
recipient_dict = { | recipient_dict = { | ||||
self.nodes[0].getnewaddress(): 1000000, | self.nodes[0].getnewaddress(): 1000000, | ||||
self.nodes[1].getnewaddress(): change, | self.nodes[1].getnewaddress(): change, | ||||
} | } | ||||
utxo_dicts = [{ | utxo_dicts = [ | ||||
'txid': utxo['txid'], | { | ||||
'vout': utxo['vout'], | "txid": utxo["txid"], | ||||
}] | "vout": utxo["vout"], | ||||
} | |||||
] | |||||
txid1 = self.nodes[1].sendrawtransaction( | txid1 = self.nodes[1].sendrawtransaction( | ||||
self.nodes[1].signrawtransactionwithwallet( | self.nodes[1].signrawtransactionwithwallet( | ||||
self.nodes[1].createrawtransaction(utxo_dicts, recipient_dict))['hex']) | self.nodes[1].createrawtransaction(utxo_dicts, recipient_dict) | ||||
)["hex"] | |||||
) | |||||
# send from nodes[2] using utxo to nodes[3] | # send from nodes[2] using utxo to nodes[3] | ||||
recipient_dict2 = { | recipient_dict2 = { | ||||
self.nodes[3].getnewaddress(): 1000000, | self.nodes[3].getnewaddress(): 1000000, | ||||
self.nodes[2].getnewaddress(): change, | self.nodes[2].getnewaddress(): change, | ||||
} | } | ||||
self.nodes[2].sendrawtransaction( | self.nodes[2].sendrawtransaction( | ||||
self.nodes[2].signrawtransactionwithwallet( | self.nodes[2].signrawtransactionwithwallet( | ||||
self.nodes[2].createrawtransaction(utxo_dicts, recipient_dict2))['hex']) | self.nodes[2].createrawtransaction(utxo_dicts, recipient_dict2) | ||||
)["hex"] | |||||
) | |||||
# generate on both sides | # generate on both sides | ||||
lastblockhash = self.generate(self.nodes[1], 3, sync_fun=self.no_op)[2] | lastblockhash = self.generate(self.nodes[1], 3, sync_fun=self.no_op)[2] | ||||
self.generate(self.nodes[2], 4, sync_fun=self.no_op) | self.generate(self.nodes[2], 4, sync_fun=self.no_op) | ||||
self.join_network() | self.join_network() | ||||
self.sync_all() | self.sync_all() | ||||
# gettransaction should work for txid1 | # gettransaction should work for txid1 | ||||
assert self.nodes[0].gettransaction( | assert ( | ||||
txid1)['txid'] == txid1, "gettransaction failed to find txid1" | self.nodes[0].gettransaction(txid1)["txid"] == txid1 | ||||
), "gettransaction failed to find txid1" | |||||
# listsinceblock(lastblockhash) should now include txid1, as seen from | # listsinceblock(lastblockhash) should now include txid1, as seen from | ||||
# nodes[0] | # nodes[0] | ||||
lsbres = self.nodes[0].listsinceblock(lastblockhash) | lsbres = self.nodes[0].listsinceblock(lastblockhash) | ||||
assert any(tx['txid'] == txid1 for tx in lsbres['removed']) | assert any(tx["txid"] == txid1 for tx in lsbres["removed"]) | ||||
# but it should not include 'removed' if include_removed=false | # but it should not include 'removed' if include_removed=false | ||||
lsbres2 = self.nodes[0].listsinceblock( | lsbres2 = self.nodes[0].listsinceblock( | ||||
blockhash=lastblockhash, include_removed=False) | blockhash=lastblockhash, include_removed=False | ||||
assert 'removed' not in lsbres2 | ) | ||||
assert "removed" not in lsbres2 | |||||
def test_double_send(self): | def test_double_send(self): | ||||
''' | """ | ||||
This tests the case where the same transaction is submitted twice on two | 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 | separate blocks as part of a reorg. The former will vanish and the | ||||
latter will appear as the true transaction (with confirmations dropping | latter will appear as the true transaction (with confirmations dropping | ||||
as a result). | as a result). | ||||
ab0 | ab0 | ||||
/ \ | / \ | ||||
aa1 [tx1] bb1 | aa1 [tx1] bb1 | ||||
| | | | | | ||||
aa2 bb2 | aa2 bb2 | ||||
| | | | | | ||||
aa3 bb3 [tx1] | aa3 bb3 [tx1] | ||||
| | | | ||||
bb4 | bb4 | ||||
Asserted: | Asserted: | ||||
1. tx1 is listed in listsinceblock. | 1. tx1 is listed in listsinceblock. | ||||
2. It is included in 'removed' as it was removed, even though it is now | 2. It is included in 'removed' as it was removed, even though it is now | ||||
present in a different block. | present in a different block. | ||||
3. It is listed with a confirmation count of 2 (bb3, bb4), not | 3. It is listed with a confirmation count of 2 (bb3, bb4), not | ||||
3 (aa1, aa2, aa3). | 3 (aa1, aa2, aa3). | ||||
''' | """ | ||||
self.log.info("Test double send") | self.log.info("Test double send") | ||||
self.sync_all() | self.sync_all() | ||||
# Split network into two | # Split network into two | ||||
self.split_network() | self.split_network() | ||||
# create and sign a transaction | # create and sign a transaction | ||||
utxos = self.nodes[2].listunspent() | utxos = self.nodes[2].listunspent() | ||||
utxo = utxos[0] | utxo = utxos[0] | ||||
change = f"{float(utxo['amount']) - 1000300.0:.2f}" | change = f"{float(utxo['amount']) - 1000300.0:.2f}" | ||||
recipient_dict = { | recipient_dict = { | ||||
self.nodes[0].getnewaddress(): 1000000, | self.nodes[0].getnewaddress(): 1000000, | ||||
self.nodes[2].getnewaddress(): change, | self.nodes[2].getnewaddress(): change, | ||||
} | } | ||||
utxo_dicts = [{ | utxo_dicts = [ | ||||
'txid': utxo['txid'], | { | ||||
'vout': utxo['vout'], | "txid": utxo["txid"], | ||||
}] | "vout": utxo["vout"], | ||||
} | |||||
] | |||||
signedtxres = self.nodes[2].signrawtransactionwithwallet( | signedtxres = self.nodes[2].signrawtransactionwithwallet( | ||||
self.nodes[2].createrawtransaction(utxo_dicts, recipient_dict)) | self.nodes[2].createrawtransaction(utxo_dicts, recipient_dict) | ||||
assert signedtxres['complete'] | ) | ||||
assert signedtxres["complete"] | |||||
signedtx = signedtxres['hex'] | signedtx = signedtxres["hex"] | ||||
# send from nodes[1]; this will end up in aa1 | # send from nodes[1]; this will end up in aa1 | ||||
txid1 = self.nodes[1].sendrawtransaction(signedtx) | txid1 = self.nodes[1].sendrawtransaction(signedtx) | ||||
# generate bb1-bb2 on right side | # generate bb1-bb2 on right side | ||||
self.generate(self.nodes[2], 2, sync_fun=self.no_op) | self.generate(self.nodes[2], 2, sync_fun=self.no_op) | ||||
# send from nodes[2]; this will end up in bb3 | # send from nodes[2]; this will end up in bb3 | ||||
txid2 = self.nodes[2].sendrawtransaction(signedtx) | txid2 = self.nodes[2].sendrawtransaction(signedtx) | ||||
assert_equal(txid1, txid2) | assert_equal(txid1, txid2) | ||||
# generate on both sides | # generate on both sides | ||||
lastblockhash = self.generate(self.nodes[1], 3, sync_fun=self.no_op)[2] | lastblockhash = self.generate(self.nodes[1], 3, sync_fun=self.no_op)[2] | ||||
self.generate(self.nodes[2], 2, sync_fun=self.no_op) | self.generate(self.nodes[2], 2, sync_fun=self.no_op) | ||||
self.join_network() | self.join_network() | ||||
self.sync_all() | self.sync_all() | ||||
# gettransaction should work for txid1 | # gettransaction should work for txid1 | ||||
tx1 = self.nodes[0].gettransaction(txid1) | tx1 = self.nodes[0].gettransaction(txid1) | ||||
assert_equal( | assert_equal( | ||||
tx1['blockheight'], | tx1["blockheight"], self.nodes[0].getblockheader(tx1["blockhash"])["height"] | ||||
self.nodes[0].getblockheader( | ) | ||||
tx1['blockhash'])['height']) | |||||
# listsinceblock(lastblockhash) should now include txid1 in transactions | # listsinceblock(lastblockhash) should now include txid1 in transactions | ||||
# as well as in removed | # as well as in removed | ||||
lsbres = self.nodes[0].listsinceblock(lastblockhash) | 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["transactions"]) | ||||
assert any(tx['txid'] == txid1 for tx in lsbres['removed']) | assert any(tx["txid"] == txid1 for tx in lsbres["removed"]) | ||||
# find transaction and ensure confirmations is valid | # find transaction and ensure confirmations is valid | ||||
for tx in lsbres['transactions']: | for tx in lsbres["transactions"]: | ||||
if tx['txid'] == txid1: | if tx["txid"] == txid1: | ||||
assert_equal(tx['confirmations'], 2) | assert_equal(tx["confirmations"], 2) | ||||
# the same check for the removed array; confirmations should STILL be 2 | # the same check for the removed array; confirmations should STILL be 2 | ||||
for tx in lsbres['removed']: | for tx in lsbres["removed"]: | ||||
if tx['txid'] == txid1: | if tx["txid"] == txid1: | ||||
assert_equal(tx['confirmations'], 2) | assert_equal(tx["confirmations"], 2) | ||||
if __name__ == '__main__': | if __name__ == "__main__": | ||||
ListSinceBlockTest().main() | ListSinceBlockTest().main() |