This implements a proposal in #12805 and is a rebase of #14533.
This seems to be a working approach, but I'm not sure why the p2p_segwit.py functional test needed a change, so I'll look into it more.
<!--e57a25ab6845829454e8d69fc972939a-->
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.
<!--174a7506f384e20aa4161008e828411d-->
No conflicts as of last run.
60 | @@ -61,7 +61,7 @@ const CBlockIndex *CChain::FindFork(const CBlockIndex *pindex) const { 61 | 62 | CBlockIndex* CChain::FindEarliestAtLeast(int64_t nTime, int height) const 63 | { 64 | - std::pair<int64_t, int> blockparams = std::make_pair(nTime, height); 65 | +2 std::pair<int64_t, int> blockparams = std::make_pair(nTime, height);
typo?
Yes. Fixed.
60 | @@ -61,6 +61,7 @@ void WalletInit::AddWalletOptions() const 61 | gArgs.AddArg("-wallet=<path>", "Specify wallet database path. Can be specified multiple times to load multiple wallets. Path is interpreted relative to <walletdir> if it is not absolute, and will be created if it does not exist (as a directory containing a wallet.dat file and log files). For backwards compatibility this will also accept names of existing data files in <walletdir>.)", ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::WALLET); 62 | gArgs.AddArg("-walletbroadcast", strprintf("Make the wallet broadcast transactions (default: %u)", DEFAULT_WALLETBROADCAST), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET); 63 | gArgs.AddArg("-walletdir=<dir>", "Specify directory to hold wallets (default: <datadir>/wallets if it exists, otherwise <datadir>)", ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::WALLET); 64 | + gArgs.AddArg("-walletcrosschain", strprintf("Allow reusing wallet files across chains (default: %u)", DEFAULT_WALLETCROSSCHAIN), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
Probably should be a debug option only?
Yes, debug options are for users who know what they are doing...
Understood. Should it also be in WALLET_DEBUG_TEST category?
3922 | + { 3923 | + // Wallet is assumed to be from another chain, if none of 3924 | + // its last 6 best known blocks are in the active chain 3925 | + // (this heavily relies on the fact that locator stores 3926 | + // last 10 blocks consecutively): 3927 | + const Optional<int> fork_height = chain.findLocatorFork(locator);
Please use std::optional in new code.
done
Looks like the CI picked up a memory leak running wallet_crosschain.py:
node0 stderr =================================================================
==39469==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 22096 byte(s) in 18 object(s) allocated from:
[#0](/bitcoin-bitcoin/0/) 0x556b3fed1e6d in malloc (/tmp/cirrus-ci-build/ci/scratch/build/bitcoin-x86_64-pc-linux-gnu/src/bitcoind+0x17b7e6d)
[#1](/bitcoin-bitcoin/1/) 0x7fd21ead48c4 in __os_malloc (/lib/x86_64-linux-gnu/libdb_cxx-5.3.so+0x1698c4)
Indirect leak of 2016 byte(s) in 18 object(s) allocated from:
[#0](/bitcoin-bitcoin/0/) 0x556b3fed1e6d in malloc (/tmp/cirrus-ci-build/ci/scratch/build/bitcoin-x86_64-pc-linux-gnu/src/bitcoind+0x17b7e6d)
[#1](/bitcoin-bitcoin/1/) 0x7fd21ead48c4 in __os_malloc (/lib/x86_64-linux-gnu/libdb_cxx-5.3.so+0x1698c4)
SUMMARY: AddressSanitizer: 24112 byte(s) leaked in 36 allocation(s).
2752 | + error = _("Wallet files should not be reused across chains."); 2753 | + return false; 2754 | + } 2755 | + } 2756 | + } 2757 | +
Had to move it here, since putting this after line 2766 where I placed it originally results in memory leaks, as detected by the sanitizer.
Concept ACK
This PR does not fix the issues that I was expecting it would. Have shared the details in https://github.com/bitcoin/bitcoin/issues/22792
2756 | + // Wallet is assumed to be from another chain, if none of 2757 | + // its last 6 best known blocks are in the active chain 2758 | + // (this heavily relies on the fact that locator stores 2759 | + // last 10 blocks consecutively): 2760 | + const std::optional<int> fork_height = chain.findLocatorFork(locator); 2761 | + if (fork_height && std::distance(locator.vHave.begin(),
This seems overly complicated. The last entry in the block locator is always the genesis block, so that can be used to compare to the expected chain's genesis block hash.
As discussed in the wallet meeting if we want to take into account altcoins that could share Bitcoin mainnet's genesis block we'd need more than just a genesis block comparison. Also stated here: #14533 (comment)
For descriptor wallets the case with altcoins is already covered by storing network magic.
My vote goes for simpler code and genesis block hash check.
As I understand there is an agreement to restore the simpler implementation I used to have in #14533, right?
@rodentrabies Yes
@rodentrabies Any update on this PR or is it up for grabs?
@prayank23, yeah, sorry about that, I'll change it to the simpler implementation as discussed above this week end.
I tested this today and tried to reproduce the issue with the steps mentioned in this issue: #22792
error code: -18
error message:
Wallet file verification failed. Failed to load database path '/mnt/testnet3/T1'. Data is not in recognized format.
One test is failing in CI: wallet_crosschain.py
I am not sure if this is introduced in this PR. Getting an error when trying to run bitcoind -regtest=1 -datadir=/path
EXCEPTION: St13runtime_error
Invalid combination of -regtest, -signet, -testnet and -chain. Can use at most one.
bitcoin in AppInit()
bitcoind: chainparamsbase.cpp:35: const CBaseChainParams& BaseParams(): Assertion `globalChainBaseParams' failed.
I was unable to reproduce the issue. However, I am getting a different error so I am not sure:
If that is a descriptor wallet, then that would be because the sqlite implementation (only used for descriptor wallets) includes a crosschain protection mechanism already. However legacy wallets don't, and this PR would allow for that.
One test is failing in CI: wallet_crosschain.py
Indeed. The test is incorrect because all regtest chains use the same genesis even if the rest of the blockchain is different. @rodentrabies You will need to change the test to start a node in testnet or mainnet mode (without syncing of course) in order to test the crosschain protection.
I am not sure if this is introduced in this PR. Getting an error when trying to run
bitcoind -regtest=1 -datadir=/path
I don't think that is related, you should double check your bitcoin.conf and make sure it does not specify a specific chain to use.
22 | + self.setup_nodes() 23 | + 24 | + def run_test(self): 25 | + self.log.info("Generating initial blockchains") 26 | + self.nodes[0].generate(5) 27 | + self.nodes[1].generate(5)
In ece5cd111660005ce71cb8a7092f96c3e084d975 "tests: add tests for cross-chain wallet use prevention"
self.generate(self.nodes[0], 5, sync_fun=self.no_op)
self.generate(self.nodes[1], 5, sync_fun=self.no_op)
Removed generation completely, as it doesn't seem necessary for the purposes of this test.
38 | + node_2_wallet = os.path.join(self.nodes[2].datadir, 'node_2_wallet') 39 | + self.nodes[2].createwallet(node_2_wallet) 40 | + self.nodes[2].unloadwallet(node_2_wallet) 41 | + 42 | + self.log.info("Loading wallets into nodes with a different genesis blocks") 43 | + assert_raises_rpc_error(-4, 'Wallet files should not be reused across chains.', self.nodes[0].loadwallet, node_1_wallet)
In ece5cd111660005ce71cb8a7092f96c3e084d975 "tests: add tests for cross-chain wallet use prevention"
This assertion is currently not raised. The two wallets are detected as being on the same chain because all regtest nodes use the same genesis block regardless of what comes after.
If that is a descriptor wallet, then that would be because the sqlite implementation (only used for descriptor wallets) includes a crosschain protection mechanism already. However legacy wallets don't, and this PR would allow for that.
I am not sure if there was some misunderstanding. Not able to reproduce issue in PR branch was a good thing but error was confusing. @rodentrabies its been couple of years and issue is still not fixed. There were some suggestions by @achow101 as well. You tried your best and I appreciate it however I think this can lead to some kind of vulnerability if not fixed soon. Last update was 3 months ago. Is it okay if someone else can work on this pull request?
@prayank23 I'm sorry for the slow progress on this. I'll work on wrapping this up today, if you don't mind. In fact I've spent 2 hours this weekend trying to make the functional test start both regtest and testnet nodes for me by playing with self.chain, but without success, so I'll just have to do that manually.
2917 | @@ -2918,6 +2918,23 @@ bool CWallet::AttachChain(const std::shared_ptr<CWallet>& walletInstance, interf 2918 | assert(!walletInstance->m_chain || walletInstance->m_chain == &chain); 2919 | walletInstance->m_chain = &chain; 2920 | 2921 | + // Unless allowed, ensure wallet files are not reused across chains: 2922 | + if (!gArgs.GetBoolArg("-walletcrosschain", DEFAULT_WALLETCROSSCHAIN))
Can you add a test for this option?
Done.
if (!gArgs.GetBoolArg("-walletcrosschain", DEFAULT_WALLETCROSSCHAIN)) {
nit: no newline after if
Done. Followed the style of several ifs on the lines below, but I see now that those are outliers.
Tried testing different things. Two tests worked as expected and 1 failed.
✅ Test 1 :
bitcoind with PR branchbitcoin-cli createwallet T1. This creates a descriptor wallet as shared by @achow101 in #18554 (comment) which I understood now after looking at PR #23002 and reading everything again.bitcoind for mainnet. This already has a protection so gives a different error: Wallet file verification failed. Failed to load database path '/home/prayank/.bitcoin/testnet3/wallets/T1'. Data is not in recognized format.✅ Test 2:
bitcoind with PR branchbitcoin-cli -named createwallet -wallet_name="T2" -descriptors=false. This creates a legacy wallet.bitcoind for mainnet. This gives an error: Wallet loading failed. Wallet files should not be reused across chains.❌ Test 3:
bitcoind with v22.0 binarybitcoin-cli createwallet T3.$ bitcoin-cli loadwallet "/home/prayank/.bitcoin/testnet3/wallets/T3"
{
"name": "/home/prayank/.bitcoin/testnet3/wallets/T3",
"warning": ""
}
Just tried this (test 3) and got expected behavior:
$ ./bitcoin-22.0/bin/bitcoind -testnet -maxconnections=0
...
$ ./bitcoin-22.0/bin/bitcoind-cli -testnet createwallet t3
...
# stop bitcoind
...
$ ./bitcoin/src/bitcoind -maxconnections=0
...
$ ./bitcoin/src/bitcoin-cli loadwallet ~/.bitcoin/testnet3/wallets/t3
error code: -4
error message:
Wallet loading failed. Wallet files should not be reused across chains.
Perhaps maxconnections somehow affects the condition batch.ReadBestBlock(locator) && locator.vHave.size() > 0 && chain.getHeight() at wallet.cpp:2926. I'll investigate.
EDIT: just tried the same without maxconnections (though before block header synchronization finishes on either of the node instances) and still see the expected behavior.
2927 | + { 2928 | + // Wallet is assumed to be from another chain, if genesis block in the active 2929 | + // chain is differs from the genesis block know to the wallet. 2930 | + if (chain.getBlockHash(0) != locator.vHave.back()) 2931 | + { 2932 | + error = _("Wallet files should not be reused across chains.");
should this mention the setting to disable the check? Also, could go with Untranslated()? It is not like this is something that happens in everyday operation?
Added a sentence mentioning -walletcrosschain and wrapped with Untranslated(...).
2924 | + WalletBatch batch(walletInstance->GetDatabase()); 2925 | + CBlockLocator locator; 2926 | + if (batch.ReadBestBlock(locator) && locator.vHave.size() > 0 && chain.getHeight()) 2927 | + { 2928 | + // Wallet is assumed to be from another chain, if genesis block in the active 2929 | + // chain is differs from the genesis block know to the wallet.
// chain differs from the genesis block known to the wallet.
Done.
ACK 5f213213cb17429353ef7ec3e97b185af06d236f
Just tried this (test 3) and got expected behavior:
Even I could not reproduce it and getting cross chain error now for a new wallet. Old wallet still loads in mainnet that was created on testnet. I have added the wallet files if that helps: https://github.com/prayank23/bitcoin/blob/test-wallets/T2.zip
Just tried this (test 3) and got expected behavior:
Even I could not reproduce it and getting cross chain error now for a new wallet. Old wallet still loads in mainnet that was created on testnet. I have added the wallet files if that helps: https://github.com/prayank23/bitcoin/blob/test-wallets/T2.zip
Are you sure you didn't just make a mistake when doing this test? The provided wallet file has a mainnet block locator record and it doesn't load in testnet with this PR.
Are you sure you didn't just make a mistake when doing this test? The provided wallet file has a mainnet block locator record and it doesn't load in testnet with this PR.
Right. Looks like I did something wrong. I think this PR fixes the issue and if I find anything else it can be done in a follow up PR. Its weird that this wallet T2 exists with other testnet wallets in testnet3 directory.
Code Review ACK 5f213213cb17429353ef7ec3e97b185af06d236f
Apropos of nothing: @rodentrabies has quite the fortitude, carrying this PR since 2018
@dongcarl to be frank, I didn't do a very good job of carrying it. Had to be reminded several times to rebase/update it :) Great it's finally merged!