Repository: bitcoin/bitcoin
File: test/functional/wallet_listsinceblock.py (Python)

Code before:
        transactions = self.nodes[0].listsinceblock(nodes1_last_blockhash)['transactions']
        found = next(tx for tx in transactions if tx['txid'] == senttx)
        assert_equal(found['blockheight'], self.nodes[0].getblockheader(nodes2_first_blockhash)['height'])

    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.log.info("Test double spend")

        self.sync_all()

        # share utxo between nodes[1] and nodes[2]
        privkey, pubkey = generate_keypair(wif=True)
        address = key_to_p2wpkh(pubkey)
        self.nodes[2].sendtoaddress(address, 10)
        self.generate(self.nodes[2], 6)
        self.nodes[2].importprivkey(privkey)
        utxos = self.nodes[2].listunspent()
        utxo = [u for u in utxos if u["address"] == address][0]
        self.nodes[1].importprivkey(privkey)

        # Split network into two
        self.split_network()

        # send from nodes[1] using utxo to nodes[0]

Review comment (bug):
re. this comment https://github.com/bitcoin/bitcoin/pull/30195#discussion_r1630237777 and needing to use the full path again, I think it is that `rename(...)` returns a new `Path` instance, which is then the 'correct' one to use going forwards for further changes you want to happen on-disk (alas Python has no 'use of moved value' concept like Rust et al. that could've pointed this out).

I think the way to think of this is that a `Path` is just a *path*, not a file-handle, so you can have a Path instance that doesn't exist, because for all Python knows you might be wanting to create it -- so `.rename(...)` on a Path that doesn't exist changes the *path instance* but of-course can't change the actual filesystem because it doesn't exist.

```python
>>> from pathlib import Path
>>> # Make a Path and rename it to add `".copy"` on the end:
>>> p = Path("README.md")
>>> p.exists()
True
>>> q = p.rename("README.md.copy")  # capture the return value in `q`
>>>
>>> # We still have the Path instance called `p`...
>>> p
PosixPath('README.md')
>>> # ...but it doesn't exist anymore (becuase we just renamed it)
>>> p.exists()
False
>>> # `.rename` returned a new Path instance, which *does* exist
>>> q
PosixPath('README.md.copy')
>>> q.exists()
True
>>>
>>> # Just to prove the rename happened: 
>>> [item.name for item in Path(".").iterdir() if "md" in item.name]
['README.md.copy']
>>>
>>> # Rename again to move it back to the original name...
>>> r = q.rename("README.md")
>>> r
PosixPath('README.md')
>>> r.exists()
True
>>> # Because `p` and `r` now have the same name, the original `p` also 'exists' again:
>>> p.exists(), q.exists(), r.exists()
(True, False, True)
```

*So:*

```suggestion
        # Renaming the block file to induce unsuccessful block read
        blk_dat = (self.nodes[0].blocks_path / "blk00000.dat")
        assert blk_dat.exists()
        blk_dat_moved = blk_dat.rename("blk00000.dat.moved")
        assert not blk_dat.exists()

        # listsinceblock(nodes1_last_blockhash) should now fail as blocks are not accessible
        assert_raises_rpc_error(-32603, "Can't read block from disk",
            self.nodes[0].listsinceblock, nodes1_last_blockhash)

        # Restoring block file
        blk_dat_moved.rename("blk00000.dat")
        assert blk_dat.exists()
```

Also suggest naming change to "moved" or "backup" instead of "copy" as you're not copying?

And additional `assert`s to check that the files exist and don't when expected (maybe overkill, but prevents unexpected pass/fails for some other reason than you renaming the file)

Code after:
        # and return the block height which listsinceblock now exposes since a5e7795.
        transactions = self.nodes[0].listsinceblock(nodes1_last_blockhash)['transactions']
        found = next(tx for tx in transactions if tx['txid'] == senttx)
        assert_equal(found['blockheight'], self.nodes[0].getblockheader(nodes2_first_blockhash)['height'])

    def test_cant_read_block(self):
        self.log.info('Test the RPC error "Can\'t read block from disk"')

        # Split network into two
        self.split_network()

        # generate on both sides
        nodes1_last_blockhash = self.generate(self.nodes[1], 6, sync_fun=lambda: self.sync_all(self.nodes[:2]))[-1]
        self.generate(self.nodes[2], 7, sync_fun=lambda: self.sync_all(self.nodes[2:]))[0]

        self.join_network()

        # Renaming the block file to induce unsuccessful block read
        blk_dat = (self.nodes[0].blocks_path / "blk00000.dat")
        blk_dat_moved = blk_dat.rename(self.nodes[0].blocks_path / "blk00000.dat.moved")
        assert not blk_dat.exists()

        # listsinceblock(nodes1_last_blockhash) should now fail as blocks are not accessible
        assert_raises_rpc_error(-32603, "Can't read block from disk",
            self.nodes[0].listsinceblock, nodes1_last_blockhash)

        # Restoring block file
        blk_dat_moved.rename(self.nodes[0].blocks_path / "blk00000.dat")
        assert blk_dat.exists()

    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.