[rpc] listsinceblock should include lost transactions when parameter is a reorg'd block #9622
Merged
laanwj
merged 2 commits into
bitcoin:master
from
kallewoof:listsinceblock-include-lost-txs
Jul 24, 2017
Commits
Jump to file or symbol
Failed to load files and symbols.
| @@ -14,7 +14,15 @@ def __init__(self): | ||
| self.setup_clean_chain = True | ||
| self.num_nodes = 4 | ||
| - 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 | ||
| no longer in the main chain: | ||
| @@ -43,14 +51,6 @@ def run_test (self): | ||
| 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 | ||
| self.split_network() | ||
| @@ -73,7 +73,177 @@ def run_test (self): | ||
| if tx['txid'] == senttx: | ||
| found = True | ||
| 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 BTC in tx1 from utxo1 in block aa1. | ||
| + 2. User 2 receives BTC 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' % (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( | ||
jnewbery
Member
|
||
| + 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' % (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']: | ||
TheBlueMatt
Contributor
|
||
| + 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__': | ||
| ListSinceBlockTest().main() | ||
Can we add something to note that transactions which were re-added are included here anyway, and may have, at that point, positive confirmations value in this array?