diff options
173 files changed, 2869 insertions, 1431 deletions
diff --git a/.cirrus.yml b/.cirrus.yml index 26bd27754f..fe75403261 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -21,16 +21,15 @@ persistent_worker_template: &PERSISTENT_WORKER_TEMPLATE base_template: &BASE_TEMPLATE skip: $CIRRUS_REPO_FULL_NAME == "bitcoin-core/gui" && $CIRRUS_PR == "" # No need to run on the read-only mirror, unless it is a PR. https://cirrus-ci.org/guide/writing-tasks/#conditional-task-execution merge_base_script: - - if [ "$CIRRUS_PR" = "" ]; then exit 0; fi - bash -c "$PACKAGE_MANAGER_INSTALL git" + - if [ "$CIRRUS_PR" = "" ]; then exit 0; fi - git fetch $CIRRUS_REPO_CLONE_URL $CIRRUS_BASE_BRANCH - git config --global user.email "ci@ci.ci" - git config --global user.name "ci" - git merge FETCH_HEAD # Merge base to detect silent merge conflicts stateful: false # https://cirrus-ci.org/guide/writing-tasks/#stateful-tasks -global_task_template: &GLOBAL_TASK_TEMPLATE - << : *BASE_TEMPLATE +main_template: &MAIN_TEMPLATE timeout_in: 120m # https://cirrus-ci.org/faq/#instance-timed-out container: # https://cirrus-ci.org/faq/#are-there-any-limits @@ -41,9 +40,14 @@ global_task_template: &GLOBAL_TASK_TEMPLATE folder: "/tmp/ccache_dir" depends_built_cache: folder: "depends/built" + fingerprint_script: echo $CIRRUS_TASK_NAME $(git rev-list -1 HEAD ./depends) ci_script: - ./ci/test_run_all.sh +global_task_template: &GLOBAL_TASK_TEMPLATE + << : *BASE_TEMPLATE + << : *MAIN_TEMPLATE + depends_sdk_cache_template: &DEPENDS_SDK_CACHE_TEMPLATE depends_sdk_cache: folder: "depends/sdk-sources" @@ -86,11 +90,14 @@ task: task: name: 'ARM [unit tests, no functional tests] [buster]' << : *GLOBAL_TASK_TEMPLATE - container: + arm_container: image: debian:buster + cpu: 2 + memory: 8G env: << : *CIRRUS_EPHEMERAL_WORKER_TEMPLATE_ENV FILE_ENV: "./ci/test/00_setup_env_arm.sh" + QEMU_USER_CMD: "" # Disable qemu and run the test natively task: name: 'Win64 [unit tests, no gui tests, no boost::process, no functional tests] [focal]' @@ -153,6 +160,7 @@ task: task: name: '[no depends, sanitizers: fuzzer,address,undefined,integer] [focal]' + only_if: $CIRRUS_BRANCH == $CIRRUS_DEFAULT_BRANCH || $CIRRUS_BASE_BRANCH == $CIRRUS_DEFAULT_BRANCH << : *GLOBAL_TASK_TEMPLATE container: image: ubuntu:focal @@ -211,9 +219,11 @@ task: task: name: 'ARM64 Android APK [focal]' << : *DEPENDS_SDK_CACHE_TEMPLATE + << : *BASE_TEMPLATE depends_sources_cache: folder: "depends/sources" - << : *GLOBAL_TASK_TEMPLATE + fingerprint_script: git rev-list -1 HEAD ./depends + << : *MAIN_TEMPLATE container: image: ubuntu:focal env: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5cd4715ef0..e0d3671b07 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -81,7 +81,7 @@ facilitates social contribution, easy testing and peer review. To contribute a patch, the workflow is as follows: - 1. Fork repository ([only for the first time](https://help.github.com/en/articles/fork-a-repo)) + 1. Fork repository ([only for the first time](https://docs.github.com/en/get-started/quickstart/fork-a-repo)) 1. Create topic branch 1. Commit patches @@ -182,7 +182,7 @@ for more information on helping with translations. ### Work in Progress Changes and Requests for Comments If a pull request is not to be considered for merging (yet), please -prefix the title with [WIP] or use [Tasks Lists](https://help.github.com/articles/basic-writing-and-formatting-syntax/#task-lists) +prefix the title with [WIP] or use [Tasks Lists](https://docs.github.com/en/github/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#task-lists) in the body of the pull request to indicate tasks are pending. ### Address Feedback @@ -386,7 +386,7 @@ about: - It may be because your code is too complex for all but a few people, and those people may not have realized your pull request even exists. A great way to find people who are qualified and care about the code you are touching is the - [Git Blame feature](https://help.github.com/articles/tracing-changes-in-a-file/). Simply + [Git Blame feature](https://docs.github.com/en/github/managing-files-in-a-repository/managing-files-on-github/tracking-changes-in-a-file). Simply look up who last modified the code you are changing and see if you can find them and give them a nudge. Don't be incessant about the nudging, though. - Finally, if all else fails, ask on IRC or elsewhere for someone to give your pull request diff --git a/ci/lint/06_script.sh b/ci/lint/06_script.sh index e38cfe8eef..c3c7619ef7 100755 --- a/ci/lint/06_script.sh +++ b/ci/lint/06_script.sh @@ -25,7 +25,7 @@ test/lint/lint-all.sh if [ "$CIRRUS_REPO_FULL_NAME" = "bitcoin/bitcoin" ] && [ -n "$CIRRUS_CRON" ]; then git log --merges --before="2 days ago" -1 --format='%H' > ./contrib/verify-commits/trusted-sha512-root-commit - ${CI_RETRY_EXE} gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys $(<contrib/verify-commits/trusted-keys) && + ${CI_RETRY_EXE} gpg --keyserver hkps://keys.openpgp.org --recv-keys $(<contrib/verify-commits/trusted-keys) && ./contrib/verify-commits/verify-commits.py --clean-merge=2; fi diff --git a/contrib/builder-keys/README.md b/contrib/builder-keys/README.md index a7c1d5ae0a..56bd87d0af 100644 --- a/contrib/builder-keys/README.md +++ b/contrib/builder-keys/README.md @@ -20,7 +20,7 @@ To fetch keys of builders and active developers, feed the list of fingerprints of the primary keys into gpg: ```sh -while read fingerprint keyholder_name; do gpg --keyserver hkp://subset.pool.sks-keyservers.net --recv-keys ${fingerprint}; done < ./keys.txt +while read fingerprint keyholder_name; do gpg --keyserver hkps://keys.openpgp.org --recv-keys ${fingerprint}; done < ./keys.txt ``` Add your key to the list if you provided Guix attestations for two major or diff --git a/contrib/builder-keys/keys.txt b/contrib/builder-keys/keys.txt index db28cd07a0..890406c745 100644 --- a/contrib/builder-keys/keys.txt +++ b/contrib/builder-keys/keys.txt @@ -5,6 +5,7 @@ E944AE667CF960B1004BC32FCA662BE18B877A60 Andreas Schildbach (aschildbach) 590B7292695AFFA5B672CBB2E13FC145CD3F4304 Antoine Poinsot (darosior) 0AD83877C1F0CD1EE9BD660AD7CC770B81FD22A8 Ben Carman (benthecarman) 912FD3228387123DC97E0E57D5566241A0295FA9 BtcDrak (btcdrak) +04017A2A6D9A0CCDC81D8EC296AB007F1A7ED999 Carl Dong (dongcarl) C519EBCF3B926298946783EFF6430754120EC2F4 Christian Decker (cdecker) 18AE2F798E0D239755DA4FD24B79F986CBDF8736 Chun Kuan Le (ken2812221) 101598DC823C1B5F9A6624ABA5E0907A0380E6C3 CoinForensics (CoinForensics) @@ -19,6 +20,7 @@ D35176BE9264832E4ACA8986BF0792FBE95DC863 fivepiece (fivepiece) 01CDF4627A3B88AAE4A571C87588242FBE38D3A8 Gavin Andresen (gavinandresen) D1DBF2C4B96F2DEBF4C16654410108112E7EA81F Hennadii Stepanov (hebasto) A2FD494D0021AA9B4FA58F759102B7AE654A4A5A Ilyas Ridhuan (IlyasRidhuan) +2688F5A9A4BE0F295E921E8A25F27A38A47AD566 James O'Beirne (jamesob) D3F22A3A4C366C2DCB66D3722DA9C5A7FA81EA35 Jarol Rodriguez (jarolrod) 7480909378D544EA6B6DCEB7535B12980BB8A4D3 Jeffri H Frontz (jhfrontz) D3CC177286005BB8FF673294C5242A1AB3936517 jl2012 (jl2012) diff --git a/contrib/devtools/test-symbol-check.py b/contrib/devtools/test-symbol-check.py index 7d83c5f751..2da7ae793d 100755 --- a/contrib/devtools/test-symbol-check.py +++ b/contrib/devtools/test-symbol-check.py @@ -73,20 +73,21 @@ class TestSymbolChecks(unittest.TestCase): (1, executable + ': NEEDED library libutil.so.1 is not allowed\n' + executable + ': failed LIBRARY_DEPENDENCIES')) - # finally, check a conforming file that simply uses a math function + # finally, check a simple conforming binary source = 'test3.c' executable = 'test3' with open(source, 'w', encoding="utf8") as f: f.write(''' - #include <math.h> + #include <stdio.h> int main() { - return (int)pow(2.0, 4.0); + printf("42"); + return 0; } ''') - self.assertEqual(call_symbol_check(cc, source, executable, ['-lm']), + self.assertEqual(call_symbol_check(cc, source, executable, []), (0, '')) def test_MACHO(self): diff --git a/contrib/guix/guix-attest b/contrib/guix/guix-attest index dcf709b542..6e12cbead7 100755 --- a/contrib/guix/guix-attest +++ b/contrib/guix/guix-attest @@ -159,23 +159,21 @@ Hint: You may wish to remove the existing attestations and their signatures by EOF } -# Given a document with unix line endings (just <LF>) in stdin, make all lines -# end in <CR><LF> and make sure there's no trailing <LF> at the end of the file. -# -# This is necessary as cleartext signatures are calculated on text after their -# line endings are canonicalized. +echo "Attesting to build outputs for version: '${VERSION}'" +echo "" + +# Given a SHA256SUMS file as stdin that has lines like: +# 0ba536819b221a91d3d42e978be016aac918f40984754d74058aa0c921cd3ea6 a/b/d/c/d/s/bitcoin-22.0rc2-riscv64-linux-gnu.tar.gz +# ... # -# For more information: -# 1. https://security.stackexchange.com/a/104261 -# 2. https://datatracker.ietf.org/doc/html/rfc4880#section-7.1 +# Replace each line's file name with its basename: +# 0ba536819b221a91d3d42e978be016aac918f40984754d74058aa0c921cd3ea6 bitcoin-22.0rc2-riscv64-linux-gnu.tar.gz +# ... # -rfc4880_normalize_document() { - sed 's/$/\r/' | head -c -2 +basenameify_SHA256SUMS() { + sed -E 's@(^[[:xdigit:]]{64}[[:space:]]+).+/([^/]+$)@\1\2@' } -echo "Attesting to build outputs for version: '${VERSION}'" -echo "" - outsigdir="$GUIX_SIGS_REPO/$VERSION/$signer_name" mkdir -p "$outsigdir" ( @@ -188,7 +186,7 @@ mkdir -p "$outsigdir" cat "${noncodesigned_fragments[@]}" \ | sort -u \ | sort -k2 \ - | rfc4880_normalize_document \ + | basenameify_SHA256SUMS \ > "$temp_noncodesigned" if [ -e noncodesigned.SHA256SUMS ]; then # The SHA256SUMS already exists, make sure it's exactly what we @@ -216,7 +214,7 @@ mkdir -p "$outsigdir" cat "${sha256sum_fragments[@]}" \ | sort -u \ | sort -k2 \ - | rfc4880_normalize_document \ + | basenameify_SHA256SUMS \ > "$temp_all" if [ -e all.SHA256SUMS ]; then # The SHA256SUMS already exists, make sure it's exactly what we diff --git a/contrib/guix/guix-verify b/contrib/guix/guix-verify index e4863f115b..02ae022741 100755 --- a/contrib/guix/guix-verify +++ b/contrib/guix/guix-verify @@ -77,11 +77,13 @@ verify() { echo "" echo "Hint: Either the signature is invalid or the public key is missing" echo "" + failure=1 elif ! diff --report-identical "$compare_manifest" "$current_manifest" 1>&2; then echo "ERR: The SHA256SUMS attestation in these two directories differ:" echo " '${compare_manifest}'" echo " '${current_manifest}'" echo "" + failure=1 else echo "Verified: '${current_manifest}'" echo "" @@ -166,3 +168,7 @@ if (( ${#all_noncodesigned[@]} + ${#all_all[@]} == 0 )); then echo "" exit 1 fi + +if [ -n "$failure" ]; then + exit 1 +fi diff --git a/contrib/linearize/linearize-data.py b/contrib/linearize/linearize-data.py index 73f54cd488..9a8bcc57a5 100755 --- a/contrib/linearize/linearize-data.py +++ b/contrib/linearize/linearize-data.py @@ -17,7 +17,6 @@ import datetime import time import glob from collections import namedtuple -from binascii import unhexlify settings = {} @@ -332,7 +331,7 @@ if __name__ == '__main__': settings['max_out_sz'] = int(settings['max_out_sz']) settings['split_timestamp'] = int(settings['split_timestamp']) settings['file_timestamp'] = int(settings['file_timestamp']) - settings['netmagic'] = unhexlify(settings['netmagic'].encode('utf-8')) + settings['netmagic'] = bytes.fromhex(settings['netmagic']) settings['out_of_order_cache_sz'] = int(settings['out_of_order_cache_sz']) settings['debug_output'] = settings['debug_output'].lower() diff --git a/contrib/signet/miner b/contrib/signet/miner index 78e1fa5ecd..012bd6cc31 100755 --- a/contrib/signet/miner +++ b/contrib/signet/miner @@ -15,7 +15,6 @@ import sys import time import subprocess -from binascii import unhexlify from io import BytesIO PATH_BASE_CONTRIB_SIGNET = os.path.abspath(os.path.dirname(os.path.realpath(__file__))) @@ -202,7 +201,7 @@ def finish_block(block, signet_solution, grind_cmd): def generate_psbt(tmpl, reward_spk, *, blocktime=None): signet_spk = tmpl["signet_challenge"] - signet_spk_bin = unhexlify(signet_spk) + signet_spk_bin = bytes.fromhex(signet_spk) cbtx = create_coinbase(height=tmpl["height"], value=tmpl["coinbasevalue"], spk=reward_spk) cbtx.vin[0].nSequence = 2**32-2 @@ -258,7 +257,7 @@ def get_reward_addr_spk(args, height): return args.address, args.reward_spk reward_addr = get_reward_address(args, height) - reward_spk = unhexlify(json.loads(args.bcli("getaddressinfo", reward_addr))["scriptPubKey"]) + reward_spk = bytes.fromhex(json.loads(args.bcli("getaddressinfo", reward_addr))["scriptPubKey"]) if args.address is not None: # will always be the same, so cache args.reward_spk = reward_spk diff --git a/contrib/verify-commits/README.md b/contrib/verify-commits/README.md index e95a57586f..b8b15280ba 100644 --- a/contrib/verify-commits/README.md +++ b/contrib/verify-commits/README.md @@ -40,7 +40,7 @@ Import trusted keys In order to check the commit signatures, you must add the trusted PGP keys to your machine. [GnuPG](https://gnupg.org/) may be used to import the trusted keys by running the following command: ```sh -gpg --keyserver hkp://keyserver.ubuntu.com --recv-keys $(<contrib/verify-commits/trusted-keys) +gpg --keyserver hkps://keys.openpgp.org --recv-keys $(<contrib/verify-commits/trusted-keys) ``` Key expiry/revocation diff --git a/contrib/zmq/zmq_sub.py b/contrib/zmq/zmq_sub.py index 9cb887e2dc..6269269d37 100755 --- a/contrib/zmq/zmq_sub.py +++ b/contrib/zmq/zmq_sub.py @@ -23,7 +23,6 @@ https://github.com/bitcoin/bitcoin/blob/37a7fe9e440b83e2364d5498931253937abe9294/contrib/zmq/zmq_sub.py """ -import binascii import asyncio import zmq import zmq.asyncio @@ -58,18 +57,18 @@ class ZMQHandler(): sequence = str(struct.unpack('<I', seq)[-1]) if topic == b"hashblock": print('- HASH BLOCK ('+sequence+') -') - print(binascii.hexlify(body)) + print(body.hex()) elif topic == b"hashtx": print('- HASH TX ('+sequence+') -') - print(binascii.hexlify(body)) + print(body.hex()) elif topic == b"rawblock": print('- RAW BLOCK HEADER ('+sequence+') -') - print(binascii.hexlify(body[:80])) + print(body[:80].hex()) elif topic == b"rawtx": print('- RAW TX ('+sequence+') -') - print(binascii.hexlify(body)) + print(body.hex()) elif topic == b"sequence": - hash = binascii.hexlify(body[:32]) + hash = body[:32].hex() label = chr(body[32]) mempool_sequence = None if len(body) != 32+1+8 else struct.unpack("<Q", body[32+1:])[0] print('- SEQUENCE ('+sequence+') -') diff --git a/doc/README.md b/doc/README.md index 38f6b1d327..4c79ecf42f 100644 --- a/doc/README.md +++ b/doc/README.md @@ -77,6 +77,7 @@ The Bitcoin repo's [root README](/README.md) contains relevant information on th - [Fuzz-testing](fuzzing.md) - [I2P Support](i2p.md) - [Init Scripts (systemd/upstart/openrc)](init.md) +- [Managing Wallets](managing-wallets.md) - [PSBT support](psbt.md) - [Reduce Memory](reduce-memory.md) - [Reduce Traffic](reduce-traffic.md) diff --git a/doc/managing-wallets.md b/doc/managing-wallets.md new file mode 100644 index 0000000000..aab6d131bd --- /dev/null +++ b/doc/managing-wallets.md @@ -0,0 +1,125 @@ +# Managing the Wallet + +## 1. Backing Up and Restoring The Wallet + +### 1.1 Creating the Wallet + +Since version 0.21, Bitcoin Core no longer has a default wallet. +Wallets can be created with the `createwallet` RPC or with the `Create wallet` GUI menu item. + +In the GUI, the `Create a new wallet` button is displayed on the main screen when there is no wallet loaded. Alternatively, there is the option `File` ->`Create wallet`. + +The following command, for example, creates a descriptor wallet. More information about this command may be found by running `bitcoin-cli help createwallet`. + +``` +$ bitcoin-cli -named createwallet wallet_name="wallet-01" descriptors=true +``` + +The `descriptors` parameter can be omitted if the intention is to create a legacy wallet. For now, the default type is the legacy wallet, but that is expected to change in a future release. + +By default, wallets are created in the `wallets` folder of the data directory, which varies by operating system, as shown below. The user can change the default by using the `-datadir` or `-walletdir` initialization parameters. + +| Operating System | Default wallet directory | +| -----------------|:------------------------------------------------------------| +| Linux | `/home/<user>/.bitcoin/wallets` | +| Windows | `C:\Users\<user>\AppData\Roaming\Bitcoin\wallets` | +| macOS | `/Users/<user>/Library/Application Support/Bitcoin/wallets` | + +### 1.2 Encrypting the Wallet + +The `wallet.dat` file is not encrypted by default and is, therefore, vulnerable if an attacker gains access to the device where the wallet or the backups are stored. + +Wallet encryption may prevent unauthorized access. However, this significantly increases the risk of losing coins due to forgotten passphrases. There is no way to recover a passphrase. This tradeoff should be well thought out by the user. + +Wallet encryption may also not protect against more sophisticated attacks. An attacker can, for example, obtain the password by installing a keylogger on the user's machine. + +After encrypting the wallet or changing the passphrase, a new backup needs to be created immediately. The reason is that the keypool is flushed and a new HD seed is generated after encryption. Any bitcoins received by the new seed cannot be recovered from the previous backups. + +The wallet's private key may be encrypted with the following command: + +``` +$ bitcoin-cli -rpcwallet="wallet-01" encryptwallet "passphrase" +``` + +Once encrypted, the passphrase can be changed with the `walletpassphrasechange` command. + +``` +$ bitcoin-cli -rpcwallet="wallet-01" walletpassphrasechange "oldpassphrase" "newpassphrase" +``` + +The argument passed to `-rpcwallet` is the name of the wallet to be encrypted. + +Only the wallet's private key is encrypted. All other wallet information, such as transactions, is still visible. + +The wallet's private key can also be encrypted in the `createwallet` command via the `passphrase` argument: + +``` +$ bitcoin-cli -named createwallet wallet_name="wallet-01" descriptors=true passphrase="passphrase" +``` + +Note that if the passphrase is lost, all the coins in the wallet will also be lost forever. + +### 1.3 Unlocking the Wallet + +If the wallet is encrypted and the user tries any operation related to private keys, such as sending bitcoins, an error message will be displayed. + +``` +$ bitcoin-cli -rpcwallet="wallet-01" sendtoaddress "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx" 0.01 +error code: -13 +error message: +Error: Please enter the wallet passphrase with walletpassphrase first. +``` + +To unlock the wallet and allow it to run these operations, the `walletpassphrase` RPC is required. + +This command takes the passphrase and an argument called `timeout`, which specifies the time in seconds that the wallet decryption key is stored in memory. After this period expires, the user needs to execute this RPC again. + +``` +$ bitcoin-cli -rpcwallet="wallet-01" walletpassphrase "passphrase" 120 +``` + +In the GUI, there is no specific menu item to unlock the wallet. When the user sends bitcoins, the passphrase will be prompted automatically. + +### 1.4 Backing Up the Wallet + +To backup the wallet, the `backupwallet` RPC or the `Backup Wallet` GUI menu item must be used to ensure the file is in a safe state when the copy is made. + +In the RPC, the destination parameter must include the name of the file. Otherwise, the command will return an error message like "Error: Wallet backup failed!" for descriptor wallets. If it is a legacy wallet, it will be copied and a file will be created with the default file name `wallet.dat`. + +``` +$ bitcoin-cli -rpcwallet="wallet-01" backupwallet /home/node01/Backups/backup-01.dat +``` + +In the GUI, the wallet is selected in the `Wallet` drop-down list in the upper right corner. If this list is not present, the wallet can be loaded in `File` ->`Open wallet` if necessary. Then, the backup can be done in `File` -> `Backup Wallet...`. + +This backup file can be stored on one or multiple offline devices, which must be reliable enough to work in an emergency and be malware free. Backup files can be regularly tested to avoid problems in the future. + +If the computer has malware, it can compromise the wallet when recovering the backup file. One way to minimize this is to not connect the backup to an online device. + +If both the wallet and all backups are lost for any reason, the bitcoins related to this wallet will become permanently inaccessible. + +### 1.5 Backup Frequency + +The original Bitcoin Core wallet was a collection of unrelated private keys. If a non-HD wallet had received funds to an address and then was restored from a backup made before the address was generated, then any funds sent to that address would have been lost because there was no deterministic mechanism to derive the address again. + +Bitcoin Core [version 0.13](https://github.com/bitcoin/bitcoin/blob/master/doc/release-notes/release-notes-0.13.0.md) introduced HD wallets with deterministic key derivation. With HD wallets, users no longer lose funds when restoring old backups because all addresses are derived from the HD wallet seed. + +This means that a single backup is enough to recover the coins at any time. It is still recommended to make regular backups (once a week) or after a significant number of new transactions to maintain the metadata, such as labels. Metadata cannot be retrieved from a blockchain rescan, so if the backup is too old, the metadata will be lost forever. + +Wallets created before version 0.13 are not HD and must be backed up every 100 keys used since the previous backup, or even more often to maintain the metadata. + +### 1.6 Restoring the Wallet From a Backup + +To restore a wallet, the `restorewallet` RPC must be used. + +``` +$ bitcoin-cli restorewallet "restored-wallet" /home/node01/Backups/backup-01.dat +``` + +After that, `getwalletinfo` can be used to check if the wallet has been fully restored. + +``` +$ bitcoin-cli -rpcwallet="restored-wallet" getwalletinfo +``` + +The restored wallet can also be loaded in the GUI via `File` ->`Open wallet`.
\ No newline at end of file diff --git a/doc/release-notes.md b/doc/release-notes.md index cf9edd9b08..55387013c2 100644 --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -57,6 +57,10 @@ Notable changes P2P and network changes ----------------------- +- A bitcoind node will no longer rumour addresses to inbound peers by default. + They will become eligible for address gossip after sending an ADDR, ADDRV2, + or GETADDR message. (#21528) + Updated RPCs ------------ @@ -66,6 +70,16 @@ New RPCs Build System ------------ +Files +----- + +* On startup, the list of banned hosts and networks (via `setban` RPC) in + `banlist.dat` is ignored and only `banlist.json` is considered. Bitcoin Core + version 22.x is the only version that can read `banlist.dat` and also write + it to `banlist.json`. If `banlist.json` already exists, version 22.x will not + try to translate the `banlist.dat` into json. After an upgrade, `listbanned` + can be used to double check the parsed entries. (#22570) + New settings ------------ @@ -94,6 +108,11 @@ RPC Tests ----- +- For the `regtest` network the activation heights of several softforks were + changed. + * BIP 34 (blockheight in coinbase) from 500 to 2 (#16333) + * BIP 66 (DERSIG) from 1251 to 102 (#22632) + Credits ======= diff --git a/doc/release-process.md b/doc/release-process.md index c57fa5b23a..3e4748b742 100644 --- a/doc/release-process.md +++ b/doc/release-process.md @@ -199,30 +199,22 @@ popd ### After 3 or more people have guix-built and their results match: -Combine `all.SHA256SUMS` and `all.SHA256SUMS.asc` into a clear-signed -`SHA256SUMS.asc` message: - -```sh -echo -e "-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA256\n\n$(cat all.SHA256SUMS)\n$(cat filename.txt.asc)" > SHA256SUMS.asc -``` - -Here's an equivalent, more readable command if you're confident that you won't -mess up whitespaces when copy-pasting: +Combine the `all.SHA256SUMS.asc` file from all signers into `SHA256SUMS.asc`: ```bash -cat << EOF > SHA256SUMS.asc ------BEGIN PGP SIGNED MESSAGE----- -Hash: SHA256 - -$(cat all.SHA256SUMS) -$(cat all.SHA256SUMS.asc) -EOF +cat "$VERSION"/*/all.SHA256SUMS.asc > SHA256SUMS.asc ``` -- Upload to the bitcoincore.org server (`/var/www/bin/bitcoin-core-${VERSION}`): - 1. The contents of `./bitcoin/guix-build-${VERSION}/output`, except for + +- Upload to the bitcoincore.org server (`/var/www/bin/bitcoin-core-${VERSION}/`): + 1. The contents of each `./bitcoin/guix-build-${VERSION}/output/${HOST}/` directory, except for `*-debug*` files. + Guix will output all of the results into host subdirectories, but the SHA256SUMS + file does not include these subdirectories. In order for downloads via torrent + to verify without directory structure modification, all of the uploaded files + need to be in the same directory as the SHA256SUMS file. + The `*-debug*` files generated by the guix build contain debug symbols for troubleshooting by developers. It is assumed that anyone that is interested in debugging can run guix to generate the files for @@ -230,7 +222,13 @@ EOF as save storage space *do not upload these to the bitcoincore.org server, nor put them in the torrent*. - 2. The combined clear-signed message you just created `SHA256SUMS.asc` + ```sh + find guix-build-${VERSION}/output/ -maxdepth 2 -type f -not -name "SHA256SUMS.part" -and -not -name "*debug*" -exec scp {} user@bitcoincore.org:/var/www/bin/bitcoin-core-${VERSION} \; + ``` + + 2. The `SHA256SUMS` file + + 3. The `SHA256SUMS.asc` combined signature file you just created - Create a torrent of the `/var/www/bin/bitcoin-core-${VERSION}` directory such that at the top level there is only one file: the `bitcoin-core-${VERSION}` diff --git a/share/rpcauth/rpcauth.py b/share/rpcauth/rpcauth.py index b14c80171e..c6d9b652b8 100755 --- a/share/rpcauth/rpcauth.py +++ b/share/rpcauth/rpcauth.py @@ -5,7 +5,6 @@ from argparse import ArgumentParser from base64 import urlsafe_b64encode -from binascii import hexlify from getpass import getpass from os import urandom @@ -13,7 +12,7 @@ import hmac def generate_salt(size): """Create size byte hex salt""" - return hexlify(urandom(size)).decode() + return urandom(size).hex() def generate_password(): """Create 32 byte b64 password""" diff --git a/src/addrdb.cpp b/src/addrdb.cpp index c3e224ee83..a5383be7cf 100644 --- a/src/addrdb.cpp +++ b/src/addrdb.cpp @@ -244,12 +244,7 @@ bool CAddrDB::Read(CAddrMan& addr) bool CAddrDB::Read(CAddrMan& addr, CDataStream& ssPeers) { - bool ret = DeserializeDB(ssPeers, addr, false); - if (!ret) { - // Ensure addrman is left in a clean state - addr.Clear(); - } - return ret; + return DeserializeDB(ssPeers, addr, false); } void DumpAnchors(const fs::path& anchors_db_path, const std::vector<CAddress>& anchors) diff --git a/src/addrman.cpp b/src/addrman.cpp index 96139182d3..edcf97f846 100644 --- a/src/addrman.cpp +++ b/src/addrman.cpp @@ -77,6 +77,23 @@ double CAddrInfo::GetChance(int64_t nNow) const return fChance; } +CAddrMan::CAddrMan(bool deterministic, int32_t consistency_check_ratio) + : insecure_rand{deterministic} + , nKey{deterministic ? uint256{1} : insecure_rand.rand256()} + , m_consistency_check_ratio{consistency_check_ratio} +{ + for (auto& bucket : vvNew) { + for (auto& entry : bucket) { + entry = -1; + } + } + for (auto& bucket : vvTried) { + for (auto& entry : bucket) { + entry = -1; + } + } +} + CAddrInfo* CAddrMan::Find(const CNetAddr& addr, int* pnId) { AssertLockHeld(cs); @@ -431,11 +448,16 @@ CAddrInfo CAddrMan::Select_(bool newOnly) const } } -#ifdef DEBUG_ADDRMAN -int CAddrMan::Check_() +int CAddrMan::Check_() const { AssertLockHeld(cs); + // Run consistency checks 1 in m_consistency_check_ratio times if enabled + if (m_consistency_check_ratio == 0) return 0; + if (insecure_rand.randrange(m_consistency_check_ratio) >= 1) return 0; + + LogPrint(BCLog::ADDRMAN, "Addrman checks started: new %i, tried %i, total %u\n", nNew, nTried, vRandom.size()); + std::unordered_set<int> setTried; std::unordered_map<int, int> mapNew; @@ -458,8 +480,10 @@ int CAddrMan::Check_() return -4; mapNew[n] = info.nRefCount; } - if (mapAddr[info] != n) + const auto it{mapAddr.find(info)}; + if (it == mapAddr.end() || it->second != n) { return -5; + } if (info.nRandomPos < 0 || (size_t)info.nRandomPos >= vRandom.size() || vRandom[info.nRandomPos] != n) return -14; if (info.nLastTry < 0) @@ -478,10 +502,13 @@ int CAddrMan::Check_() if (vvTried[n][i] != -1) { if (!setTried.count(vvTried[n][i])) return -11; - if (mapInfo[vvTried[n][i]].GetTriedBucket(nKey, m_asmap) != n) + const auto it{mapInfo.find(vvTried[n][i])}; + if (it == mapInfo.end() || it->second.GetTriedBucket(nKey, m_asmap) != n) { return -17; - if (mapInfo[vvTried[n][i]].GetBucketPosition(nKey, false, n) != i) + } + if (it->second.GetBucketPosition(nKey, false, n) != i) { return -18; + } setTried.erase(vvTried[n][i]); } } @@ -492,8 +519,10 @@ int CAddrMan::Check_() if (vvNew[n][i] != -1) { if (!mapNew.count(vvNew[n][i])) return -12; - if (mapInfo[vvNew[n][i]].GetBucketPosition(nKey, true, n) != i) + const auto it{mapInfo.find(vvNew[n][i])}; + if (it == mapInfo.end() || it->second.GetBucketPosition(nKey, true, n) != i) { return -19; + } if (--mapNew[vvNew[n][i]] == 0) mapNew.erase(vvNew[n][i]); } @@ -507,9 +536,9 @@ int CAddrMan::Check_() if (nKey.IsNull()) return -16; + LogPrint(BCLog::ADDRMAN, "Addrman checks completed successfully\n"); return 0; } -#endif void CAddrMan::GetAddr_(std::vector<CAddress>& vAddr, size_t max_addresses, size_t max_pct, std::optional<Network> network) const { diff --git a/src/addrman.h b/src/addrman.h index 4d8d05a99a..e2cb60b061 100644 --- a/src/addrman.h +++ b/src/addrman.h @@ -26,6 +26,9 @@ #include <unordered_map> #include <vector> +/** Default for -checkaddrman */ +static constexpr int32_t DEFAULT_ADDRMAN_CONSISTENCY_CHECKS{0}; + /** * Extended statistics about a CAddress */ @@ -58,6 +61,7 @@ private: mutable int nRandomPos{-1}; friend class CAddrMan; + friend class CAddrManDeterministic; public: @@ -104,23 +108,27 @@ public: * * Make sure no (localized) attacker can fill the entire table with his nodes/addresses. * * To that end: - * * Addresses are organized into buckets. - * * Addresses that have not yet been tried go into 1024 "new" buckets. - * * Based on the address range (/16 for IPv4) of the source of information, 64 buckets are selected at random. + * * Addresses are organized into buckets that can each store up to 64 entries. + * * Addresses to which our node has not successfully connected go into 1024 "new" buckets. + * * Based on the address range (/16 for IPv4) of the source of information, or if an asmap is provided, + * the AS it belongs to (for IPv4/IPv6), 64 buckets are selected at random. * * The actual bucket is chosen from one of these, based on the range in which the address itself is located. + * * The position in the bucket is chosen based on the full address. * * One single address can occur in up to 8 different buckets to increase selection chances for addresses that * are seen frequently. The chance for increasing this multiplicity decreases exponentially. - * * When adding a new address to a full bucket, a randomly chosen entry (with a bias favoring less recently seen - * ones) is removed from it first. + * * When adding a new address to an occupied position of a bucket, it will not replace the existing entry + * unless that address is also stored in another bucket or it doesn't meet one of several quality criteria + * (see IsTerrible for exact criteria). * * Addresses of nodes that are known to be accessible go into 256 "tried" buckets. * * Each address range selects at random 8 of these buckets. * * The actual bucket is chosen from one of these, based on the full address. - * * When adding a new good address to a full bucket, a randomly chosen entry (with a bias favoring less recently - * tried ones) is evicted from it, back to the "new" buckets. + * * When adding a new good address to an occupied position of a bucket, a FEELER connection to the + * old address is attempted. The old entry is only replaced and moved back to the "new" buckets if this + * attempt was unsuccessful. * * Bucket selection is based on cryptographic hashing, using a randomly-generated 256-bit key, which should not * be observable by adversaries. - * * Several indexes are kept for high performance. Defining DEBUG_ADDRMAN will introduce frequent (and expensive) - * consistency checks for the entire data structure. + * * Several indexes are kept for high performance. Setting m_consistency_check_ratio with the -checkaddrman + * configuration option will introduce (expensive) consistency checks for the entire data structure. */ //! total number of buckets for tried addresses @@ -463,35 +471,7 @@ public: Check(); } - void Clear() - EXCLUSIVE_LOCKS_REQUIRED(!cs) - { - LOCK(cs); - std::vector<int>().swap(vRandom); - nKey = insecure_rand.rand256(); - for (size_t bucket = 0; bucket < ADDRMAN_NEW_BUCKET_COUNT; bucket++) { - for (size_t entry = 0; entry < ADDRMAN_BUCKET_SIZE; entry++) { - vvNew[bucket][entry] = -1; - } - } - for (size_t bucket = 0; bucket < ADDRMAN_TRIED_BUCKET_COUNT; bucket++) { - for (size_t entry = 0; entry < ADDRMAN_BUCKET_SIZE; entry++) { - vvTried[bucket][entry] = -1; - } - } - - nIdCount = 0; - nTried = 0; - nNew = 0; - nLastGood = 1; //Initially at 1 so that "never" is strictly worse. - mapInfo.clear(); - mapAddr.clear(); - } - - CAddrMan() - { - Clear(); - } + explicit CAddrMan(bool deterministic, int32_t consistency_check_ratio); ~CAddrMan() { @@ -506,22 +486,7 @@ public: return vRandom.size(); } - //! Add a single address. - bool Add(const CAddress &addr, const CNetAddr& source, int64_t nTimePenalty = 0) - EXCLUSIVE_LOCKS_REQUIRED(!cs) - { - LOCK(cs); - bool fRet = false; - Check(); - fRet |= Add_(addr, source, nTimePenalty); - Check(); - if (fRet) { - LogPrint(BCLog::ADDRMAN, "Added %s from %s: %i tried, %i new\n", addr.ToStringIPPort(), source.ToString(), nTried, nNew); - } - return fRet; - } - - //! Add multiple addresses. + //! Add addresses to addrman's new table. bool Add(const std::vector<CAddress> &vAddr, const CNetAddr& source, int64_t nTimePenalty = 0) EXCLUSIVE_LOCKS_REQUIRED(!cs) { @@ -628,17 +593,16 @@ public: Check(); } -protected: - //! secret key to randomize bucket select with - uint256 nKey; +private: + //! A mutex to protect the inner data structures. + mutable Mutex cs; //! Source of random numbers for randomization in inner loops mutable FastRandomContext insecure_rand GUARDED_BY(cs); - //! A mutex to protect the inner data structures. - mutable Mutex cs; + //! secret key to randomize bucket select with + uint256 nKey; -private: //! Serialization versions. enum Format : uint8_t { V0_HISTORICAL = 0, //!< historic format, before commit e6b343d88 @@ -662,7 +626,7 @@ private: static constexpr uint8_t INCOMPATIBILITY_BASE = 32; //! last used nId - int nIdCount GUARDED_BY(cs); + int nIdCount GUARDED_BY(cs){0}; //! table with information about all nIds std::unordered_map<int, CAddrInfo> mapInfo GUARDED_BY(cs); @@ -676,28 +640,30 @@ private: mutable std::vector<int> vRandom GUARDED_BY(cs); // number of "tried" entries - int nTried GUARDED_BY(cs); + int nTried GUARDED_BY(cs){0}; //! list of "tried" buckets int vvTried[ADDRMAN_TRIED_BUCKET_COUNT][ADDRMAN_BUCKET_SIZE] GUARDED_BY(cs); //! number of (unique) "new" entries - int nNew GUARDED_BY(cs); + int nNew GUARDED_BY(cs){0}; //! list of "new" buckets int vvNew[ADDRMAN_NEW_BUCKET_COUNT][ADDRMAN_BUCKET_SIZE] GUARDED_BY(cs); - //! last time Good was called (memory only) - int64_t nLastGood GUARDED_BY(cs); + //! last time Good was called (memory only). Initially set to 1 so that "never" is strictly worse. + int64_t nLastGood GUARDED_BY(cs){1}; //! Holds addrs inserted into tried table that collide with existing entries. Test-before-evict discipline used to resolve these collisions. std::set<int> m_tried_collisions; + /** Perform consistency checks every m_consistency_check_ratio operations (if non-zero). */ + const int32_t m_consistency_check_ratio; + //! Find an entry. CAddrInfo* Find(const CNetAddr& addr, int *pnId = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs); - //! find an entry, creating it if necessary. - //! nTime and nServices of the found node are updated, if necessary. + //! Create a new entry and add it to the internal data structures mapInfo, mapAddr and vRandom. CAddrInfo* Create(const CAddress &addr, const CNetAddr &addrSource, int *pnId = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs); //! Swap two elements in vRandom. @@ -731,22 +697,19 @@ private: CAddrInfo SelectTriedCollision_() EXCLUSIVE_LOCKS_REQUIRED(cs); //! Consistency check - void Check() const - EXCLUSIVE_LOCKS_REQUIRED(cs) + void Check() const EXCLUSIVE_LOCKS_REQUIRED(cs) { -#ifdef DEBUG_ADDRMAN AssertLockHeld(cs); + const int err = Check_(); if (err) { LogPrintf("ADDRMAN CONSISTENCY CHECK FAILED!!! err=%i\n", err); + assert(false); } -#endif } -#ifdef DEBUG_ADDRMAN //! Perform consistency check. Returns an error code or zero. int Check_() const EXCLUSIVE_LOCKS_REQUIRED(cs); -#endif /** * Return all or many randomly selected addresses, optionally by network. @@ -775,6 +738,7 @@ private: void SetServices_(const CService &addr, ServiceFlags nServices) EXCLUSIVE_LOCKS_REQUIRED(cs); friend class CAddrManTest; + friend class CAddrManDeterministic; }; #endif // BITCOIN_ADDRMAN_H diff --git a/src/bench/addrman.cpp b/src/bench/addrman.cpp index b7bd8a3261..d69a651811 100644 --- a/src/bench/addrman.cpp +++ b/src/bench/addrman.cpp @@ -72,17 +72,15 @@ static void AddrManAdd(benchmark::Bench& bench) { CreateAddresses(); - CAddrMan addrman; - bench.run([&] { + CAddrMan addrman{/* deterministic */ false, /* consistency_check_ratio */ 0}; AddAddressesToAddrMan(addrman); - addrman.Clear(); }); } static void AddrManSelect(benchmark::Bench& bench) { - CAddrMan addrman; + CAddrMan addrman(/* deterministic */ false, /* consistency_check_ratio */ 0); FillAddrMan(addrman); @@ -94,7 +92,7 @@ static void AddrManSelect(benchmark::Bench& bench) static void AddrManGetAddr(benchmark::Bench& bench) { - CAddrMan addrman; + CAddrMan addrman(/* deterministic */ false, /* consistency_check_ratio */ 0); FillAddrMan(addrman); @@ -112,10 +110,12 @@ static void AddrManGood(benchmark::Bench& bench) * we want to do the same amount of work in every loop iteration. */ bench.epochs(5).epochIterations(1); + const size_t addrman_count{bench.epochs() * bench.epochIterations()}; - std::vector<CAddrMan> addrmans(bench.epochs() * bench.epochIterations()); - for (auto& addrman : addrmans) { - FillAddrMan(addrman); + std::vector<std::unique_ptr<CAddrMan>> addrmans(addrman_count); + for (size_t i{0}; i < addrman_count; ++i) { + addrmans[i] = std::make_unique<CAddrMan>(/* deterministic */ false, /* consistency_check_ratio */ 0); + FillAddrMan(*addrmans[i]); } auto markSomeAsGood = [](CAddrMan& addrman) { @@ -130,7 +130,7 @@ static void AddrManGood(benchmark::Bench& bench) uint64_t i = 0; bench.run([&] { - markSomeAsGood(addrmans.at(i)); + markSomeAsGood(*addrmans.at(i)); ++i; }); } diff --git a/src/bitcoin-cli-res.rc b/src/bitcoin-cli-res.rc index 405a302261..d9e5dcf7fd 100644 --- a/src/bitcoin-cli-res.rc +++ b/src/bitcoin-cli-res.rc @@ -2,9 +2,7 @@ #include "clientversion.h" // holds the needed client version information #define VER_PRODUCTVERSION CLIENT_VERSION_MAJOR,CLIENT_VERSION_MINOR,CLIENT_VERSION_BUILD -#define VER_PRODUCTVERSION_STR STRINGIZE(CLIENT_VERSION_MAJOR) "." STRINGIZE(CLIENT_VERSION_MINOR) "." STRINGIZE(CLIENT_VERSION_BUILD) #define VER_FILEVERSION VER_PRODUCTVERSION -#define VER_FILEVERSION_STR VER_PRODUCTVERSION_STR VS_VERSION_INFO VERSIONINFO FILEVERSION VER_FILEVERSION @@ -18,13 +16,13 @@ BEGIN BEGIN VALUE "CompanyName", "Bitcoin" VALUE "FileDescription", "bitcoin-cli (JSON-RPC client for " PACKAGE_NAME ")" - VALUE "FileVersion", VER_FILEVERSION_STR + VALUE "FileVersion", PACKAGE_VERSION VALUE "InternalName", "bitcoin-cli" VALUE "LegalCopyright", COPYRIGHT_STR VALUE "LegalTrademarks1", "Distributed under the MIT software license, see the accompanying file COPYING or http://www.opensource.org/licenses/mit-license.php." VALUE "OriginalFilename", "bitcoin-cli.exe" VALUE "ProductName", "bitcoin-cli" - VALUE "ProductVersion", VER_PRODUCTVERSION_STR + VALUE "ProductVersion", PACKAGE_VERSION END END diff --git a/src/bitcoin-cli.cpp b/src/bitcoin-cli.cpp index 1ec6411e32..bc0af6398c 100644 --- a/src/bitcoin-cli.cpp +++ b/src/bitcoin-cli.cpp @@ -885,6 +885,29 @@ static void GetWalletBalances(UniValue& result) } /** + * GetProgressBar contructs a progress bar with 5% intervals. + * + * @param[in] progress The proportion of the progress bar to be filled between 0 and 1. + * @param[out] progress_bar String representation of the progress bar. + */ +static void GetProgressBar(double progress, std::string& progress_bar) +{ + if (progress < 0 || progress > 1) return; + + static constexpr double INCREMENT{0.05}; + static const std::string COMPLETE_BAR{"\u2592"}; + static const std::string INCOMPLETE_BAR{"\u2591"}; + + for (int i = 0; i < progress / INCREMENT; ++i) { + progress_bar += COMPLETE_BAR; + } + + for (int i = 0; i < (1 - progress) / INCREMENT; ++i) { + progress_bar += INCOMPLETE_BAR; + } +} + +/** * ParseGetInfoResult takes in -getinfo result in UniValue object and parses it * into a user friendly UniValue string to be printed on the console. * @param[out] result Reference to UniValue result containing the -getinfo output. @@ -926,7 +949,17 @@ static void ParseGetInfoResult(UniValue& result) std::string result_string = strprintf("%sChain: %s%s\n", BLUE, result["chain"].getValStr(), RESET); result_string += strprintf("Blocks: %s\n", result["blocks"].getValStr()); result_string += strprintf("Headers: %s\n", result["headers"].getValStr()); - result_string += strprintf("Verification progress: %.4f%%\n", result["verificationprogress"].get_real() * 100); + + const double ibd_progress{result["verificationprogress"].get_real()}; + std::string ibd_progress_bar; + // Display the progress bar only if IBD progress is less than 99% + if (ibd_progress < 0.99) { + GetProgressBar(ibd_progress, ibd_progress_bar); + // Add padding between progress bar and IBD progress + ibd_progress_bar += " "; + } + + result_string += strprintf("Verification progress: %s%.4f%%\n", ibd_progress_bar, ibd_progress * 100); result_string += strprintf("Difficulty: %s\n\n", result["difficulty"].getValStr()); result_string += strprintf( diff --git a/src/bitcoin-tx-res.rc b/src/bitcoin-tx-res.rc index b545ce9dbe..46e4fc9274 100644 --- a/src/bitcoin-tx-res.rc +++ b/src/bitcoin-tx-res.rc @@ -2,9 +2,7 @@ #include "clientversion.h" // holds the needed client version information #define VER_PRODUCTVERSION CLIENT_VERSION_MAJOR,CLIENT_VERSION_MINOR,CLIENT_VERSION_BUILD -#define VER_PRODUCTVERSION_STR STRINGIZE(CLIENT_VERSION_MAJOR) "." STRINGIZE(CLIENT_VERSION_MINOR) "." STRINGIZE(CLIENT_VERSION_BUILD) #define VER_FILEVERSION VER_PRODUCTVERSION -#define VER_FILEVERSION_STR VER_PRODUCTVERSION_STR VS_VERSION_INFO VERSIONINFO FILEVERSION VER_FILEVERSION @@ -18,13 +16,13 @@ BEGIN BEGIN VALUE "CompanyName", "Bitcoin" VALUE "FileDescription", "bitcoin-tx (CLI Bitcoin transaction editor utility)" - VALUE "FileVersion", VER_FILEVERSION_STR + VALUE "FileVersion", PACKAGE_VERSION VALUE "InternalName", "bitcoin-tx" VALUE "LegalCopyright", COPYRIGHT_STR VALUE "LegalTrademarks1", "Distributed under the MIT software license, see the accompanying file COPYING or http://www.opensource.org/licenses/mit-license.php." VALUE "OriginalFilename", "bitcoin-tx.exe" VALUE "ProductName", "bitcoin-tx" - VALUE "ProductVersion", VER_PRODUCTVERSION_STR + VALUE "ProductVersion", PACKAGE_VERSION END END diff --git a/src/bitcoin-util-res.rc b/src/bitcoin-util-res.rc index 3f0fa8ab6d..0de8c5befa 100644 --- a/src/bitcoin-util-res.rc +++ b/src/bitcoin-util-res.rc @@ -2,9 +2,7 @@ #include "clientversion.h" // holds the needed client version information #define VER_PRODUCTVERSION CLIENT_VERSION_MAJOR,CLIENT_VERSION_MINOR,CLIENT_VERSION_BUILD -#define VER_PRODUCTVERSION_STR STRINGIZE(CLIENT_VERSION_MAJOR) "." STRINGIZE(CLIENT_VERSION_MINOR) "." STRINGIZE(CLIENT_VERSION_BUILD) #define VER_FILEVERSION VER_PRODUCTVERSION -#define VER_FILEVERSION_STR VER_PRODUCTVERSION_STR VS_VERSION_INFO VERSIONINFO FILEVERSION VER_FILEVERSION @@ -18,13 +16,13 @@ BEGIN BEGIN VALUE "CompanyName", "Bitcoin" VALUE "FileDescription", "bitcoin-util (CLI Bitcoin utility)" - VALUE "FileVersion", VER_FILEVERSION_STR + VALUE "FileVersion", PACKAGE_VERSION VALUE "InternalName", "bitcoin-util" VALUE "LegalCopyright", COPYRIGHT_STR VALUE "LegalTrademarks1", "Distributed under the MIT software license, see the accompanying file COPYING or http://www.opensource.org/licenses/mit-license.php." VALUE "OriginalFilename", "bitcoin-util.exe" VALUE "ProductName", "bitcoin-util" - VALUE "ProductVersion", VER_PRODUCTVERSION_STR + VALUE "ProductVersion", PACKAGE_VERSION END END diff --git a/src/bitcoin-wallet-res.rc b/src/bitcoin-wallet-res.rc index 59346ab8f6..d86ffbd9f1 100644 --- a/src/bitcoin-wallet-res.rc +++ b/src/bitcoin-wallet-res.rc @@ -2,9 +2,7 @@ #include "clientversion.h" // holds the needed client version information #define VER_PRODUCTVERSION CLIENT_VERSION_MAJOR,CLIENT_VERSION_MINOR,CLIENT_VERSION_BUILD -#define VER_PRODUCTVERSION_STR STRINGIZE(CLIENT_VERSION_MAJOR) "." STRINGIZE(CLIENT_VERSION_MINOR) "." STRINGIZE(CLIENT_VERSION_BUILD) #define VER_FILEVERSION VER_PRODUCTVERSION -#define VER_FILEVERSION_STR VER_PRODUCTVERSION_STR VS_VERSION_INFO VERSIONINFO FILEVERSION VER_FILEVERSION @@ -18,13 +16,13 @@ BEGIN BEGIN VALUE "CompanyName", "Bitcoin" VALUE "FileDescription", "bitcoin-wallet (CLI tool for " PACKAGE_NAME " wallets)" - VALUE "FileVersion", VER_FILEVERSION_STR + VALUE "FileVersion", PACKAGE_VERSION VALUE "InternalName", "bitcoin-wallet" VALUE "LegalCopyright", COPYRIGHT_STR VALUE "LegalTrademarks1", "Distributed under the MIT software license, see the accompanying file COPYING or http://www.opensource.org/licenses/mit-license.php." VALUE "OriginalFilename", "bitcoin-wallet.exe" VALUE "ProductName", "bitcoin-wallet" - VALUE "ProductVersion", VER_PRODUCTVERSION_STR + VALUE "ProductVersion", PACKAGE_VERSION END END diff --git a/src/bitcoind-res.rc b/src/bitcoind-res.rc index a98b50c899..353761dfa7 100644 --- a/src/bitcoind-res.rc +++ b/src/bitcoind-res.rc @@ -2,9 +2,7 @@ #include "clientversion.h" // holds the needed client version information #define VER_PRODUCTVERSION CLIENT_VERSION_MAJOR,CLIENT_VERSION_MINOR,CLIENT_VERSION_BUILD -#define VER_PRODUCTVERSION_STR STRINGIZE(CLIENT_VERSION_MAJOR) "." STRINGIZE(CLIENT_VERSION_MINOR) "." STRINGIZE(CLIENT_VERSION_BUILD) #define VER_FILEVERSION VER_PRODUCTVERSION -#define VER_FILEVERSION_STR VER_PRODUCTVERSION_STR VS_VERSION_INFO VERSIONINFO FILEVERSION VER_FILEVERSION @@ -18,13 +16,13 @@ BEGIN BEGIN VALUE "CompanyName", "Bitcoin" VALUE "FileDescription", "bitcoind (Bitcoin node with a JSON-RPC server)" - VALUE "FileVersion", VER_FILEVERSION_STR + VALUE "FileVersion", PACKAGE_VERSION VALUE "InternalName", "bitcoind" VALUE "LegalCopyright", COPYRIGHT_STR VALUE "LegalTrademarks1", "Distributed under the MIT software license, see the accompanying file COPYING or http://www.opensource.org/licenses/mit-license.php." VALUE "OriginalFilename", "bitcoind.exe" VALUE "ProductName", "bitcoind" - VALUE "ProductVersion", VER_PRODUCTVERSION_STR + VALUE "ProductVersion", PACKAGE_VERSION END END diff --git a/src/chainparams.cpp b/src/chainparams.cpp index 0b3242b1aa..c3bbb147be 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -393,7 +393,7 @@ public: consensus.BIP34Height = 2; // BIP34 activated on regtest (Block at height 1 not enforced for testing purposes) consensus.BIP34Hash = uint256(); consensus.BIP65Height = 1351; // BIP65 activated on regtest (Used in functional tests) - consensus.BIP66Height = 1251; // BIP66 activated on regtest (Used in functional tests) + consensus.BIP66Height = 102; // BIP66 activated on regtest (Block at height 101 and earlier not enforced for testing purposes) consensus.CSVHeight = 432; // CSV activated on regtest (Used in rpc activation tests) consensus.SegwitHeight = 0; // SEGWIT is always activated on regtest unless overridden consensus.MinBIP9WarningHeight = 0; diff --git a/src/clientversion.cpp b/src/clientversion.cpp index 29c38e2d3b..f97e4097e8 100644 --- a/src/clientversion.cpp +++ b/src/clientversion.cpp @@ -30,8 +30,10 @@ const std::string CLIENT_NAME("Satoshi"); #define BUILD_DESC BUILD_GIT_TAG #define BUILD_SUFFIX "" #else - #define BUILD_DESC "v" STRINGIZE(CLIENT_VERSION_MAJOR) "." STRINGIZE(CLIENT_VERSION_MINOR) "." STRINGIZE(CLIENT_VERSION_BUILD) - #ifdef BUILD_GIT_COMMIT + #define BUILD_DESC "v" PACKAGE_VERSION + #if CLIENT_VERSION_IS_RELEASE + #define BUILD_SUFFIX "" + #elif defined(BUILD_GIT_COMMIT) #define BUILD_SUFFIX "-" BUILD_GIT_COMMIT #elif defined(GIT_COMMIT_ID) #define BUILD_SUFFIX "-g" GIT_COMMIT_ID @@ -40,8 +42,6 @@ const std::string CLIENT_NAME("Satoshi"); #endif #endif -const std::string CLIENT_BUILD(BUILD_DESC BUILD_SUFFIX); - static std::string FormatVersion(int nVersion) { return strprintf("%d.%d.%d", nVersion / 10000, (nVersion / 100) % 100, nVersion % 100); @@ -49,6 +49,7 @@ static std::string FormatVersion(int nVersion) std::string FormatFullVersion() { + static const std::string CLIENT_BUILD(BUILD_DESC BUILD_SUFFIX); return CLIENT_BUILD; } diff --git a/src/clientversion.h b/src/clientversion.h index 0ed3f68094..a3e6233437 100644 --- a/src/clientversion.h +++ b/src/clientversion.h @@ -36,7 +36,6 @@ static const int CLIENT_VERSION = + 1 * CLIENT_VERSION_BUILD; extern const std::string CLIENT_NAME; -extern const std::string CLIENT_BUILD; std::string FormatFullVersion(); diff --git a/src/crypto/chacha_poly_aead.cpp b/src/crypto/chacha_poly_aead.cpp index 0582a60c4f..b73b22a2b8 100644 --- a/src/crypto/chacha_poly_aead.cpp +++ b/src/crypto/chacha_poly_aead.cpp @@ -31,8 +31,9 @@ ChaCha20Poly1305AEAD::ChaCha20Poly1305AEAD(const unsigned char* K_1, size_t K_1_ { assert(K_1_len == CHACHA20_POLY1305_AEAD_KEY_LEN); assert(K_2_len == CHACHA20_POLY1305_AEAD_KEY_LEN); - m_chacha_main.SetKey(K_1, CHACHA20_POLY1305_AEAD_KEY_LEN); - m_chacha_header.SetKey(K_2, CHACHA20_POLY1305_AEAD_KEY_LEN); + + m_chacha_header.SetKey(K_1, CHACHA20_POLY1305_AEAD_KEY_LEN); + m_chacha_main.SetKey(K_2, CHACHA20_POLY1305_AEAD_KEY_LEN); // set the cached sequence number to uint64 max which hints for an unset cache. // we can't hit uint64 max since the rekey rule (which resets the sequence number) is 1GB diff --git a/src/init.cpp b/src/init.cpp index aee8b78999..bf027b2879 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -501,7 +501,8 @@ void SetupServerArgs(ArgsManager& argsman) argsman.AddArg("-checkblocks=<n>", strprintf("How many blocks to check at startup (default: %u, 0 = all)", DEFAULT_CHECKBLOCKS), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); argsman.AddArg("-checklevel=<n>", strprintf("How thorough the block verification of -checkblocks is: %s (0-4, default: %u)", Join(CHECKLEVEL_DOC, ", "), DEFAULT_CHECKLEVEL), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); argsman.AddArg("-checkblockindex", strprintf("Do a consistency check for the block tree, chainstate, and other validation data structures occasionally. (default: %u, regtest: %u)", defaultChainParams->DefaultConsistencyChecks(), regtestChainParams->DefaultConsistencyChecks()), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); - argsman.AddArg("-checkmempool=<n>", strprintf("Run checks every <n> transactions (default: %u, regtest: %u)", defaultChainParams->DefaultConsistencyChecks(), regtestChainParams->DefaultConsistencyChecks()), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); + argsman.AddArg("-checkaddrman=<n>", strprintf("Run addrman consistency checks every <n> operations. Use 0 to disable. (default: %u)", DEFAULT_ADDRMAN_CONSISTENCY_CHECKS), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); + argsman.AddArg("-checkmempool=<n>", strprintf("Run mempool consistency checks every <n> transactions. Use 0 to disable. (default: %u, regtest: %u)", defaultChainParams->DefaultConsistencyChecks(), regtestChainParams->DefaultConsistencyChecks()), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); argsman.AddArg("-checkpoints", strprintf("Enable rejection of any forks from the known historical chain until block %s (default: %u)", defaultChainParams->Checkpoints().GetHeight(), DEFAULT_CHECKPOINTS_ENABLED), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); argsman.AddArg("-deprecatedrpc=<method>", "Allows deprecated RPC method(s) to be used", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); argsman.AddArg("-stopafterblockimport", strprintf("Stop running after importing blocks from disk (default: %u)", DEFAULT_STOPAFTERBLOCKIMPORT), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); @@ -859,6 +860,11 @@ bool AppInitParameterInteraction(const ArgsManager& args) return InitError(Untranslated("Cannot set -bind or -whitebind together with -listen=0")); } + // if listen=0, then disallow listenonion=1 + if (!args.GetBoolArg("-listen", DEFAULT_LISTEN) && args.GetBoolArg("-listenonion", DEFAULT_LISTEN_ONION)) { + return InitError(Untranslated("Cannot set -listen=0 together with -listenonion=1")); + } + // Make sure enough file descriptors are available int nBind = std::max(nUserBind, size_t(1)); nUserMaxConnections = args.GetArg("-maxconnections", DEFAULT_MAX_PEER_CONNECTIONS); @@ -1164,7 +1170,22 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) const bool ignores_incoming_txs{args.GetBoolArg("-blocksonly", DEFAULT_BLOCKSONLY)}; assert(!node.addrman); - node.addrman = std::make_unique<CAddrMan>(); + auto check_addrman = std::clamp<int32_t>(args.GetArg("-checkaddrman", DEFAULT_ADDRMAN_CONSISTENCY_CHECKS), 0, 1000000); + node.addrman = std::make_unique<CAddrMan>(/* deterministic */ false, /* consistency_check_ratio */ check_addrman); + { + // Load addresses from peers.dat + uiInterface.InitMessage(_("Loading P2P addresses…").translated); + int64_t nStart = GetTimeMillis(); + CAddrDB adb; + if (adb.Read(*node.addrman)) { + LogPrintf("Loaded %i addresses from peers.dat %dms\n", node.addrman->size(), GetTimeMillis() - nStart); + } else { + // Addrman can be in an inconsistent state after failure, reset it + node.addrman = std::make_unique<CAddrMan>(/* deterministic */ false, /* consistency_check_ratio */ check_addrman); + LogPrintf("Recreating peers.dat\n"); + adb.Write(*node.addrman); + } + } assert(!node.banman); node.banman = std::make_unique<BanMan>(gArgs.GetDataDirNet() / "banlist", &uiInterface, args.GetArg("-bantime", DEFAULT_MISBEHAVING_BANTIME)); assert(!node.connman); @@ -1185,7 +1206,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) assert(!node.peerman); node.peerman = PeerManager::make(chainparams, *node.connman, *node.addrman, node.banman.get(), - *node.scheduler, chainman, *node.mempool, ignores_incoming_txs); + chainman, *node.mempool, ignores_incoming_txs); RegisterValidationInterface(node.peerman.get()); // sanitize comments per BIP-0014, format user agent and check total size @@ -1794,6 +1815,8 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) banman->DumpBanlist(); }, DUMP_BANS_INTERVAL); + if (node.peerman) node.peerman->StartScheduledTasks(*node.scheduler); + #if HAVE_SYSTEM StartupNotify(args); #endif diff --git a/src/interfaces/chain.h b/src/interfaces/chain.h index 7cac435e96..eceede3c8f 100644 --- a/src/interfaces/chain.h +++ b/src/interfaces/chain.h @@ -35,7 +35,9 @@ namespace interfaces { class Handler; class Wallet; -//! Helper for findBlock to selectively return pieces of block data. +//! Helper for findBlock to selectively return pieces of block data. If block is +//! found, data will be returned by setting specified output variables. If block +//! is not found, output variables will keep their previous values. class FoundBlock { public: @@ -60,6 +62,7 @@ public: bool* m_in_active_chain = nullptr; const FoundBlock* m_next_block = nullptr; CBlock* m_data = nullptr; + mutable bool found = false; }; //! Interface giving clients (wallet processes, maybe other analysis tools in @@ -262,11 +265,18 @@ public: //! Current RPC serialization flags. virtual int rpcSerializationFlags() = 0; + //! Get settings value. + virtual util::SettingsValue getSetting(const std::string& arg) = 0; + + //! Get list of settings values. + virtual std::vector<util::SettingsValue> getSettingsList(const std::string& arg) = 0; + //! Return <datadir>/settings.json setting value. virtual util::SettingsValue getRwSetting(const std::string& name) = 0; - //! Write a setting to <datadir>/settings.json. - virtual bool updateRwSetting(const std::string& name, const util::SettingsValue& value) = 0; + //! Write a setting to <datadir>/settings.json. Optionally just update the + //! setting in memory and do not write the file. + virtual bool updateRwSetting(const std::string& name, const util::SettingsValue& value, bool write=true) = 0; //! Synchronously send transactionAddedToMempool notifications about all //! current mempool transactions to the specified handler and return after diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index fb1febc11b..a85db04b8b 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -332,6 +332,9 @@ public: //! loaded at startup or by RPC. using LoadWalletFn = std::function<void(std::unique_ptr<Wallet> wallet)>; virtual std::unique_ptr<Handler> handleLoadWallet(LoadWalletFn fn) = 0; + + //! Return pointer to internal context, useful for testing. + virtual WalletContext* context() { return nullptr; } }; //! Information about one wallet address. @@ -410,7 +413,7 @@ struct WalletTxOut //! Return implementation of Wallet interface. This function is defined in //! dummywallet.cpp and throws if the wallet component is not compiled. -std::unique_ptr<Wallet> MakeWallet(const std::shared_ptr<CWallet>& wallet); +std::unique_ptr<Wallet> MakeWallet(WalletContext& context, const std::shared_ptr<CWallet>& wallet); //! Return implementation of ChainClient interface for a wallet client. This //! function will be undefined in builds where ENABLE_WALLET is false. @@ -133,10 +133,15 @@ public: * optionally tweaked by *merkle_root. Additional nonce entropy can be provided through * aux. * - * When merkle_root is not nullptr, this results in a signature with a modified key as - * specified in BIP341: - * - If merkle_root->IsNull(): key + H_TapTweak(pubkey)*G - * - Otherwise: key + H_TapTweak(pubkey || *merkle_root) + * merkle_root is used to optionally perform tweaking of the private key, as specified + * in BIP341: + * - If merkle_root == nullptr: no tweaking is done, sign with key directly (this is + * used for signatures in BIP342 script). + * - If merkle_root->IsNull(): sign with key + H_TapTweak(pubkey) (this is used for + * key path spending when no scripts are present). + * - Otherwise: sign with key + H_TapTweak(pubkey || *merkle_root) + * (this is used for key path spending, with specific + * Merkle root of the script tree). */ bool SignSchnorr(const uint256& hash, Span<unsigned char> sig, const uint256* merkle_root = nullptr, const uint256* aux = nullptr) const; diff --git a/src/net.cpp b/src/net.cpp index 8ef770ede2..57b8844d6b 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -1304,8 +1304,9 @@ void CConnman::NotifyNumConnectionsChanged() } if(vNodesSize != nPrevNodeCount) { nPrevNodeCount = vNodesSize; - if(clientInterface) - clientInterface->NotifyNumConnectionsChanged(vNodesSize); + if (m_client_interface) { + m_client_interface->NotifyNumConnectionsChanged(vNodesSize); + } } } @@ -2448,7 +2449,9 @@ void CConnman::SetNetworkActive(bool active) fNetworkActive = active; - uiInterface.NotifyNetworkActiveChanged(fNetworkActive); + if (m_client_interface) { + m_client_interface->NotifyNetworkActiveChanged(fNetworkActive); + } } CConnman::CConnman(uint64_t nSeed0In, uint64_t nSeed1In, CAddrMan& addrman_in, bool network_active) @@ -2473,8 +2476,8 @@ bool CConnman::Bind(const CService &addr, unsigned int flags, NetPermissionFlags } bilingual_str strError; if (!BindListenPort(addr, strError, permissions)) { - if ((flags & BF_REPORT_ERROR) && clientInterface) { - clientInterface->ThreadSafeMessageBox(strError, "", CClientUIInterface::MSG_ERROR); + if ((flags & BF_REPORT_ERROR) && m_client_interface) { + m_client_interface->ThreadSafeMessageBox(strError, "", CClientUIInterface::MSG_ERROR); } return false; } @@ -2513,8 +2516,8 @@ bool CConnman::Start(CScheduler& scheduler, const Options& connOptions) Init(connOptions); if (fListen && !InitBinds(connOptions)) { - if (clientInterface) { - clientInterface->ThreadSafeMessageBox( + if (m_client_interface) { + m_client_interface->ThreadSafeMessageBox( _("Failed to listen on any port. Use -listen=0 if you want this."), "", CClientUIInterface::MSG_ERROR); } @@ -2531,22 +2534,6 @@ bool CConnman::Start(CScheduler& scheduler, const Options& connOptions) AddAddrFetch(strDest); } - if (clientInterface) { - clientInterface->InitMessage(_("Loading P2P addresses…").translated); - } - // Load addresses from peers.dat - int64_t nStart = GetTimeMillis(); - { - CAddrDB adb; - if (adb.Read(addrman)) - LogPrintf("Loaded %i addresses from peers.dat %dms\n", addrman.size(), GetTimeMillis() - nStart); - else { - addrman.Clear(); // Addrman can be in an inconsistent state after failure, reset it - LogPrintf("Recreating peers.dat\n"); - DumpAddresses(); - } - } - if (m_use_addrman_outgoing) { // Load addresses from anchors.dat m_anchors = ReadAnchors(gArgs.GetDataDirNet() / ANCHORS_DATABASE_FILENAME); @@ -2556,7 +2543,9 @@ bool CConnman::Start(CScheduler& scheduler, const Options& connOptions) LogPrintf("%i block-relay-only anchors will be tried for connections.\n", m_anchors.size()); } - uiInterface.InitMessage(_("Starting network threads…").translated); + if (m_client_interface) { + m_client_interface->InitMessage(_("Starting network threads…").translated); + } fAddressesInitialized = true; @@ -2594,8 +2583,8 @@ bool CConnman::Start(CScheduler& scheduler, const Options& connOptions) threadOpenAddedConnections = std::thread(&util::TraceThread, "addcon", [this] { ThreadOpenAddedConnections(); }); if (connOptions.m_use_addrman_outgoing && !connOptions.m_specified_outgoing.empty()) { - if (clientInterface) { - clientInterface->ThreadSafeMessageBox( + if (m_client_interface) { + m_client_interface->ThreadSafeMessageBox( _("Cannot provide specific connections and have addrman find outgoing connections at the same."), "", CClientUIInterface::MSG_ERROR); } @@ -787,7 +787,7 @@ public: nMaxAddnode = connOptions.nMaxAddnode; nMaxFeeler = connOptions.nMaxFeeler; m_max_outbound = m_max_outbound_full_relay + m_max_outbound_block_relay + nMaxFeeler; - clientInterface = connOptions.uiInterface; + m_client_interface = connOptions.uiInterface; m_banman = connOptions.m_banman; m_msgproc = connOptions.m_msgproc; nSendBufferMaxSize = connOptions.nSendBufferMaxSize; @@ -1126,7 +1126,7 @@ private: int nMaxFeeler; int m_max_outbound; bool m_use_addrman_outgoing; - CClientUIInterface* clientInterface; + CClientUIInterface* m_client_interface; NetEventsInterface* m_msgproc; /** Pointer to this node's banman. May be nullptr - check existence before dereferencing. */ BanMan* m_banman; diff --git a/src/net_processing.cpp b/src/net_processing.cpp index 005fe1bf0c..0cb13f9253 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -161,7 +161,7 @@ static constexpr size_t MAX_ADDR_TO_SEND{1000}; static constexpr double MAX_ADDR_RATE_PER_SECOND{0.1}; /** The soft limit of the address processing token bucket (the regular MAX_ADDR_RATE_PER_SECOND * based increments won't go above this, but the MAX_ADDR_TO_SEND increment following GETADDR - * is exempt from this limit. */ + * is exempt from this limit). */ static constexpr size_t MAX_ADDR_PROCESSING_TOKEN_BUCKET{MAX_ADDR_TO_SEND}; // Internal stuff @@ -263,14 +263,14 @@ struct Peer { std::atomic_bool m_wants_addrv2{false}; /** Whether this peer has already sent us a getaddr message. */ bool m_getaddr_recvd{false}; - /** Number of addr messages that can be processed from this peer. Start at 1 to + /** Number of addresses that can be processed from this peer. Start at 1 to * permit self-announcement. */ double m_addr_token_bucket{1.0}; /** When m_addr_token_bucket was last updated */ std::chrono::microseconds m_addr_token_timestamp{GetTime<std::chrono::microseconds>()}; /** Total number of addresses that were dropped due to rate limiting. */ std::atomic<uint64_t> m_addr_rate_limited{0}; - /** Total number of addresses that were processed (excludes rate limited ones). */ + /** Total number of addresses that were processed (excludes rate-limited ones). */ std::atomic<uint64_t> m_addr_processed{0}; /** Set of txids to reconsider once their parent transactions have been accepted **/ @@ -292,7 +292,7 @@ class PeerManagerImpl final : public PeerManager { public: PeerManagerImpl(const CChainParams& chainparams, CConnman& connman, CAddrMan& addrman, - BanMan* banman, CScheduler& scheduler, ChainstateManager& chainman, + BanMan* banman, ChainstateManager& chainman, CTxMemPool& pool, bool ignore_incoming_txs); /** Overridden from CValidationInterface. */ @@ -309,6 +309,7 @@ public: bool SendMessages(CNode* pto) override EXCLUSIVE_LOCKS_REQUIRED(pto->cs_sendProcessing); /** Implement PeerManager */ + void StartScheduledTasks(CScheduler& scheduler) override; void CheckForStaleTipAndEvictPeers() override; bool GetNodeStateStats(NodeId nodeid, CNodeStateStats& stats) const override; bool IgnoresIncomingTxs() override { return m_ignore_incoming_txs; } @@ -652,7 +653,7 @@ private: * @return True if address relay is enabled with peer * False if address relay is disallowed */ - bool SetupAddressRelay(CNode& node, Peer& peer); + bool SetupAddressRelay(const CNode& node, Peer& peer); }; } // namespace @@ -1419,14 +1420,14 @@ bool PeerManagerImpl::BlockRequestAllowed(const CBlockIndex* pindex) } std::unique_ptr<PeerManager> PeerManager::make(const CChainParams& chainparams, CConnman& connman, CAddrMan& addrman, - BanMan* banman, CScheduler& scheduler, ChainstateManager& chainman, + BanMan* banman, ChainstateManager& chainman, CTxMemPool& pool, bool ignore_incoming_txs) { - return std::make_unique<PeerManagerImpl>(chainparams, connman, addrman, banman, scheduler, chainman, pool, ignore_incoming_txs); + return std::make_unique<PeerManagerImpl>(chainparams, connman, addrman, banman, chainman, pool, ignore_incoming_txs); } PeerManagerImpl::PeerManagerImpl(const CChainParams& chainparams, CConnman& connman, CAddrMan& addrman, - BanMan* banman, CScheduler& scheduler, ChainstateManager& chainman, + BanMan* banman, ChainstateManager& chainman, CTxMemPool& pool, bool ignore_incoming_txs) : m_chainparams(chainparams), m_connman(connman), @@ -1436,6 +1437,10 @@ PeerManagerImpl::PeerManagerImpl(const CChainParams& chainparams, CConnman& conn m_mempool(pool), m_ignore_incoming_txs(ignore_incoming_txs) { +} + +void PeerManagerImpl::StartScheduledTasks(CScheduler& scheduler) +{ // Stale tip checking and peer eviction are on two different timers, but we // don't want them to get out of sync due to drift in the scheduler, so we // combine them in one function and schedule at the quicker (peer-eviction) @@ -2843,11 +2848,12 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type, return; // Apply rate limiting. - if (rate_limited) { - if (peer->m_addr_token_bucket < 1.0) { + if (peer->m_addr_token_bucket < 1.0) { + if (rate_limited) { ++num_rate_limit; continue; } + } else { peer->m_addr_token_bucket -= 1.0; } // We only bother storing full nodes, though this may include @@ -2875,12 +2881,8 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type, } peer->m_addr_processed += num_proc; peer->m_addr_rate_limited += num_rate_limit; - LogPrint(BCLog::NET, "Received addr: %u addresses (%u processed, %u rate-limited) from peer=%d%s\n", - vAddr.size(), - num_proc, - num_rate_limit, - pfrom.GetId(), - fLogIPs ? ", peeraddr=" + pfrom.addr.ToString() : ""); + LogPrint(BCLog::NET, "Received addr: %u addresses (%u processed, %u rate-limited) from peer=%d\n", + vAddr.size(), num_proc, num_rate_limit, pfrom.GetId()); m_addrman.Add(vAddrOk, pfrom.addr, 2 * 60 * 60); if (vAddr.size() < 1000) peer->m_getaddr_sent = false; @@ -3744,7 +3746,9 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type, return; } - SetupAddressRelay(pfrom, *peer); + // Since this must be an inbound connection, SetupAddressRelay will + // never fail. + Assume(SetupAddressRelay(pfrom, *peer)); // Only send one GetAddr response per connection to reduce resource waste // and discourage addr stamping of INV announcements. @@ -4461,7 +4465,7 @@ public: }; } -bool PeerManagerImpl::SetupAddressRelay(CNode& node, Peer& peer) +bool PeerManagerImpl::SetupAddressRelay(const CNode& node, Peer& peer) { // We don't participate in addr relay with outbound block-relay-only // connections to prevent providing adversaries with the additional diff --git a/src/net_processing.h b/src/net_processing.h index 4532a0505e..9d8d788583 100644 --- a/src/net_processing.h +++ b/src/net_processing.h @@ -38,10 +38,13 @@ class PeerManager : public CValidationInterface, public NetEventsInterface { public: static std::unique_ptr<PeerManager> make(const CChainParams& chainparams, CConnman& connman, CAddrMan& addrman, - BanMan* banman, CScheduler& scheduler, ChainstateManager& chainman, + BanMan* banman, ChainstateManager& chainman, CTxMemPool& pool, bool ignore_incoming_txs); virtual ~PeerManager() { } + /** Begin running background tasks, should only be called once */ + virtual void StartScheduledTasks(CScheduler& scheduler) = 0; + /** Get statistics from node state */ virtual bool GetNodeStateStats(NodeId nodeid, CNodeStateStats& stats) const = 0; diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index 183b5a5d91..b46ad0333e 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -334,6 +334,7 @@ bool FillBlock(const CBlockIndex* index, const FoundBlock& block, UniqueLock<Rec REVERSE_LOCK(lock); if (!ReadBlockFromDisk(*block.m_data, index, Params().GetConsensus())) block.m_data->SetNull(); } + block.found = true; return true; } @@ -660,6 +661,14 @@ public: RPCRunLater(name, std::move(fn), seconds); } int rpcSerializationFlags() override { return RPCSerializationFlags(); } + util::SettingsValue getSetting(const std::string& name) override + { + return gArgs.GetSetting(name); + } + std::vector<util::SettingsValue> getSettingsList(const std::string& name) override + { + return gArgs.GetSettingsList(name); + } util::SettingsValue getRwSetting(const std::string& name) override { util::SettingsValue result; @@ -670,7 +679,7 @@ public: }); return result; } - bool updateRwSetting(const std::string& name, const util::SettingsValue& value) override + bool updateRwSetting(const std::string& name, const util::SettingsValue& value, bool write) override { gArgs.LockSettings([&](util::Settings& settings) { if (value.isNull()) { @@ -679,7 +688,7 @@ public: settings.rw_settings[name] = value; } }); - return gArgs.WriteSettingsFile(); + return !write || gArgs.WriteSettingsFile(); } void requestMempoolTransactions(Notifications& notifications) override { diff --git a/src/outputtype.cpp b/src/outputtype.cpp index 8ede7b9974..b5f1df9792 100644 --- a/src/outputtype.cpp +++ b/src/outputtype.cpp @@ -13,6 +13,7 @@ #include <util/vector.h> #include <assert.h> +#include <optional> #include <string> static const std::string OUTPUT_TYPE_STRING_LEGACY = "legacy"; @@ -20,22 +21,18 @@ static const std::string OUTPUT_TYPE_STRING_P2SH_SEGWIT = "p2sh-segwit"; static const std::string OUTPUT_TYPE_STRING_BECH32 = "bech32"; static const std::string OUTPUT_TYPE_STRING_BECH32M = "bech32m"; -bool ParseOutputType(const std::string& type, OutputType& output_type) +std::optional<OutputType> ParseOutputType(const std::string& type) { if (type == OUTPUT_TYPE_STRING_LEGACY) { - output_type = OutputType::LEGACY; - return true; + return OutputType::LEGACY; } else if (type == OUTPUT_TYPE_STRING_P2SH_SEGWIT) { - output_type = OutputType::P2SH_SEGWIT; - return true; + return OutputType::P2SH_SEGWIT; } else if (type == OUTPUT_TYPE_STRING_BECH32) { - output_type = OutputType::BECH32; - return true; + return OutputType::BECH32; } else if (type == OUTPUT_TYPE_STRING_BECH32M) { - output_type = OutputType::BECH32M; - return true; + return OutputType::BECH32M; } - return false; + return std::nullopt; } const std::string& FormatOutputType(OutputType type) diff --git a/src/outputtype.h b/src/outputtype.h index 2b83235cd0..0de7689125 100644 --- a/src/outputtype.h +++ b/src/outputtype.h @@ -11,6 +11,7 @@ #include <script/standard.h> #include <array> +#include <optional> #include <string> #include <vector> @@ -28,7 +29,7 @@ static constexpr auto OUTPUT_TYPES = std::array{ OutputType::BECH32M, }; -[[nodiscard]] bool ParseOutputType(const std::string& str, OutputType& output_type); +std::optional<OutputType> ParseOutputType(const std::string& str); const std::string& FormatOutputType(OutputType type); /** diff --git a/src/qt/bitcoin.cpp b/src/qt/bitcoin.cpp index 4ab4e388d9..f6ea147ddb 100644 --- a/src/qt/bitcoin.cpp +++ b/src/qt/bitcoin.cpp @@ -28,6 +28,7 @@ #include <qt/utilitydialog.h> #include <qt/winshutdownmonitor.h> #include <uint256.h> +#include <util/string.h> #include <util/system.h> #include <util/threadnames.h> #include <util/translation.h> @@ -144,6 +145,53 @@ static void initTranslations(QTranslator &qtTranslatorBase, QTranslator &qtTrans QApplication::installTranslator(&translator); } +static bool InitSettings() +{ + if (!gArgs.GetSettingsPath()) { + return true; // Do nothing if settings file disabled. + } + + std::vector<std::string> errors; + if (!gArgs.ReadSettingsFile(&errors)) { + bilingual_str error = _("Settings file could not be read"); + InitError(Untranslated(strprintf("%s:\n%s\n", error.original, MakeUnorderedList(errors)))); + + QMessageBox messagebox(QMessageBox::Critical, PACKAGE_NAME, QString::fromStdString(strprintf("%s.", error.translated)), QMessageBox::Reset | QMessageBox::Abort); + /*: Explanatory text shown on startup when the settings file cannot be read. + Prompts user to make a choice between resetting or aborting. */ + messagebox.setInformativeText(QObject::tr("Do you want to reset settings to default values, or to abort without making changes?")); + messagebox.setDetailedText(QString::fromStdString(MakeUnorderedList(errors))); + messagebox.setTextFormat(Qt::PlainText); + messagebox.setDefaultButton(QMessageBox::Reset); + switch (messagebox.exec()) { + case QMessageBox::Reset: + break; + case QMessageBox::Abort: + return false; + default: + assert(false); + } + } + + errors.clear(); + if (!gArgs.WriteSettingsFile(&errors)) { + bilingual_str error = _("Settings file could not be written"); + InitError(Untranslated(strprintf("%s:\n%s\n", error.original, MakeUnorderedList(errors)))); + + QMessageBox messagebox(QMessageBox::Critical, PACKAGE_NAME, QString::fromStdString(strprintf("%s.", error.translated)), QMessageBox::Ok); + /*: Explanatory text shown on startup when the settings file could not be written. + Prompts user to check that we have the ability to write to the file. + Explains that the user has the option of running without a settings file.*/ + messagebox.setInformativeText(QObject::tr("A fatal error occurred. Check that settings file is writable, or try running with -nosettings.")); + messagebox.setDetailedText(QString::fromStdString(MakeUnorderedList(errors))); + messagebox.setTextFormat(Qt::PlainText); + messagebox.setDefaultButton(QMessageBox::Ok); + messagebox.exec(); + return false; + } + return true; +} + /* qDebug() message handler --> debug.log */ void DebugMessageHandler(QtMsgType type, const QMessageLogContext& context, const QString &msg) { @@ -295,6 +343,17 @@ void BitcoinApplication::requestShutdown() window->setClientModel(nullptr); pollShutdownTimer->stop(); +#ifdef ENABLE_WALLET + // Delete wallet controller here manually, instead of relying on Qt object + // tracking (https://doc.qt.io/qt-5/objecttrees.html). This makes sure + // walletmodel m_handle_* notification handlers are deleted before wallets + // are unloaded, which can simplify wallet implementations. It also avoids + // these notifications having to be handled while GUI objects are being + // destroyed, making GUI code less fragile as well. + delete m_wallet_controller; + m_wallet_controller = nullptr; +#endif // ENABLE_WALLET + delete clientModel; clientModel = nullptr; @@ -512,9 +571,8 @@ int GuiMain(int argc, char* argv[]) // Parse URIs on command line -- this can affect Params() PaymentServer::ipcParseCommandLine(argc, argv); #endif - if (!gArgs.InitSettings(error)) { - InitError(Untranslated(error)); - QMessageBox::critical(nullptr, PACKAGE_NAME, QObject::tr("Error initializing settings: %1").arg(QString::fromStdString(error))); + + if (!InitSettings()) { return EXIT_FAILURE; } diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index f8aeb01659..fe606519af 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -110,6 +110,9 @@ BitcoinGUI::BitcoinGUI(interfaces::Node& node, const PlatformStyle *_platformSty connect(activity, &CreateWalletActivity::finished, activity, &QObject::deleteLater); activity->create(); }); + connect(walletFrame, &WalletFrame::message, [this](const QString& title, const QString& message, unsigned int style) { + this->message(title, message, style); + }); setCentralWidget(walletFrame); } else #endif // ENABLE_WALLET @@ -682,8 +685,6 @@ void BitcoinGUI::addWallet(WalletModel* walletModel) m_wallet_selector_label_action->setVisible(true); m_wallet_selector_action->setVisible(true); } - const QString display_name = walletModel->getDisplayName(); - m_wallet_selector->addItem(display_name, QVariant::fromValue(walletModel)); connect(wallet_view, &WalletView::outOfSyncWarningClicked, this, &BitcoinGUI::showModalOverlay); connect(wallet_view, &WalletView::transactionClicked, this, &BitcoinGUI::gotoHistoryPage); @@ -696,6 +697,8 @@ void BitcoinGUI::addWallet(WalletModel* walletModel) connect(wallet_view, &WalletView::hdEnabledStatusChanged, this, &BitcoinGUI::updateWalletStatus); connect(this, &BitcoinGUI::setPrivacy, wallet_view, &WalletView::setPrivacy); wallet_view->setPrivacy(isPrivacyModeActivated()); + const QString display_name = walletModel->getDisplayName(); + m_wallet_selector->addItem(display_name, QVariant::fromValue(walletModel)); } void BitcoinGUI::removeWallet(WalletModel* walletModel) diff --git a/src/qt/createwalletdialog.cpp b/src/qt/createwalletdialog.cpp index dc24bbc6a6..f9a61c3e60 100644 --- a/src/qt/createwalletdialog.cpp +++ b/src/qt/createwalletdialog.cpp @@ -32,7 +32,7 @@ CreateWalletDialog::CreateWalletDialog(QWidget* parent) : // set to true, enable it when isEncryptWalletChecked is false. ui->disable_privkeys_checkbox->setEnabled(!checked); #ifdef ENABLE_EXTERNAL_SIGNER - ui->external_signer_checkbox->setEnabled(!checked); + ui->external_signer_checkbox->setEnabled(m_has_signers && !checked); #endif // When the disable_privkeys_checkbox is disabled, uncheck it. if (!ui->disable_privkeys_checkbox->isEnabled()) { @@ -115,7 +115,8 @@ CreateWalletDialog::~CreateWalletDialog() void CreateWalletDialog::setSigners(const std::vector<ExternalSigner>& signers) { - if (!signers.empty()) { + m_has_signers = !signers.empty(); + if (m_has_signers) { ui->external_signer_checkbox->setEnabled(true); ui->external_signer_checkbox->setChecked(true); ui->encrypt_wallet_checkbox->setEnabled(false); diff --git a/src/qt/createwalletdialog.h b/src/qt/createwalletdialog.h index 25ddf97585..fc13cc44eb 100644 --- a/src/qt/createwalletdialog.h +++ b/src/qt/createwalletdialog.h @@ -35,6 +35,7 @@ public: private: Ui::CreateWalletDialog *ui; + bool m_has_signers = false; }; #endif // BITCOIN_QT_CREATEWALLETDIALOG_H diff --git a/src/qt/forms/optionsdialog.ui b/src/qt/forms/optionsdialog.ui index bd72328c02..2ff1445709 100644 --- a/src/qt/forms/optionsdialog.ui +++ b/src/qt/forms/optionsdialog.ui @@ -51,20 +51,20 @@ </spacer> </item> <item> - <layout class="QHBoxLayout" name="horizontalLayout_Main_Prune"> - <item> - <widget class="QCheckBox" name="prune"> - <property name="toolTip"> - <string>Enabling pruning significantly reduces the disk space required to store transactions. All blocks are still fully validated. Reverting this setting requires re-downloading the entire blockchain.</string> - </property> - <property name="text"> - <string>Prune &block storage to</string> - </property> - </widget> - </item> - <item> - <widget class="QSpinBox" name="pruneSize"/> - </item> + <layout class="QHBoxLayout" name="horizontalLayout_Main_Prune"> + <item> + <widget class="QCheckBox" name="prune"> + <property name="toolTip"> + <string>Enabling pruning significantly reduces the disk space required to store transactions. All blocks are still fully validated. Reverting this setting requires re-downloading the entire blockchain.</string> + </property> + <property name="text"> + <string>Prune &block storage to</string> + </property> + </widget> + </item> + <item> + <widget class="QSpinBox" name="pruneSize"/> + </item> <item> <widget class="QLabel" name="pruneSizeUnitLabel"> <property name="text"> @@ -201,6 +201,16 @@ </attribute> <layout class="QVBoxLayout" name="verticalLayout_Wallet"> <item> + <widget class="QCheckBox" name="subFeeFromAmount"> + <property name="toolTip"> + <string extracomment="Tooltip text for Options window setting that sets subtracting the fee from a sending amount as default.">Whether to set subtract fee from amount as default or not.</string> + </property> + <property name="text"> + <string extracomment="An Options window setting to set subtracting the fee from a sending amount as default.">Subtract &fee from amount by default</string> + </property> + </widget> + </item> + <item> <widget class="QGroupBox" name="groupBox"> <property name="title"> <string>Expert</string> @@ -235,27 +245,27 @@ <string>External Signer (e.g. hardware wallet)</string> </property> <layout class="QVBoxLayout" name="verticalLayoutHww"> - <item> - <layout class="QHBoxLayout" name="horizontalLayoutHww"> - <item> - <widget class="QLabel" name="externalSignerPathLabel"> - <property name="text"> - <string>&External signer script path</string> - </property> - <property name="buddy"> - <cstring>externalSignerPath</cstring> - </property> - </widget> - </item> - <item> - <widget class="QLineEdit" name="externalSignerPath"> - <property name="toolTip"> - <string>Full path to a Bitcoin Core compatible script (e.g. C:\Downloads\hwi.exe or /Users/you/Downloads/hwi.py). Beware: malware can steal your coins!</string> - </property> - </widget> - </item> - </layout> - </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayoutHww"> + <item> + <widget class="QLabel" name="externalSignerPathLabel"> + <property name="text"> + <string>&External signer script path</string> + </property> + <property name="buddy"> + <cstring>externalSignerPath</cstring> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="externalSignerPath"> + <property name="toolTip"> + <string>Full path to a Bitcoin Core compatible script (e.g. C:\Downloads\hwi.exe or /Users/you/Downloads/hwi.py). Beware: malware can steal your coins!</string> + </property> + </widget> + </item> + </layout> + </item> </layout> </widget> </item> diff --git a/src/qt/optionsdialog.cpp b/src/qt/optionsdialog.cpp index b12fe96567..5ad4fc9b33 100644 --- a/src/qt/optionsdialog.cpp +++ b/src/qt/optionsdialog.cpp @@ -239,6 +239,7 @@ void OptionsDialog::setMapper() /* Wallet */ mapper->addMapping(ui->spendZeroConfChange, OptionsModel::SpendZeroConfChange); mapper->addMapping(ui->coinControlFeatures, OptionsModel::CoinControlFeatures); + mapper->addMapping(ui->subFeeFromAmount, OptionsModel::SubFeeFromAmount); mapper->addMapping(ui->externalSignerPath, OptionsModel::ExternalSignerPath); /* Network */ diff --git a/src/qt/optionsmodel.cpp b/src/qt/optionsmodel.cpp index 24a4e9ee96..d87fc1f84a 100644 --- a/src/qt/optionsmodel.cpp +++ b/src/qt/optionsmodel.cpp @@ -124,6 +124,11 @@ void OptionsModel::Init(bool resetSettings) if (!gArgs.SoftSetArg("-signer", settings.value("external_signer_path").toString().toStdString())) { addOverriddenOption("-signer"); } + + if (!settings.contains("SubFeeFromAmount")) { + settings.setValue("SubFeeFromAmount", false); + } + m_sub_fee_from_amount = settings.value("SubFeeFromAmount", false).toBool(); #endif // Network @@ -335,6 +340,8 @@ QVariant OptionsModel::data(const QModelIndex & index, int role) const return settings.value("bSpendZeroConfChange"); case ExternalSignerPath: return settings.value("external_signer_path"); + case SubFeeFromAmount: + return m_sub_fee_from_amount; #endif case DisplayUnit: return nDisplayUnit; @@ -460,6 +467,10 @@ bool OptionsModel::setData(const QModelIndex & index, const QVariant & value, in setRestartRequired(true); } break; + case SubFeeFromAmount: + m_sub_fee_from_amount = value.toBool(); + settings.setValue("SubFeeFromAmount", m_sub_fee_from_amount); + break; #endif case DisplayUnit: setDisplayUnit(value); diff --git a/src/qt/optionsmodel.h b/src/qt/optionsmodel.h index 535843e8ba..203ee27ad8 100644 --- a/src/qt/optionsmodel.h +++ b/src/qt/optionsmodel.h @@ -61,6 +61,7 @@ public: Language, // QString UseEmbeddedMonospacedFont, // bool CoinControlFeatures, // bool + SubFeeFromAmount, // bool ThreadsScriptVerif, // int Prune, // bool PruneSize, // int @@ -88,6 +89,7 @@ public: QString getThirdPartyTxUrls() const { return strThirdPartyTxUrls; } bool getUseEmbeddedMonospacedFont() const { return m_use_embedded_monospaced_font; } bool getCoinControlFeatures() const { return fCoinControlFeatures; } + bool getSubFeeFromAmount() const { return m_sub_fee_from_amount; } const QString& getOverriddenByCommandLine() { return strOverriddenByCommandLine; } /* Explicit setters */ @@ -112,6 +114,7 @@ private: QString strThirdPartyTxUrls; bool m_use_embedded_monospaced_font; bool fCoinControlFeatures; + bool m_sub_fee_from_amount; /* settings that were overridden by command-line */ QString strOverriddenByCommandLine; diff --git a/src/qt/peertablemodel.cpp b/src/qt/peertablemodel.cpp index 1b7fda6e77..98efaf29d7 100644 --- a/src/qt/peertablemodel.cpp +++ b/src/qt/peertablemodel.cpp @@ -72,8 +72,13 @@ QVariant PeerTableModel::data(const QModelIndex& index, int role) const case NetNodeId: return (qint64)rec->nodeStats.nodeid; case Address: - // prepend to peer address down-arrow symbol for inbound connection and up-arrow for outbound connection - return QString::fromStdString((rec->nodeStats.fInbound ? "↓ " : "↑ ") + rec->nodeStats.addrName); + return QString::fromStdString(rec->nodeStats.addrName); + case Direction: + return QString(rec->nodeStats.fInbound ? + //: An Inbound Connection from a Peer. + tr("Inbound") : + //: An Outbound Connection to a Peer. + tr("Outbound")); case ConnectionType: return GUIUtil::ConnectionTypeToQString(rec->nodeStats.m_conn_type, /* prepend_direction */ false); case Network: @@ -94,6 +99,7 @@ QVariant PeerTableModel::data(const QModelIndex& index, int role) const return QVariant(Qt::AlignRight | Qt::AlignVCenter); case Address: return {}; + case Direction: case ConnectionType: case Network: return QVariant(Qt::AlignCenter); diff --git a/src/qt/peertablemodel.h b/src/qt/peertablemodel.h index 0d841ebf28..40265ee266 100644 --- a/src/qt/peertablemodel.h +++ b/src/qt/peertablemodel.h @@ -48,6 +48,7 @@ public: enum ColumnIndex { NetNodeId = 0, Address, + Direction, ConnectionType, Network, Ping, @@ -84,6 +85,9 @@ private: /*: Title of Peers Table column which contains the IP/Onion/I2P address of the connected peer. */ tr("Address"), + /*: Title of Peers Table column which indicates the direction + the peer connection was initiated from. */ + tr("Direction"), /*: Title of Peers Table column which describes the type of peer connection. The "type" describes why the connection exists. */ tr("Type"), diff --git a/src/qt/peertablesortproxy.cpp b/src/qt/peertablesortproxy.cpp index 78932da8d4..f92eef48f1 100644 --- a/src/qt/peertablesortproxy.cpp +++ b/src/qt/peertablesortproxy.cpp @@ -26,6 +26,8 @@ bool PeerTableSortProxy::lessThan(const QModelIndex& left_index, const QModelInd return left_stats.nodeid < right_stats.nodeid; case PeerTableModel::Address: return left_stats.addrName.compare(right_stats.addrName) < 0; + case PeerTableModel::Direction: + return left_stats.fInbound > right_stats.fInbound; // default sort Inbound, then Outbound case PeerTableModel::ConnectionType: return left_stats.m_conn_type < right_stats.m_conn_type; case PeerTableModel::Network: diff --git a/src/qt/psbtoperationsdialog.cpp b/src/qt/psbtoperationsdialog.cpp index 2adfeeaaf0..289fb9f7c8 100644 --- a/src/qt/psbtoperationsdialog.cpp +++ b/src/qt/psbtoperationsdialog.cpp @@ -47,18 +47,22 @@ void PSBTOperationsDialog::openWithPSBT(PartiallySignedTransaction psbtx) { m_transaction_data = psbtx; - bool complete; - size_t n_could_sign; - FinalizePSBT(psbtx); // Make sure all existing signatures are fully combined before checking for completeness. - TransactionError err = m_wallet_model->wallet().fillPSBT(SIGHASH_ALL, false /* sign */, true /* bip32derivs */, &n_could_sign, m_transaction_data, complete); - if (err != TransactionError::OK) { - showStatus(tr("Failed to load transaction: %1") - .arg(QString::fromStdString(TransactionErrorString(err).translated)), StatusLevel::ERR); - return; + bool complete = FinalizePSBT(psbtx); // Make sure all existing signatures are fully combined before checking for completeness. + if (m_wallet_model) { + size_t n_could_sign; + TransactionError err = m_wallet_model->wallet().fillPSBT(SIGHASH_ALL, false /* sign */, true /* bip32derivs */, &n_could_sign, m_transaction_data, complete); + if (err != TransactionError::OK) { + showStatus(tr("Failed to load transaction: %1") + .arg(QString::fromStdString(TransactionErrorString(err).translated)), + StatusLevel::ERR); + return; + } + m_ui->signTransactionButton->setEnabled(!complete && !m_wallet_model->wallet().privateKeysDisabled() && n_could_sign > 0); + } else { + m_ui->signTransactionButton->setEnabled(false); } m_ui->broadcastTransactionButton->setEnabled(complete); - m_ui->signTransactionButton->setEnabled(!complete && !m_wallet_model->wallet().privateKeysDisabled() && n_could_sign > 0); updateTransactionDisplay(); } @@ -133,7 +137,7 @@ void PSBTOperationsDialog::saveTransaction() { } CTxDestination address; ExtractDestination(out.scriptPubKey, address); - QString amount = BitcoinUnits::format(m_wallet_model->getOptionsModel()->getDisplayUnit(), out.nValue); + QString amount = BitcoinUnits::format(m_client_model->getOptionsModel()->getDisplayUnit(), out.nValue); QString address_str = QString::fromStdString(EncodeDestination(address)); filename_suggestion.append(address_str + "-" + amount); first = false; @@ -224,6 +228,10 @@ void PSBTOperationsDialog::showStatus(const QString &msg, StatusLevel level) { } size_t PSBTOperationsDialog::couldSignInputs(const PartiallySignedTransaction &psbtx) { + if (!m_wallet_model) { + return 0; + } + size_t n_signed; bool complete; TransactionError err = m_wallet_model->wallet().fillPSBT(SIGHASH_ALL, false /* sign */, false /* bip32derivs */, &n_signed, m_transaction_data, complete); @@ -246,7 +254,10 @@ void PSBTOperationsDialog::showTransactionStatus(const PartiallySignedTransactio case PSBTRole::SIGNER: { QString need_sig_text = tr("Transaction still needs signature(s)."); StatusLevel level = StatusLevel::INFO; - if (m_wallet_model->wallet().privateKeysDisabled()) { + if (!m_wallet_model) { + need_sig_text += " " + tr("(But no wallet is loaded.)"); + level = StatusLevel::WARN; + } else if (m_wallet_model->wallet().privateKeysDisabled()) { need_sig_text += " " + tr("(But this wallet cannot sign transactions.)"); level = StatusLevel::WARN; } else if (n_could_sign < 1) { diff --git a/src/qt/sendcoinsentry.cpp b/src/qt/sendcoinsentry.cpp index 683c0441fa..5fa5165615 100644 --- a/src/qt/sendcoinsentry.cpp +++ b/src/qt/sendcoinsentry.cpp @@ -97,7 +97,9 @@ void SendCoinsEntry::clear() ui->payTo->clear(); ui->addAsLabel->clear(); ui->payAmount->clear(); - ui->checkboxSubtractFeeFromAmount->setCheckState(Qt::Unchecked); + if (model && model->getOptionsModel()) { + ui->checkboxSubtractFeeFromAmount->setChecked(model->getOptionsModel()->getSubFeeFromAmount()); + } ui->messageTextLabel->clear(); ui->messageTextLabel->hide(); ui->messageLabel->hide(); diff --git a/src/qt/test/addressbooktests.cpp b/src/qt/test/addressbooktests.cpp index 39c69fe184..022f367422 100644 --- a/src/qt/test/addressbooktests.cpp +++ b/src/qt/test/addressbooktests.cpp @@ -109,9 +109,10 @@ void TestAddAddressesToSendBook(interfaces::Node& node) std::unique_ptr<const PlatformStyle> platformStyle(PlatformStyle::instantiate("other")); OptionsModel optionsModel; ClientModel clientModel(node, &optionsModel); - AddWallet(wallet); - WalletModel walletModel(interfaces::MakeWallet(wallet), clientModel, platformStyle.get()); - RemoveWallet(wallet, std::nullopt); + WalletContext& context = *node.walletClient().context(); + AddWallet(context, wallet); + WalletModel walletModel(interfaces::MakeWallet(context, wallet), clientModel, platformStyle.get()); + RemoveWallet(context, wallet, /* load_on_startup= */ std::nullopt); EditAddressDialog editAddressDialog(EditAddressDialog::NewSendingAddress); editAddressDialog.setModel(walletModel.getAddressTableModel()); diff --git a/src/qt/test/apptests.cpp b/src/qt/test/apptests.cpp index 9c31cd50df..8489b33144 100644 --- a/src/qt/test/apptests.cpp +++ b/src/qt/test/apptests.cpp @@ -12,7 +12,6 @@ #include <qt/rpcconsole.h> #include <shutdown.h> #include <test/util/setup_common.h> -#include <univalue.h> #include <validation.h> #if defined(HAVE_CONFIG_H) @@ -21,8 +20,10 @@ #include <QAction> #include <QLineEdit> +#include <QRegularExpression> #include <QScopedPointer> #include <QSignalSpy> +#include <QString> #include <QTest> #include <QTextEdit> #include <QtGlobal> @@ -30,6 +31,13 @@ #include <QtTest/QtTestGui> namespace { +//! Regex find a string group inside of the console output +QString FindInConsole(const QString& output, const QString& pattern) +{ + const QRegularExpression re(pattern); + return re.match(output).captured(1); +} + //! Call getblockchaininfo RPC and check first field of JSON output. void TestRpcCommand(RPCConsole* console) { @@ -41,10 +49,9 @@ void TestRpcCommand(RPCConsole* console) QTest::keyClick(lineEdit, Qt::Key_Return); QVERIFY(mw_spy.wait(1000)); QCOMPARE(mw_spy.count(), 4); - QString output = messagesWidget->toPlainText(); - UniValue value; - value.read(output.right(output.size() - output.lastIndexOf(QChar::ObjectReplacementCharacter) - 1).toStdString()); - QCOMPARE(value["chain"].get_str(), std::string("regtest")); + const QString output = messagesWidget->toPlainText(); + const QString pattern = QStringLiteral("\"chain\": \"(\\w+)\""); + QCOMPARE(FindInConsole(output, pattern), QString("regtest")); } } // namespace diff --git a/src/qt/test/wallettests.cpp b/src/qt/test/wallettests.cpp index e883337fb5..1976bee74b 100644 --- a/src/qt/test/wallettests.cpp +++ b/src/qt/test/wallettests.cpp @@ -164,9 +164,10 @@ void TestGUI(interfaces::Node& node) TransactionView transactionView(platformStyle.get()); OptionsModel optionsModel; ClientModel clientModel(node, &optionsModel); - AddWallet(wallet); - WalletModel walletModel(interfaces::MakeWallet(wallet), clientModel, platformStyle.get()); - RemoveWallet(wallet, std::nullopt); + WalletContext& context = *node.walletClient().context(); + AddWallet(context, wallet); + WalletModel walletModel(interfaces::MakeWallet(context, wallet), clientModel, platformStyle.get()); + RemoveWallet(context, wallet, /* load_on_startup= */ std::nullopt); sendCoinsDialog.setModel(&walletModel); transactionView.setModel(&walletModel); diff --git a/src/qt/transactionfilterproxy.cpp b/src/qt/transactionfilterproxy.cpp index a631f497af..75cbd6b3be 100644 --- a/src/qt/transactionfilterproxy.cpp +++ b/src/qt/transactionfilterproxy.cpp @@ -9,15 +9,8 @@ #include <cstdlib> -// Earliest date that can be represented (far in the past) -const QDateTime TransactionFilterProxy::MIN_DATE = QDateTime::fromTime_t(0); -// Last date that can be represented (far in the future) -const QDateTime TransactionFilterProxy::MAX_DATE = QDateTime::fromTime_t(0xFFFFFFFF); - TransactionFilterProxy::TransactionFilterProxy(QObject *parent) : QSortFilterProxyModel(parent), - dateFrom(MIN_DATE), - dateTo(MAX_DATE), m_search_string(), typeFilter(ALL_TYPES), watchOnlyFilter(WatchOnlyFilter_All), @@ -46,8 +39,8 @@ bool TransactionFilterProxy::filterAcceptsRow(int sourceRow, const QModelIndex & return false; QDateTime datetime = index.data(TransactionTableModel::DateRole).toDateTime(); - if (datetime < dateFrom || datetime > dateTo) - return false; + if (dateFrom && datetime < *dateFrom) return false; + if (dateTo && datetime > *dateTo) return false; QString address = index.data(TransactionTableModel::AddressRole).toString(); QString label = index.data(TransactionTableModel::LabelRole).toString(); @@ -65,10 +58,10 @@ bool TransactionFilterProxy::filterAcceptsRow(int sourceRow, const QModelIndex & return true; } -void TransactionFilterProxy::setDateRange(const QDateTime &from, const QDateTime &to) +void TransactionFilterProxy::setDateRange(const std::optional<QDateTime>& from, const std::optional<QDateTime>& to) { - this->dateFrom = from; - this->dateTo = to; + dateFrom = from; + dateTo = to; invalidateFilter(); } diff --git a/src/qt/transactionfilterproxy.h b/src/qt/transactionfilterproxy.h index 693b363692..09bc9e75db 100644 --- a/src/qt/transactionfilterproxy.h +++ b/src/qt/transactionfilterproxy.h @@ -10,6 +10,8 @@ #include <QDateTime> #include <QSortFilterProxyModel> +#include <optional> + /** Filter the transaction list according to pre-specified rules. */ class TransactionFilterProxy : public QSortFilterProxyModel { @@ -18,10 +20,6 @@ class TransactionFilterProxy : public QSortFilterProxyModel public: explicit TransactionFilterProxy(QObject *parent = nullptr); - /** Earliest date that can be represented (far in the past) */ - static const QDateTime MIN_DATE; - /** Last date that can be represented (far in the future) */ - static const QDateTime MAX_DATE; /** Type filter bit field (all types) */ static const quint32 ALL_TYPES = 0xFFFFFFFF; @@ -34,7 +32,8 @@ public: WatchOnlyFilter_No }; - void setDateRange(const QDateTime &from, const QDateTime &to); + /** Filter transactions between date range. Use std::nullopt for open range. */ + void setDateRange(const std::optional<QDateTime>& from, const std::optional<QDateTime>& to); void setSearchString(const QString &); /** @note Type filter takes a bit field created with TYPE() or ALL_TYPES @@ -55,8 +54,8 @@ protected: bool filterAcceptsRow(int source_row, const QModelIndex & source_parent) const override; private: - QDateTime dateFrom; - QDateTime dateTo; + std::optional<QDateTime> dateFrom; + std::optional<QDateTime> dateTo; QString m_search_string; quint32 typeFilter; WatchOnlyFilter watchOnlyFilter; diff --git a/src/qt/transactionview.cpp b/src/qt/transactionview.cpp index 83d17a32c0..908cb917f1 100644 --- a/src/qt/transactionview.cpp +++ b/src/qt/transactionview.cpp @@ -19,6 +19,8 @@ #include <node/ui_interface.h> +#include <optional> + #include <QApplication> #include <QComboBox> #include <QDateTimeEdit> @@ -266,26 +268,26 @@ void TransactionView::chooseDate(int idx) { case All: transactionProxyModel->setDateRange( - TransactionFilterProxy::MIN_DATE, - TransactionFilterProxy::MAX_DATE); + std::nullopt, + std::nullopt); break; case Today: transactionProxyModel->setDateRange( GUIUtil::StartOfDay(current), - TransactionFilterProxy::MAX_DATE); + std::nullopt); break; case ThisWeek: { // Find last Monday QDate startOfWeek = current.addDays(-(current.dayOfWeek()-1)); transactionProxyModel->setDateRange( GUIUtil::StartOfDay(startOfWeek), - TransactionFilterProxy::MAX_DATE); + std::nullopt); } break; case ThisMonth: transactionProxyModel->setDateRange( GUIUtil::StartOfDay(QDate(current.year(), current.month(), 1)), - TransactionFilterProxy::MAX_DATE); + std::nullopt); break; case LastMonth: transactionProxyModel->setDateRange( @@ -295,7 +297,7 @@ void TransactionView::chooseDate(int idx) case ThisYear: transactionProxyModel->setDateRange( GUIUtil::StartOfDay(QDate(current.year(), 1, 1)), - TransactionFilterProxy::MAX_DATE); + std::nullopt); break; case Range: dateRangeWidget->setVisible(true); diff --git a/src/qt/walletframe.cpp b/src/qt/walletframe.cpp index a1f357e0db..3d8bc0c7c5 100644 --- a/src/qt/walletframe.cpp +++ b/src/qt/walletframe.cpp @@ -4,12 +4,18 @@ #include <qt/walletframe.h> +#include <node/ui_interface.h> +#include <psbt.h> +#include <qt/guiutil.h> #include <qt/overviewpage.h> +#include <qt/psbtoperationsdialog.h> #include <qt/walletmodel.h> #include <qt/walletview.h> #include <cassert> +#include <QApplication> +#include <QClipboard> #include <QGroupBox> #include <QHBoxLayout> #include <QLabel> @@ -184,10 +190,40 @@ void WalletFrame::gotoVerifyMessageTab(QString addr) void WalletFrame::gotoLoadPSBT(bool from_clipboard) { - WalletView *walletView = currentWalletView(); - if (walletView) { - walletView->gotoLoadPSBT(from_clipboard); + std::string data; + + if (from_clipboard) { + std::string raw = QApplication::clipboard()->text().toStdString(); + bool invalid; + data = DecodeBase64(raw, &invalid); + if (invalid) { + Q_EMIT message(tr("Error"), tr("Unable to decode PSBT from clipboard (invalid base64)"), CClientUIInterface::MSG_ERROR); + return; + } + } else { + QString filename = GUIUtil::getOpenFileName(this, + tr("Load Transaction Data"), QString(), + tr("Partially Signed Transaction (*.psbt)"), nullptr); + if (filename.isEmpty()) return; + if (GetFileSize(filename.toLocal8Bit().data(), MAX_FILE_SIZE_PSBT) == MAX_FILE_SIZE_PSBT) { + Q_EMIT message(tr("Error"), tr("PSBT file must be smaller than 100 MiB"), CClientUIInterface::MSG_ERROR); + return; + } + std::ifstream in(filename.toLocal8Bit().data(), std::ios::binary); + data = std::string(std::istreambuf_iterator<char>{in}, {}); } + + std::string error; + PartiallySignedTransaction psbtx; + if (!DecodeRawPSBT(psbtx, data, error)) { + Q_EMIT message(tr("Error"), tr("Unable to decode PSBT") + "\n" + QString::fromStdString(error), CClientUIInterface::MSG_ERROR); + return; + } + + PSBTOperationsDialog* dlg = new PSBTOperationsDialog(this, currentWalletModel(), clientModel); + dlg->openWithPSBT(psbtx); + dlg->setAttribute(Qt::WA_DeleteOnClose); + dlg->exec(); } void WalletFrame::encryptWallet() diff --git a/src/qt/walletframe.h b/src/qt/walletframe.h index 4f77bd716f..fe42293abc 100644 --- a/src/qt/walletframe.h +++ b/src/qt/walletframe.h @@ -48,6 +48,7 @@ public: Q_SIGNALS: void createWalletButtonClicked(); + void message(const QString& title, const QString& message, unsigned int style); private: QStackedWidget *walletStack; diff --git a/src/qt/walletview.cpp b/src/qt/walletview.cpp index 3b8cf4c7ed..2326af80b6 100644 --- a/src/qt/walletview.cpp +++ b/src/qt/walletview.cpp @@ -8,7 +8,6 @@ #include <qt/askpassphrasedialog.h> #include <qt/clientmodel.h> #include <qt/guiutil.h> -#include <qt/psbtoperationsdialog.h> #include <qt/optionsmodel.h> #include <qt/overviewpage.h> #include <qt/platformstyle.h> @@ -21,13 +20,10 @@ #include <interfaces/node.h> #include <node/ui_interface.h> -#include <psbt.h> #include <util/strencodings.h> #include <QAction> #include <QActionGroup> -#include <QApplication> -#include <QClipboard> #include <QFileDialog> #include <QHBoxLayout> #include <QProgressDialog> @@ -205,44 +201,6 @@ void WalletView::gotoVerifyMessageTab(QString addr) signVerifyMessageDialog->setAddress_VM(addr); } -void WalletView::gotoLoadPSBT(bool from_clipboard) -{ - std::string data; - - if (from_clipboard) { - std::string raw = QApplication::clipboard()->text().toStdString(); - bool invalid; - data = DecodeBase64(raw, &invalid); - if (invalid) { - Q_EMIT message(tr("Error"), tr("Unable to decode PSBT from clipboard (invalid base64)"), CClientUIInterface::MSG_ERROR); - return; - } - } else { - QString filename = GUIUtil::getOpenFileName(this, - tr("Load Transaction Data"), QString(), - tr("Partially Signed Transaction (*.psbt)"), nullptr); - if (filename.isEmpty()) return; - if (GetFileSize(filename.toLocal8Bit().data(), MAX_FILE_SIZE_PSBT) == MAX_FILE_SIZE_PSBT) { - Q_EMIT message(tr("Error"), tr("PSBT file must be smaller than 100 MiB"), CClientUIInterface::MSG_ERROR); - return; - } - std::ifstream in(filename.toLocal8Bit().data(), std::ios::binary); - data = std::string(std::istreambuf_iterator<char>{in}, {}); - } - - std::string error; - PartiallySignedTransaction psbtx; - if (!DecodeRawPSBT(psbtx, data, error)) { - Q_EMIT message(tr("Error"), tr("Unable to decode PSBT") + "\n" + QString::fromStdString(error), CClientUIInterface::MSG_ERROR); - return; - } - - PSBTOperationsDialog* dlg = new PSBTOperationsDialog(this, walletModel, clientModel); - dlg->openWithPSBT(psbtx); - dlg->setAttribute(Qt::WA_DeleteOnClose); - dlg->exec(); -} - bool WalletView::handlePaymentRequest(const SendCoinsRecipient& recipient) { return sendCoinsPage->handlePaymentRequest(recipient); diff --git a/src/qt/walletview.h b/src/qt/walletview.h index fedf06b710..5c42a9ffc0 100644 --- a/src/qt/walletview.h +++ b/src/qt/walletview.h @@ -83,8 +83,6 @@ public Q_SLOTS: void gotoSignMessageTab(QString addr = ""); /** Show Sign/Verify Message dialog and switch to verify message tab */ void gotoVerifyMessageTab(QString addr = ""); - /** Load Partially Signed Bitcoin Transaction */ - void gotoLoadPSBT(bool from_clipboard = false); /** Show incoming transaction notification for new transactions. diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index 4956ee39e9..909019d796 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -36,6 +36,7 @@ #include <txmempool.h> #include <undo.h> #include <util/strencodings.h> +#include <util/string.h> #include <util/system.h> #include <util/translation.h> #include <validation.h> @@ -1328,7 +1329,7 @@ static RPCHelpMan verifychain() "\nVerifies blockchain database.\n", { {"checklevel", RPCArg::Type::NUM, RPCArg::DefaultHint{strprintf("%d, range=0-4", DEFAULT_CHECKLEVEL)}, - strprintf("How thorough the block verification is:\n - %s", Join(CHECKLEVEL_DOC, "\n- "))}, + strprintf("How thorough the block verification is:\n%s", MakeUnorderedList(CHECKLEVEL_DOC))}, {"nblocks", RPCArg::Type::NUM, RPCArg::DefaultHint{strprintf("%d, 0=all", DEFAULT_CHECKBLOCKS)}, "The number of blocks to check."}, }, RPCResult{ diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 9c8582c7a3..4357ab2bb3 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -142,6 +142,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "importmulti", 0, "requests" }, { "importmulti", 1, "options" }, { "importdescriptors", 0, "requests" }, + { "listdescriptors", 0, "private" }, { "verifychain", 0, "checklevel" }, { "verifychain", 1, "nblocks" }, { "getblockstats", 0, "hash_or_height" }, @@ -186,6 +187,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "createwallet", 5, "descriptors"}, { "createwallet", 6, "load_on_startup"}, { "createwallet", 7, "external_signer"}, + { "restorewallet", 2, "load_on_startup"}, { "loadwallet", 1, "load_on_startup"}, { "unloadwallet", 1, "load_on_startup"}, { "getnodeaddresses", 0, "count"}, diff --git a/src/rpc/misc.cpp b/src/rpc/misc.cpp index 5178ce60e8..1a94abf6d3 100644 --- a/src/rpc/misc.cpp +++ b/src/rpc/misc.cpp @@ -24,6 +24,7 @@ #include <util/strencodings.h> #include <util/system.h> +#include <optional> #include <stdint.h> #include <tuple> #ifdef HAVE_MALLOC_INFO @@ -128,12 +129,13 @@ static RPCHelpMan createmultisig() // Get the output type OutputType output_type = OutputType::LEGACY; if (!request.params[2].isNull()) { - if (!ParseOutputType(request.params[2].get_str(), output_type)) { + std::optional<OutputType> parsed = ParseOutputType(request.params[2].get_str()); + if (!parsed) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Unknown address type '%s'", request.params[2].get_str())); - } - if (output_type == OutputType::BECH32M) { + } else if (parsed.value() == OutputType::BECH32M) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "createmultisig cannot create bech32m multisig addresses"); } + output_type = parsed.value(); } // Construct using pay-to-script-hash: diff --git a/src/rpc/net.cpp b/src/rpc/net.cpp index 3962a13924..861b889118 100644 --- a/src/rpc/net.cpp +++ b/src/rpc/net.cpp @@ -118,7 +118,6 @@ static RPCHelpMan getpeerinfo() {RPCResult::Type::STR, "addr", "(host:port) The IP address and port of the peer"}, {RPCResult::Type::STR, "addrbind", "(ip:port) Bind address of the connection to the peer"}, {RPCResult::Type::STR, "addrlocal", "(ip:port) Local address as reported by the peer"}, - {RPCResult::Type::BOOL, "addr_relay_enabled", "Whether we participate in address relay with this peer"}, {RPCResult::Type::STR, "network", "Network (" + Join(GetNetworkNames(/* append_unroutable */ true), ", ") + ")"}, {RPCResult::Type::NUM, "mapped_as", "The AS in the BGP route to the peer used for diversifying\n" "peer selection (only available if the asmap config flag is set)"}, @@ -151,6 +150,9 @@ static RPCHelpMan getpeerinfo() { {RPCResult::Type::NUM, "n", "The heights of blocks we're currently asking from this peer"}, }}, + {RPCResult::Type::BOOL, "addr_relay_enabled", "Whether we participate in address relay with this peer"}, + {RPCResult::Type::NUM, "addr_processed", "The total number of addresses processed, excluding those dropped due to rate limiting"}, + {RPCResult::Type::NUM, "addr_rate_limited", "The total number of addresses dropped due to rate limiting"}, {RPCResult::Type::ARR, "permissions", "Any special permissions that have been granted to this peer", { {RPCResult::Type::STR, "permission_type", Join(NET_PERMISSIONS_DOC, ",\n") + ".\n"}, @@ -202,7 +204,6 @@ static RPCHelpMan getpeerinfo() if (!(stats.addrLocal.empty())) { obj.pushKV("addrlocal", stats.addrLocal); } - obj.pushKV("addr_relay_enabled", statestats.m_addr_relay_enabled); obj.pushKV("network", GetNetworkName(stats.m_network)); if (stats.m_mapped_as != 0) { obj.pushKV("mapped_as", uint64_t(stats.m_mapped_as)); @@ -244,6 +245,7 @@ static RPCHelpMan getpeerinfo() heights.push_back(height); } obj.pushKV("inflight", heights); + obj.pushKV("addr_relay_enabled", statestats.m_addr_relay_enabled); obj.pushKV("addr_processed", statestats.m_addr_processed); obj.pushKV("addr_rate_limited", statestats.m_addr_rate_limited); } @@ -949,7 +951,7 @@ static RPCHelpMan addpeeraddress() address.nTime = GetAdjustedTime(); // The source address is set equal to the address. This is equivalent to the peer // announcing itself. - if (node.addrman->Add(address, address)) success = true; + if (node.addrman->Add({address}, address)) success = true; } obj.pushKV("success", success); diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index c617b0389c..00e77d89e5 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -903,7 +903,7 @@ static RPCHelpMan testmempoolaccept() RPCResult{ RPCResult::Type::ARR, "", "The result of the mempool acceptance test for each raw transaction in the input array.\n" "Returns results for each transaction in the same order they were passed in.\n" - "It is possible for transactions to not be fully validated ('allowed' unset) if another transaction failed.\n", + "Transactions that cannot be fully validated due to failures in other transactions will not contain an 'allowed' result.\n", { {RPCResult::Type::OBJ, "", "", { diff --git a/src/rpc/rawtransaction_util.cpp b/src/rpc/rawtransaction_util.cpp index 122a92f084..f21eddf56c 100644 --- a/src/rpc/rawtransaction_util.cpp +++ b/src/rpc/rawtransaction_util.cpp @@ -18,6 +18,7 @@ #include <univalue.h> #include <util/rbf.h> #include <util/strencodings.h> +#include <util/translation.h> CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniValue& outputs_in, const UniValue& locktime, bool rbf) { @@ -280,22 +281,22 @@ void SignTransaction(CMutableTransaction& mtx, const SigningProvider* keystore, int nHashType = ParseSighashString(hashType); // Script verification errors - std::map<int, std::string> input_errors; + std::map<int, bilingual_str> input_errors; bool complete = SignTransaction(mtx, keystore, coins, nHashType, input_errors); SignTransactionResultToJSON(mtx, complete, coins, input_errors, result); } -void SignTransactionResultToJSON(CMutableTransaction& mtx, bool complete, const std::map<COutPoint, Coin>& coins, const std::map<int, std::string>& input_errors, UniValue& result) +void SignTransactionResultToJSON(CMutableTransaction& mtx, bool complete, const std::map<COutPoint, Coin>& coins, const std::map<int, bilingual_str>& input_errors, UniValue& result) { // Make errors UniValue UniValue vErrors(UniValue::VARR); for (const auto& err_pair : input_errors) { - if (err_pair.second == "Missing amount") { + if (err_pair.second.original == "Missing amount") { // This particular error needs to be an exception for some reason throw JSONRPCError(RPC_TYPE_ERROR, strprintf("Missing amount for %s", coins.at(mtx.vin.at(err_pair.first).prevout).out.ToString())); } - TxInErrorToJSON(mtx.vin.at(err_pair.first), vErrors, err_pair.second); + TxInErrorToJSON(mtx.vin.at(err_pair.first), vErrors, err_pair.second.original); } result.pushKV("hex", EncodeHexTx(CTransaction(mtx))); diff --git a/src/rpc/rawtransaction_util.h b/src/rpc/rawtransaction_util.h index ce7d5834fa..d2e116f7ee 100644 --- a/src/rpc/rawtransaction_util.h +++ b/src/rpc/rawtransaction_util.h @@ -8,6 +8,7 @@ #include <map> #include <string> +struct bilingual_str; class FillableSigningProvider; class UniValue; struct CMutableTransaction; @@ -25,7 +26,7 @@ class SigningProvider; * @param result JSON object where signed transaction results accumulate */ void SignTransaction(CMutableTransaction& mtx, const SigningProvider* keystore, const std::map<COutPoint, Coin>& coins, const UniValue& hashType, UniValue& result); -void SignTransactionResultToJSON(CMutableTransaction& mtx, bool complete, const std::map<COutPoint, Coin>& coins, const std::map<int, std::string>& input_errors, UniValue& result); +void SignTransactionResultToJSON(CMutableTransaction& mtx, bool complete, const std::map<COutPoint, Coin>& coins, const std::map<int, bilingual_str>& input_errors, UniValue& result); /** * Parse a prevtxs UniValue array and get the map of coins from it diff --git a/src/script/interpreter.cpp b/src/script/interpreter.cpp index dd7c0a4a05..eafa9840d7 100644 --- a/src/script/interpreter.cpp +++ b/src/script/interpreter.cpp @@ -1874,9 +1874,9 @@ static bool VerifyTaprootCommitment(const std::vector<unsigned char>& control, c assert(control.size() >= TAPROOT_CONTROL_BASE_SIZE); assert(program.size() >= uint256::size()); //! The internal pubkey (x-only, so no Y coordinate parity). - const XOnlyPubKey p{uint256(std::vector<unsigned char>(control.begin() + 1, control.begin() + TAPROOT_CONTROL_BASE_SIZE))}; + const XOnlyPubKey p{Span<const unsigned char>{control.data() + 1, control.data() + TAPROOT_CONTROL_BASE_SIZE}}; //! The output pubkey (taken from the scriptPubKey). - const XOnlyPubKey q{uint256(program)}; + const XOnlyPubKey q{program}; // Compute the Merkle root from the leaf and the provided path. const uint256 merkle_root = ComputeTaprootMerkleRoot(control, tapleaf_hash); // Verify that the output pubkey matches the tweaked internal pubkey, after correcting for parity. diff --git a/src/script/interpreter.h b/src/script/interpreter.h index 93136a0b79..ab49e84577 100644 --- a/src/script/interpreter.h +++ b/src/script/interpreter.h @@ -170,6 +170,13 @@ struct PrecomputedTransactionData PrecomputedTransactionData() = default; + /** Initialize this PrecomputedTransactionData with transaction data. + * + * @param[in] tx The transaction for which data is being precomputed. + * @param[in] spent_outputs The CTxOuts being spent, one for each tx.vin, in order. + * @param[in] force Whether to precompute data for all optional features, + * regardless of what is in the inputs (used at signing + * time, when the inputs aren't filled in yet). */ template <class T> void Init(const T& tx, std::vector<CTxOut>&& spent_outputs, bool force = false); diff --git a/src/script/sign.cpp b/src/script/sign.cpp index 7864e690d8..4714d0ef11 100644 --- a/src/script/sign.cpp +++ b/src/script/sign.cpp @@ -11,6 +11,7 @@ #include <script/signingprovider.h> #include <script/standard.h> #include <uint256.h> +#include <util/translation.h> #include <util/vector.h> typedef std::vector<unsigned char> valtype; @@ -60,7 +61,7 @@ bool MutableTransactionSignatureCreator::CreateSchnorrSig(const SigningProvider& CKey key; { - // For now, use the old full pubkey-based key derivation logic. As it indexed by + // For now, use the old full pubkey-based key derivation logic. As it is indexed by // Hash160(full pubkey), we need to try both a version prefixed with 0x02, and one // with 0x03. unsigned char b[33] = {0x02}; @@ -629,7 +630,7 @@ bool IsSegWitOutput(const SigningProvider& provider, const CScript& script) return false; } -bool SignTransaction(CMutableTransaction& mtx, const SigningProvider* keystore, const std::map<COutPoint, Coin>& coins, int nHashType, std::map<int, std::string>& input_errors) +bool SignTransaction(CMutableTransaction& mtx, const SigningProvider* keystore, const std::map<COutPoint, Coin>& coins, int nHashType, std::map<int, bilingual_str>& input_errors) { bool fHashSingle = ((nHashType & ~SIGHASH_ANYONECANPAY) == SIGHASH_SINGLE); @@ -639,29 +640,26 @@ bool SignTransaction(CMutableTransaction& mtx, const SigningProvider* keystore, PrecomputedTransactionData txdata; std::vector<CTxOut> spent_outputs; - spent_outputs.resize(mtx.vin.size()); - bool have_all_spent_outputs = true; - for (unsigned int i = 0; i < mtx.vin.size(); i++) { + for (unsigned int i = 0; i < mtx.vin.size(); ++i) { CTxIn& txin = mtx.vin[i]; auto coin = coins.find(txin.prevout); if (coin == coins.end() || coin->second.IsSpent()) { - have_all_spent_outputs = false; + txdata.Init(txConst, /* spent_outputs */ {}, /* force */ true); + break; } else { - spent_outputs[i] = CTxOut(coin->second.out.nValue, coin->second.out.scriptPubKey); + spent_outputs.emplace_back(coin->second.out.nValue, coin->second.out.scriptPubKey); } } - if (have_all_spent_outputs) { + if (spent_outputs.size() == mtx.vin.size()) { txdata.Init(txConst, std::move(spent_outputs), true); - } else { - txdata.Init(txConst, {}, true); } // Sign what we can: - for (unsigned int i = 0; i < mtx.vin.size(); i++) { + for (unsigned int i = 0; i < mtx.vin.size(); ++i) { CTxIn& txin = mtx.vin[i]; auto coin = coins.find(txin.prevout); if (coin == coins.end() || coin->second.IsSpent()) { - input_errors[i] = "Input not found or already spent"; + input_errors[i] = _("Input not found or already spent"); continue; } const CScript& prevPubKey = coin->second.out.scriptPubKey; @@ -677,7 +675,7 @@ bool SignTransaction(CMutableTransaction& mtx, const SigningProvider* keystore, // amount must be specified for valid segwit signature if (amount == MAX_MONEY && !txin.scriptWitness.IsNull()) { - input_errors[i] = "Missing amount"; + input_errors[i] = _("Missing amount"); continue; } @@ -685,12 +683,12 @@ bool SignTransaction(CMutableTransaction& mtx, const SigningProvider* keystore, if (!VerifyScript(txin.scriptSig, prevPubKey, &txin.scriptWitness, STANDARD_SCRIPT_VERIFY_FLAGS, TransactionSignatureChecker(&txConst, i, amount, txdata, MissingDataBehavior::FAIL), &serror)) { if (serror == SCRIPT_ERR_INVALID_STACK_OPERATION) { // Unable to sign input and verification failed (possible attempt to partially sign). - input_errors[i] = "Unable to sign input, invalid stack size (possibly missing key)"; + input_errors[i] = Untranslated("Unable to sign input, invalid stack size (possibly missing key)"); } else if (serror == SCRIPT_ERR_SIG_NULLFAIL) { // Verification failed (possibly due to insufficient signatures). - input_errors[i] = "CHECK(MULTI)SIG failing with non-zero signature (possibly need more signatures)"; + input_errors[i] = Untranslated("CHECK(MULTI)SIG failing with non-zero signature (possibly need more signatures)"); } else { - input_errors[i] = ScriptErrorString(serror); + input_errors[i] = Untranslated(ScriptErrorString(serror)); } } else { // If this input succeeds, make sure there is no error set for it diff --git a/src/script/sign.h b/src/script/sign.h index b4e7318892..6d3479c143 100644 --- a/src/script/sign.h +++ b/src/script/sign.h @@ -21,6 +21,7 @@ class CScript; class CTransaction; class SigningProvider; +struct bilingual_str; struct CMutableTransaction; /** Interface for signature creators. */ @@ -44,8 +45,8 @@ class MutableTransactionSignatureCreator : public BaseSignatureCreator { const PrecomputedTransactionData* m_txdata; public: - MutableTransactionSignatureCreator(const CMutableTransaction* txToIn, unsigned int nInIn, const CAmount& amountIn, int nHashTypeIn = SIGHASH_ALL); - MutableTransactionSignatureCreator(const CMutableTransaction* txToIn, unsigned int nInIn, const CAmount& amountIn, const PrecomputedTransactionData* txdata, int nHashTypeIn = SIGHASH_ALL); + MutableTransactionSignatureCreator(const CMutableTransaction* txToIn, unsigned int nInIn, const CAmount& amountIn, int nHashTypeIn); + MutableTransactionSignatureCreator(const CMutableTransaction* txToIn, unsigned int nInIn, const CAmount& amountIn, const PrecomputedTransactionData* txdata, int nHashTypeIn); const BaseSignatureChecker& Checker() const override { return checker; } bool CreateSig(const SigningProvider& provider, std::vector<unsigned char>& vchSig, const CKeyID& keyid, const CScript& scriptCode, SigVersion sigversion) const override; bool CreateSchnorrSig(const SigningProvider& provider, std::vector<unsigned char>& sig, const XOnlyPubKey& pubkey, const uint256* leaf_hash, const uint256* merkle_root, SigVersion sigversion) const override; @@ -178,6 +179,6 @@ bool IsSolvable(const SigningProvider& provider, const CScript& script); bool IsSegWitOutput(const SigningProvider& provider, const CScript& script); /** Sign the CMutableTransaction */ -bool SignTransaction(CMutableTransaction& mtx, const SigningProvider* provider, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, std::string>& input_errors); +bool SignTransaction(CMutableTransaction& mtx, const SigningProvider* provider, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, bilingual_str>& input_errors); #endif // BITCOIN_SCRIPT_SIGN_H diff --git a/src/script/standard.cpp b/src/script/standard.cpp index b8349bb9ab..67a79a157c 100644 --- a/src/script/standard.cpp +++ b/src/script/standard.cpp @@ -504,6 +504,7 @@ WitnessV1Taproot TaprootBuilder::GetOutput() { return WitnessV1Taproot{m_output_ TaprootSpendData TaprootBuilder::GetSpendData() const { + assert(IsComplete()); TaprootSpendData spd; spd.merkle_root = m_branch.size() == 0 ? uint256() : m_branch[0]->hash; spd.internal_key = m_internal_key; diff --git a/src/script/standard.h b/src/script/standard.h index ac4e2f3276..78492733db 100644 --- a/src/script/standard.h +++ b/src/script/standard.h @@ -227,8 +227,11 @@ struct TaprootSpendData /** The Merkle root of the script tree (0 if no scripts). */ uint256 merkle_root; /** Map from (script, leaf_version) to (sets of) control blocks. - * The control blocks are sorted by size, so that the signing logic can - * easily prefer the cheapest one. */ + * More than one control block for a given script is only possible if it + * appears in multiple branches of the tree. We keep them all so that + * inference can reconstruct the full tree. Within each set, the control + * blocks are sorted by size, so that the signing logic can easily + * prefer the cheapest one. */ std::map<std::pair<CScript, int>, std::set<std::vector<unsigned char>, ShortestVectorFirstComparator>> scripts; /** Merge other TaprootSpendData (for the same scriptPubKey) into this. */ void Merge(TaprootSpendData other); @@ -252,7 +255,7 @@ private: /** Merkle hash of this node. */ uint256 hash; /** Tracked leaves underneath this node (either from the node itself, or its children). - * The merkle_branch field for each is the partners to get to *this* node. */ + * The merkle_branch field of each is the partners to get to *this* node. */ std::vector<LeafInfo> leaves; }; /** Whether the builder is in a valid state so far. */ diff --git a/src/test/addrman_tests.cpp b/src/test/addrman_tests.cpp index 79c7102c4f..cd5dc2370f 100644 --- a/src/test/addrman_tests.cpp +++ b/src/test/addrman_tests.cpp @@ -1,7 +1,10 @@ // Copyright (c) 2012-2020 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include <addrdb.h> #include <addrman.h> +#include <chainparams.h> #include <test/data/asmap.raw.h> #include <test/util/setup_common.h> #include <util/asmap.h> @@ -15,30 +18,76 @@ #include <optional> #include <string> +using namespace std::literals; + +class CAddrManSerializationMock : public CAddrMan +{ +public: + virtual void Serialize(CDataStream& s) const = 0; + + CAddrManSerializationMock() + : CAddrMan(/* deterministic */ true, /* consistency_check_ratio */ 100) + {} +}; + +class CAddrManUncorrupted : public CAddrManSerializationMock +{ +public: + void Serialize(CDataStream& s) const override + { + CAddrMan::Serialize(s); + } +}; + +class CAddrManCorrupted : public CAddrManSerializationMock +{ +public: + void Serialize(CDataStream& s) const override + { + // Produces corrupt output that claims addrman has 20 addrs when it only has one addr. + unsigned char nVersion = 1; + s << nVersion; + s << ((unsigned char)32); + s << uint256::ONE; + s << 10; // nNew + s << 10; // nTried + + int nUBuckets = ADDRMAN_NEW_BUCKET_COUNT ^ (1 << 30); + s << nUBuckets; + + CService serv; + BOOST_CHECK(Lookup("252.1.1.1", serv, 7777, false)); + CAddress addr = CAddress(serv, NODE_NONE); + CNetAddr resolved; + BOOST_CHECK(LookupHost("252.2.2.2", resolved, false)); + CAddrInfo info = CAddrInfo(addr, resolved); + s << info; + } +}; + +static CDataStream AddrmanToStream(const CAddrManSerializationMock& _addrman) +{ + CDataStream ssPeersIn(SER_DISK, CLIENT_VERSION); + ssPeersIn << Params().MessageStart(); + ssPeersIn << _addrman; + std::string str = ssPeersIn.str(); + std::vector<unsigned char> vchData(str.begin(), str.end()); + return CDataStream(vchData, SER_DISK, CLIENT_VERSION); +} + class CAddrManTest : public CAddrMan { private: bool deterministic; public: explicit CAddrManTest(bool makeDeterministic = true, - std::vector<bool> asmap = std::vector<bool>()) + std::vector<bool> asmap = std::vector<bool>()) + : CAddrMan(makeDeterministic, /* consistency_check_ratio */ 100) { - if (makeDeterministic) { - // Set addrman addr placement to be deterministic. - MakeDeterministic(); - } deterministic = makeDeterministic; m_asmap = asmap; } - //! Ensure that bucket placement is always the same for testing purposes. - void MakeDeterministic() - { - LOCK(cs); - nKey.SetNull(); - insecure_rand = FastRandomContext(true); - } - CAddrInfo* Find(const CNetAddr& addr, int* pnId = nullptr) { LOCK(cs); @@ -83,16 +132,6 @@ public: int64_t nLastTry = GetAdjustedTime()-61; Attempt(addr, count_failure, nLastTry); } - - void Clear() - { - CAddrMan::Clear(); - if (deterministic) { - LOCK(cs); - nKey.SetNull(); - insecure_rand = FastRandomContext(true); - } - } }; static CNetAddr ResolveIP(const std::string& ip) @@ -126,27 +165,27 @@ BOOST_FIXTURE_TEST_SUITE(addrman_tests, BasicTestingSetup) BOOST_AUTO_TEST_CASE(addrman_simple) { - CAddrManTest addrman; + auto addrman = std::make_unique<CAddrManTest>(); CNetAddr source = ResolveIP("252.2.2.2"); // Test: Does Addrman respond correctly when empty. - BOOST_CHECK_EQUAL(addrman.size(), 0U); - CAddrInfo addr_null = addrman.Select(); + BOOST_CHECK_EQUAL(addrman->size(), 0U); + CAddrInfo addr_null = addrman->Select(); BOOST_CHECK_EQUAL(addr_null.ToString(), "[::]:0"); // Test: Does Addrman::Add work as expected. CService addr1 = ResolveService("250.1.1.1", 8333); - BOOST_CHECK(addrman.Add(CAddress(addr1, NODE_NONE), source)); - BOOST_CHECK_EQUAL(addrman.size(), 1U); - CAddrInfo addr_ret1 = addrman.Select(); + BOOST_CHECK(addrman->Add({CAddress(addr1, NODE_NONE)}, source)); + BOOST_CHECK_EQUAL(addrman->size(), 1U); + CAddrInfo addr_ret1 = addrman->Select(); BOOST_CHECK_EQUAL(addr_ret1.ToString(), "250.1.1.1:8333"); // Test: Does IP address deduplication work correctly. // Expected dup IP should not be added. CService addr1_dup = ResolveService("250.1.1.1", 8333); - BOOST_CHECK(!addrman.Add(CAddress(addr1_dup, NODE_NONE), source)); - BOOST_CHECK_EQUAL(addrman.size(), 1U); + BOOST_CHECK(!addrman->Add({CAddress(addr1_dup, NODE_NONE)}, source)); + BOOST_CHECK_EQUAL(addrman->size(), 1U); // Test: New table has one addr and we add a diff addr we should @@ -156,21 +195,16 @@ BOOST_AUTO_TEST_CASE(addrman_simple) // success. CService addr2 = ResolveService("250.1.1.2", 8333); - BOOST_CHECK(addrman.Add(CAddress(addr2, NODE_NONE), source)); - BOOST_CHECK(addrman.size() >= 1); - - // Test: AddrMan::Clear() should empty the new table. - addrman.Clear(); - BOOST_CHECK_EQUAL(addrman.size(), 0U); - CAddrInfo addr_null2 = addrman.Select(); - BOOST_CHECK_EQUAL(addr_null2.ToString(), "[::]:0"); + BOOST_CHECK(addrman->Add({CAddress(addr2, NODE_NONE)}, source)); + BOOST_CHECK(addrman->size() >= 1); - // Test: AddrMan::Add multiple addresses works as expected + // Test: reset addrman and test AddrMan::Add multiple addresses works as expected + addrman = std::make_unique<CAddrManTest>(); std::vector<CAddress> vAddr; vAddr.push_back(CAddress(ResolveService("250.1.1.3", 8333), NODE_NONE)); vAddr.push_back(CAddress(ResolveService("250.1.1.4", 8333), NODE_NONE)); - BOOST_CHECK(addrman.Add(vAddr, source)); - BOOST_CHECK(addrman.size() >= 1); + BOOST_CHECK(addrman->Add(vAddr, source)); + BOOST_CHECK(addrman->size() >= 1); } BOOST_AUTO_TEST_CASE(addrman_ports) @@ -183,11 +217,11 @@ BOOST_AUTO_TEST_CASE(addrman_ports) // Test 7; Addr with same IP but diff port does not replace existing addr. CService addr1 = ResolveService("250.1.1.1", 8333); - BOOST_CHECK(addrman.Add(CAddress(addr1, NODE_NONE), source)); + BOOST_CHECK(addrman.Add({CAddress(addr1, NODE_NONE)}, source)); BOOST_CHECK_EQUAL(addrman.size(), 1U); CService addr1_port = ResolveService("250.1.1.1", 8334); - BOOST_CHECK(!addrman.Add(CAddress(addr1_port, NODE_NONE), source)); + BOOST_CHECK(!addrman.Add({CAddress(addr1_port, NODE_NONE)}, source)); BOOST_CHECK_EQUAL(addrman.size(), 1U); CAddrInfo addr_ret2 = addrman.Select(); BOOST_CHECK_EQUAL(addr_ret2.ToString(), "250.1.1.1:8333"); @@ -210,7 +244,7 @@ BOOST_AUTO_TEST_CASE(addrman_select) // Test: Select from new with 1 addr in new. CService addr1 = ResolveService("250.1.1.1", 8333); - BOOST_CHECK(addrman.Add(CAddress(addr1, NODE_NONE), source)); + BOOST_CHECK(addrman.Add({CAddress(addr1, NODE_NONE)}, source)); BOOST_CHECK_EQUAL(addrman.size(), 1U); bool newOnly = true; @@ -234,20 +268,20 @@ BOOST_AUTO_TEST_CASE(addrman_select) CService addr3 = ResolveService("250.3.2.2", 9999); CService addr4 = ResolveService("250.3.3.3", 9999); - BOOST_CHECK(addrman.Add(CAddress(addr2, NODE_NONE), ResolveService("250.3.1.1", 8333))); - BOOST_CHECK(addrman.Add(CAddress(addr3, NODE_NONE), ResolveService("250.3.1.1", 8333))); - BOOST_CHECK(addrman.Add(CAddress(addr4, NODE_NONE), ResolveService("250.4.1.1", 8333))); + BOOST_CHECK(addrman.Add({CAddress(addr2, NODE_NONE)}, ResolveService("250.3.1.1", 8333))); + BOOST_CHECK(addrman.Add({CAddress(addr3, NODE_NONE)}, ResolveService("250.3.1.1", 8333))); + BOOST_CHECK(addrman.Add({CAddress(addr4, NODE_NONE)}, ResolveService("250.4.1.1", 8333))); // Add three addresses to tried table. CService addr5 = ResolveService("250.4.4.4", 8333); CService addr6 = ResolveService("250.4.5.5", 7777); CService addr7 = ResolveService("250.4.6.6", 8333); - BOOST_CHECK(addrman.Add(CAddress(addr5, NODE_NONE), ResolveService("250.3.1.1", 8333))); + BOOST_CHECK(addrman.Add({CAddress(addr5, NODE_NONE)}, ResolveService("250.3.1.1", 8333))); addrman.Good(CAddress(addr5, NODE_NONE)); - BOOST_CHECK(addrman.Add(CAddress(addr6, NODE_NONE), ResolveService("250.3.1.1", 8333))); + BOOST_CHECK(addrman.Add({CAddress(addr6, NODE_NONE)}, ResolveService("250.3.1.1", 8333))); addrman.Good(CAddress(addr6, NODE_NONE)); - BOOST_CHECK(addrman.Add(CAddress(addr7, NODE_NONE), ResolveService("250.1.1.3", 8333))); + BOOST_CHECK(addrman.Add({CAddress(addr7, NODE_NONE)}, ResolveService("250.1.1.3", 8333))); addrman.Good(CAddress(addr7, NODE_NONE)); // Test: 6 addrs + 1 addr from last test = 7. @@ -267,24 +301,27 @@ BOOST_AUTO_TEST_CASE(addrman_new_collisions) CNetAddr source = ResolveIP("252.2.2.2"); - BOOST_CHECK_EQUAL(addrman.size(), 0U); + uint32_t num_addrs{0}; + + BOOST_CHECK_EQUAL(addrman.size(), num_addrs); - for (unsigned int i = 1; i < 18; i++) { - CService addr = ResolveService("250.1.1." + ToString(i)); - BOOST_CHECK(addrman.Add(CAddress(addr, NODE_NONE), source)); + while (num_addrs < 22) { // Magic number! 250.1.1.1 - 250.1.1.22 do not collide with deterministic key = 1 + CService addr = ResolveService("250.1.1." + ToString(++num_addrs)); + BOOST_CHECK(addrman.Add({CAddress(addr, NODE_NONE)}, source)); //Test: No collision in new table yet. - BOOST_CHECK_EQUAL(addrman.size(), i); + BOOST_CHECK_EQUAL(addrman.size(), num_addrs); } //Test: new table collision! - CService addr1 = ResolveService("250.1.1.18"); - BOOST_CHECK(addrman.Add(CAddress(addr1, NODE_NONE), source)); - BOOST_CHECK_EQUAL(addrman.size(), 17U); - - CService addr2 = ResolveService("250.1.1.19"); - BOOST_CHECK(addrman.Add(CAddress(addr2, NODE_NONE), source)); - BOOST_CHECK_EQUAL(addrman.size(), 18U); + CService addr1 = ResolveService("250.1.1." + ToString(++num_addrs)); + uint32_t collisions{1}; + BOOST_CHECK(addrman.Add({CAddress(addr1, NODE_NONE)}, source)); + BOOST_CHECK_EQUAL(addrman.size(), num_addrs - collisions); + + CService addr2 = ResolveService("250.1.1." + ToString(++num_addrs)); + BOOST_CHECK(addrman.Add({CAddress(addr2, NODE_NONE)}, source)); + BOOST_CHECK_EQUAL(addrman.size(), num_addrs - collisions); } BOOST_AUTO_TEST_CASE(addrman_tried_collisions) @@ -293,25 +330,28 @@ BOOST_AUTO_TEST_CASE(addrman_tried_collisions) CNetAddr source = ResolveIP("252.2.2.2"); - BOOST_CHECK_EQUAL(addrman.size(), 0U); + uint32_t num_addrs{0}; - for (unsigned int i = 1; i < 80; i++) { - CService addr = ResolveService("250.1.1." + ToString(i)); - BOOST_CHECK(addrman.Add(CAddress(addr, NODE_NONE), source)); + BOOST_CHECK_EQUAL(addrman.size(), num_addrs); + + while (num_addrs < 64) { // Magic number! 250.1.1.1 - 250.1.1.64 do not collide with deterministic key = 1 + CService addr = ResolveService("250.1.1." + ToString(++num_addrs)); + BOOST_CHECK(addrman.Add({CAddress(addr, NODE_NONE)}, source)); addrman.Good(CAddress(addr, NODE_NONE)); //Test: No collision in tried table yet. - BOOST_CHECK_EQUAL(addrman.size(), i); + BOOST_CHECK_EQUAL(addrman.size(), num_addrs); } //Test: tried table collision! - CService addr1 = ResolveService("250.1.1.80"); - BOOST_CHECK(addrman.Add(CAddress(addr1, NODE_NONE), source)); - BOOST_CHECK_EQUAL(addrman.size(), 79U); - - CService addr2 = ResolveService("250.1.1.81"); - BOOST_CHECK(addrman.Add(CAddress(addr2, NODE_NONE), source)); - BOOST_CHECK_EQUAL(addrman.size(), 80U); + CService addr1 = ResolveService("250.1.1." + ToString(++num_addrs)); + uint32_t collisions{1}; + BOOST_CHECK(addrman.Add({CAddress(addr1, NODE_NONE)}, source)); + BOOST_CHECK_EQUAL(addrman.size(), num_addrs - collisions); + + CService addr2 = ResolveService("250.1.1." + ToString(++num_addrs)); + BOOST_CHECK(addrman.Add({CAddress(addr2, NODE_NONE)}, source)); + BOOST_CHECK_EQUAL(addrman.size(), num_addrs - collisions); } BOOST_AUTO_TEST_CASE(addrman_find) @@ -327,9 +367,9 @@ BOOST_AUTO_TEST_CASE(addrman_find) CNetAddr source1 = ResolveIP("250.1.2.1"); CNetAddr source2 = ResolveIP("250.1.2.2"); - BOOST_CHECK(addrman.Add(addr1, source1)); - BOOST_CHECK(!addrman.Add(addr2, source2)); - BOOST_CHECK(addrman.Add(addr3, source1)); + BOOST_CHECK(addrman.Add({addr1}, source1)); + BOOST_CHECK(!addrman.Add({addr2}, source2)); + BOOST_CHECK(addrman.Add({addr3}, source1)); // Test: ensure Find returns an IP matching what we searched on. CAddrInfo* info1 = addrman.Find(addr1); @@ -411,11 +451,8 @@ BOOST_AUTO_TEST_CASE(addrman_getaddr) CNetAddr source2 = ResolveIP("250.2.3.3"); // Test: Ensure GetAddr works with new addresses. - BOOST_CHECK(addrman.Add(addr1, source1)); - BOOST_CHECK(addrman.Add(addr2, source2)); - BOOST_CHECK(addrman.Add(addr3, source1)); - BOOST_CHECK(addrman.Add(addr4, source2)); - BOOST_CHECK(addrman.Add(addr5, source1)); + BOOST_CHECK(addrman.Add({addr1, addr3, addr5}, source1)); + BOOST_CHECK(addrman.Add({addr2, addr4}, source2)); BOOST_CHECK_EQUAL(addrman.GetAddr(/* max_addresses */ 0, /* max_pct */ 0, /* network */ std::nullopt).size(), 5U); // Net processing asks for 23% of addresses. 23% of 5 is 1 rounded down. @@ -436,7 +473,7 @@ BOOST_AUTO_TEST_CASE(addrman_getaddr) // Ensure that for all addrs in addrman, isTerrible == false. addr.nTime = GetAdjustedTime(); - addrman.Add(addr, ResolveIP(strAddr)); + addrman.Add({addr}, ResolveIP(strAddr)); if (i % 8 == 0) addrman.Good(addr); } @@ -722,23 +759,23 @@ BOOST_AUTO_TEST_CASE(addrman_serialization) { std::vector<bool> asmap1 = FromBytes(asmap_raw, sizeof(asmap_raw) * 8); - CAddrManTest addrman_asmap1(true, asmap1); - CAddrManTest addrman_asmap1_dup(true, asmap1); - CAddrManTest addrman_noasmap; + auto addrman_asmap1 = std::make_unique<CAddrManTest>(true, asmap1); + auto addrman_asmap1_dup = std::make_unique<CAddrManTest>(true, asmap1); + auto addrman_noasmap = std::make_unique<CAddrManTest>(); CDataStream stream(SER_NETWORK, PROTOCOL_VERSION); CAddress addr = CAddress(ResolveService("250.1.1.1"), NODE_NONE); CNetAddr default_source; - addrman_asmap1.Add(addr, default_source); + addrman_asmap1->Add({addr}, default_source); - stream << addrman_asmap1; + stream << *addrman_asmap1; // serizalizing/deserializing addrman with the same asmap - stream >> addrman_asmap1_dup; + stream >> *addrman_asmap1_dup; - std::pair<int, int> bucketAndEntry_asmap1 = addrman_asmap1.GetBucketAndEntry(addr); - std::pair<int, int> bucketAndEntry_asmap1_dup = addrman_asmap1_dup.GetBucketAndEntry(addr); + std::pair<int, int> bucketAndEntry_asmap1 = addrman_asmap1->GetBucketAndEntry(addr); + std::pair<int, int> bucketAndEntry_asmap1_dup = addrman_asmap1_dup->GetBucketAndEntry(addr); BOOST_CHECK(bucketAndEntry_asmap1.second != -1); BOOST_CHECK(bucketAndEntry_asmap1_dup.second != -1); @@ -746,40 +783,39 @@ BOOST_AUTO_TEST_CASE(addrman_serialization) BOOST_CHECK(bucketAndEntry_asmap1.second == bucketAndEntry_asmap1_dup.second); // deserializing asmaped peers.dat to non-asmaped addrman - stream << addrman_asmap1; - stream >> addrman_noasmap; - std::pair<int, int> bucketAndEntry_noasmap = addrman_noasmap.GetBucketAndEntry(addr); + stream << *addrman_asmap1; + stream >> *addrman_noasmap; + std::pair<int, int> bucketAndEntry_noasmap = addrman_noasmap->GetBucketAndEntry(addr); BOOST_CHECK(bucketAndEntry_noasmap.second != -1); BOOST_CHECK(bucketAndEntry_asmap1.first != bucketAndEntry_noasmap.first); BOOST_CHECK(bucketAndEntry_asmap1.second != bucketAndEntry_noasmap.second); // deserializing non-asmaped peers.dat to asmaped addrman - addrman_asmap1.Clear(); - addrman_noasmap.Clear(); - addrman_noasmap.Add(addr, default_source); - stream << addrman_noasmap; - stream >> addrman_asmap1; - std::pair<int, int> bucketAndEntry_asmap1_deser = addrman_asmap1.GetBucketAndEntry(addr); + addrman_asmap1 = std::make_unique<CAddrManTest>(true, asmap1); + addrman_noasmap = std::make_unique<CAddrManTest>(); + addrman_noasmap->Add({addr}, default_source); + stream << *addrman_noasmap; + stream >> *addrman_asmap1; + std::pair<int, int> bucketAndEntry_asmap1_deser = addrman_asmap1->GetBucketAndEntry(addr); BOOST_CHECK(bucketAndEntry_asmap1_deser.second != -1); BOOST_CHECK(bucketAndEntry_asmap1_deser.first != bucketAndEntry_noasmap.first); BOOST_CHECK(bucketAndEntry_asmap1_deser.first == bucketAndEntry_asmap1_dup.first); BOOST_CHECK(bucketAndEntry_asmap1_deser.second == bucketAndEntry_asmap1_dup.second); // used to map to different buckets, now maps to the same bucket. - addrman_asmap1.Clear(); - addrman_noasmap.Clear(); + addrman_asmap1 = std::make_unique<CAddrManTest>(true, asmap1); + addrman_noasmap = std::make_unique<CAddrManTest>(); CAddress addr1 = CAddress(ResolveService("250.1.1.1"), NODE_NONE); CAddress addr2 = CAddress(ResolveService("250.2.1.1"), NODE_NONE); - addrman_noasmap.Add(addr, default_source); - addrman_noasmap.Add(addr2, default_source); - std::pair<int, int> bucketAndEntry_noasmap_addr1 = addrman_noasmap.GetBucketAndEntry(addr1); - std::pair<int, int> bucketAndEntry_noasmap_addr2 = addrman_noasmap.GetBucketAndEntry(addr2); + addrman_noasmap->Add({addr, addr2}, default_source); + std::pair<int, int> bucketAndEntry_noasmap_addr1 = addrman_noasmap->GetBucketAndEntry(addr1); + std::pair<int, int> bucketAndEntry_noasmap_addr2 = addrman_noasmap->GetBucketAndEntry(addr2); BOOST_CHECK(bucketAndEntry_noasmap_addr1.first != bucketAndEntry_noasmap_addr2.first); BOOST_CHECK(bucketAndEntry_noasmap_addr1.second != bucketAndEntry_noasmap_addr2.second); - stream << addrman_noasmap; - stream >> addrman_asmap1; - std::pair<int, int> bucketAndEntry_asmap1_deser_addr1 = addrman_asmap1.GetBucketAndEntry(addr1); - std::pair<int, int> bucketAndEntry_asmap1_deser_addr2 = addrman_asmap1.GetBucketAndEntry(addr2); + stream << *addrman_noasmap; + stream >> *addrman_asmap1; + std::pair<int, int> bucketAndEntry_asmap1_deser_addr1 = addrman_asmap1->GetBucketAndEntry(addr1); + std::pair<int, int> bucketAndEntry_asmap1_deser_addr2 = addrman_asmap1->GetBucketAndEntry(addr2); BOOST_CHECK(bucketAndEntry_asmap1_deser_addr1.first == bucketAndEntry_asmap1_deser_addr2.first); BOOST_CHECK(bucketAndEntry_asmap1_deser_addr1.second != bucketAndEntry_asmap1_deser_addr2.second); } @@ -788,7 +824,7 @@ BOOST_AUTO_TEST_CASE(remove_invalid) { // Confirm that invalid addresses are ignored in unserialization. - CAddrManTest addrman; + auto addrman = std::make_unique<CAddrManTest>(); CDataStream stream(SER_NETWORK, PROTOCOL_VERSION); const CAddress new1{ResolveService("5.5.5.5"), NODE_NONE}; @@ -796,12 +832,12 @@ BOOST_AUTO_TEST_CASE(remove_invalid) const CAddress tried1{ResolveService("7.7.7.7"), NODE_NONE}; const CAddress tried2{ResolveService("8.8.8.8"), NODE_NONE}; - addrman.Add({new1, tried1, new2, tried2}, CNetAddr{}); - addrman.Good(tried1); - addrman.Good(tried2); - BOOST_REQUIRE_EQUAL(addrman.size(), 4); + addrman->Add({new1, tried1, new2, tried2}, CNetAddr{}); + addrman->Good(tried1); + addrman->Good(tried2); + BOOST_REQUIRE_EQUAL(addrman->size(), 4); - stream << addrman; + stream << *addrman; const std::string str{stream.str()}; size_t pos; @@ -820,9 +856,9 @@ BOOST_AUTO_TEST_CASE(remove_invalid) BOOST_REQUIRE(pos + sizeof(tried2_raw_replacement) <= stream.size()); memcpy(stream.data() + pos, tried2_raw_replacement, sizeof(tried2_raw_replacement)); - addrman.Clear(); - stream >> addrman; - BOOST_CHECK_EQUAL(addrman.size(), 2); + addrman = std::make_unique<CAddrManTest>(); + stream >> *addrman; + BOOST_CHECK_EQUAL(addrman->size(), 2); } BOOST_AUTO_TEST_CASE(addrman_selecttriedcollision) @@ -838,7 +874,7 @@ BOOST_AUTO_TEST_CASE(addrman_selecttriedcollision) CNetAddr source = ResolveIP("252.2.2.2"); for (unsigned int i = 1; i < 23; i++) { CService addr = ResolveService("250.1.1."+ToString(i)); - BOOST_CHECK(addrman.Add(CAddress(addr, NODE_NONE), source)); + BOOST_CHECK(addrman.Add({CAddress(addr, NODE_NONE)}, source)); addrman.Good(addr); // No collisions yet. @@ -861,11 +897,11 @@ BOOST_AUTO_TEST_CASE(addrman_noevict) { CAddrManTest addrman; - // Add twenty two addresses. + // Add 35 addresses. CNetAddr source = ResolveIP("252.2.2.2"); - for (unsigned int i = 1; i < 23; i++) { + for (unsigned int i = 1; i < 36; i++) { CService addr = ResolveService("250.1.1."+ToString(i)); - BOOST_CHECK(addrman.Add(CAddress(addr, NODE_NONE), source)); + BOOST_CHECK(addrman.Add({CAddress(addr, NODE_NONE)}, source)); addrman.Good(addr); // No collision yet. @@ -873,22 +909,22 @@ BOOST_AUTO_TEST_CASE(addrman_noevict) BOOST_CHECK(addrman.SelectTriedCollision().ToString() == "[::]:0"); } - // Collision between 23 and 19. - CService addr23 = ResolveService("250.1.1.23"); - BOOST_CHECK(addrman.Add(CAddress(addr23, NODE_NONE), source)); - addrman.Good(addr23); + // Collision between 36 and 19. + CService addr36 = ResolveService("250.1.1.36"); + BOOST_CHECK(addrman.Add({CAddress(addr36, NODE_NONE)}, source)); + addrman.Good(addr36); - BOOST_CHECK(addrman.size() == 23); - BOOST_CHECK(addrman.SelectTriedCollision().ToString() == "250.1.1.19:0"); + BOOST_CHECK(addrman.size() == 36); + BOOST_CHECK_EQUAL(addrman.SelectTriedCollision().ToString(), "250.1.1.19:0"); - // 23 should be discarded and 19 not evicted. + // 36 should be discarded and 19 not evicted. addrman.ResolveCollisions(); BOOST_CHECK(addrman.SelectTriedCollision().ToString() == "[::]:0"); // Lets create two collisions. - for (unsigned int i = 24; i < 33; i++) { + for (unsigned int i = 37; i < 59; i++) { CService addr = ResolveService("250.1.1."+ToString(i)); - BOOST_CHECK(addrman.Add(CAddress(addr, NODE_NONE), source)); + BOOST_CHECK(addrman.Add({CAddress(addr, NODE_NONE)}, source)); addrman.Good(addr); BOOST_CHECK(addrman.size() == i); @@ -896,17 +932,17 @@ BOOST_AUTO_TEST_CASE(addrman_noevict) } // Cause a collision. - CService addr33 = ResolveService("250.1.1.33"); - BOOST_CHECK(addrman.Add(CAddress(addr33, NODE_NONE), source)); - addrman.Good(addr33); - BOOST_CHECK(addrman.size() == 33); + CService addr59 = ResolveService("250.1.1.59"); + BOOST_CHECK(addrman.Add({CAddress(addr59, NODE_NONE)}, source)); + addrman.Good(addr59); + BOOST_CHECK(addrman.size() == 59); - BOOST_CHECK(addrman.SelectTriedCollision().ToString() == "250.1.1.27:0"); + BOOST_CHECK_EQUAL(addrman.SelectTriedCollision().ToString(), "250.1.1.10:0"); // Cause a second collision. - BOOST_CHECK(!addrman.Add(CAddress(addr23, NODE_NONE), source)); - addrman.Good(addr23); - BOOST_CHECK(addrman.size() == 33); + BOOST_CHECK(!addrman.Add({CAddress(addr36, NODE_NONE)}, source)); + addrman.Good(addr36); + BOOST_CHECK(addrman.size() == 59); BOOST_CHECK(addrman.SelectTriedCollision().ToString() != "[::]:0"); addrman.ResolveCollisions(); @@ -922,11 +958,11 @@ BOOST_AUTO_TEST_CASE(addrman_evictionworks) // Empty addrman should return blank addrman info. BOOST_CHECK(addrman.SelectTriedCollision().ToString() == "[::]:0"); - // Add twenty two addresses. + // Add 35 addresses CNetAddr source = ResolveIP("252.2.2.2"); - for (unsigned int i = 1; i < 23; i++) { + for (unsigned int i = 1; i < 36; i++) { CService addr = ResolveService("250.1.1."+ToString(i)); - BOOST_CHECK(addrman.Add(CAddress(addr, NODE_NONE), source)); + BOOST_CHECK(addrman.Add({CAddress(addr, NODE_NONE)}, source)); addrman.Good(addr); // No collision yet. @@ -934,38 +970,111 @@ BOOST_AUTO_TEST_CASE(addrman_evictionworks) BOOST_CHECK(addrman.SelectTriedCollision().ToString() == "[::]:0"); } - // Collision between 23 and 19. - CService addr = ResolveService("250.1.1.23"); - BOOST_CHECK(addrman.Add(CAddress(addr, NODE_NONE), source)); + // Collision between 36 and 19. + CService addr = ResolveService("250.1.1.36"); + BOOST_CHECK(addrman.Add({CAddress(addr, NODE_NONE)}, source)); addrman.Good(addr); - BOOST_CHECK(addrman.size() == 23); + BOOST_CHECK_EQUAL(addrman.size(), 36); CAddrInfo info = addrman.SelectTriedCollision(); - BOOST_CHECK(info.ToString() == "250.1.1.19:0"); + BOOST_CHECK_EQUAL(info.ToString(), "250.1.1.19:0"); // Ensure test of address fails, so that it is evicted. addrman.SimConnFail(info); - // Should swap 23 for 19. + // Should swap 36 for 19. addrman.ResolveCollisions(); BOOST_CHECK(addrman.SelectTriedCollision().ToString() == "[::]:0"); - // If 23 was swapped for 19, then this should cause no collisions. - BOOST_CHECK(!addrman.Add(CAddress(addr, NODE_NONE), source)); + // If 36 was swapped for 19, then this should cause no collisions. + BOOST_CHECK(!addrman.Add({CAddress(addr, NODE_NONE)}, source)); addrman.Good(addr); BOOST_CHECK(addrman.SelectTriedCollision().ToString() == "[::]:0"); - // If we insert 19 is should collide with 23. + // If we insert 19 it should collide with 36 CService addr19 = ResolveService("250.1.1.19"); - BOOST_CHECK(!addrman.Add(CAddress(addr19, NODE_NONE), source)); + BOOST_CHECK(!addrman.Add({CAddress(addr19, NODE_NONE)}, source)); addrman.Good(addr19); - BOOST_CHECK(addrman.SelectTriedCollision().ToString() == "250.1.1.23:0"); + BOOST_CHECK_EQUAL(addrman.SelectTriedCollision().ToString(), "250.1.1.36:0"); addrman.ResolveCollisions(); BOOST_CHECK(addrman.SelectTriedCollision().ToString() == "[::]:0"); } +BOOST_AUTO_TEST_CASE(caddrdb_read) +{ + CAddrManUncorrupted addrmanUncorrupted; + + CService addr1, addr2, addr3; + BOOST_CHECK(Lookup("250.7.1.1", addr1, 8333, false)); + BOOST_CHECK(Lookup("250.7.2.2", addr2, 9999, false)); + BOOST_CHECK(Lookup("250.7.3.3", addr3, 9999, false)); + BOOST_CHECK(Lookup("250.7.3.3"s, addr3, 9999, false)); + BOOST_CHECK(!Lookup("250.7.3.3\0example.com"s, addr3, 9999, false)); + + // Add three addresses to new table. + CService source; + BOOST_CHECK(Lookup("252.5.1.1", source, 8333, false)); + std::vector<CAddress> addresses{CAddress(addr1, NODE_NONE), CAddress(addr2, NODE_NONE), CAddress(addr3, NODE_NONE)}; + BOOST_CHECK(addrmanUncorrupted.Add(addresses, source)); + BOOST_CHECK(addrmanUncorrupted.size() == 3); + + // Test that the de-serialization does not throw an exception. + CDataStream ssPeers1 = AddrmanToStream(addrmanUncorrupted); + bool exceptionThrown = false; + CAddrMan addrman1(/* deterministic */ false, /* consistency_check_ratio */ 100); + + BOOST_CHECK(addrman1.size() == 0); + try { + unsigned char pchMsgTmp[4]; + ssPeers1 >> pchMsgTmp; + ssPeers1 >> addrman1; + } catch (const std::exception&) { + exceptionThrown = true; + } + + BOOST_CHECK(addrman1.size() == 3); + BOOST_CHECK(exceptionThrown == false); + + // Test that CAddrDB::Read creates an addrman with the correct number of addrs. + CDataStream ssPeers2 = AddrmanToStream(addrmanUncorrupted); + + CAddrMan addrman2(/* deterministic */ false, /* consistency_check_ratio */ 100); + BOOST_CHECK(addrman2.size() == 0); + BOOST_CHECK(CAddrDB::Read(addrman2, ssPeers2)); + BOOST_CHECK(addrman2.size() == 3); +} + + +BOOST_AUTO_TEST_CASE(caddrdb_read_corrupted) +{ + CAddrManCorrupted addrmanCorrupted; + + // Test that the de-serialization of corrupted addrman throws an exception. + CDataStream ssPeers1 = AddrmanToStream(addrmanCorrupted); + bool exceptionThrown = false; + CAddrMan addrman1(/* deterministic */ false, /* consistency_check_ratio */ 100); + BOOST_CHECK(addrman1.size() == 0); + try { + unsigned char pchMsgTmp[4]; + ssPeers1 >> pchMsgTmp; + ssPeers1 >> addrman1; + } catch (const std::exception&) { + exceptionThrown = true; + } + // Even through de-serialization failed addrman is not left in a clean state. + BOOST_CHECK(addrman1.size() == 1); + BOOST_CHECK(exceptionThrown); + + // Test that CAddrDB::Read fails if peers.dat is corrupt + CDataStream ssPeers2 = AddrmanToStream(addrmanCorrupted); + + CAddrMan addrman2(/* deterministic */ false, /* consistency_check_ratio */ 100); + BOOST_CHECK(addrman2.size() == 0); + BOOST_CHECK(!CAddrDB::Read(addrman2, ssPeers2)); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/crypto_tests.cpp b/src/test/crypto_tests.cpp index edec5f0a31..5b3b39fdb8 100644 --- a/src/test/crypto_tests.cpp +++ b/src/test/crypto_tests.cpp @@ -617,7 +617,7 @@ static void TestChaCha20Poly1305AEAD(bool must_succeed, unsigned int expected_aa ChaCha20Poly1305AEAD aead(aead_K_1.data(), aead_K_1.size(), aead_K_2.data(), aead_K_2.size()); // create a chacha20 instance to compare against - ChaCha20 cmp_ctx(aead_K_2.data(), 32); + ChaCha20 cmp_ctx(aead_K_1.data(), 32); // encipher bool res = aead.Crypt(seqnr_payload, seqnr_aad, aad_pos, ciphertext_buf.data(), ciphertext_buf.size(), plaintext_buf.data(), plaintext_buf.size(), true); @@ -708,8 +708,8 @@ BOOST_AUTO_TEST_CASE(chacha20_poly1305_aead_testvector) "b1a03d5bd2855d60699e7d3a3133fa47be740fe4e4c1f967555e2d9271f31c3a8bd94d54b5ecabbc41ffbb0c90924080"); TestChaCha20Poly1305AEAD(true, 255, "ff0000f195e66982105ffb640bb7757f579da31602fc93ec01ac56f85ac3c134a4547b733b46413042c9440049176905d3be59ea1c53f15916155c2be8241a38008b9a26bc35941e2444177c8ade6689de95264986d95889fb60e84629c9bd9a5acb1cc118be563eb9b3a4a472f82e09a7e778492b562ef7130e88dfe031c79db9d4f7c7a899151b9a475032b63fc385245fe054e3dd5a97a5f576fe064025d3ce042c566ab2c507b138db853e3d6959660996546cc9c4a6eafdc777c040d70eaf46f76dad3979e5c5360c3317166a1c894c94a371876a94df7628fe4eaaf2ccb27d5aaae0ad7ad0f9d4b6ad3b54098746d4524d38407a6deb3ab78fab78c9", - "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", "ff0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", "c640c1711e3ee904ac35c57ab9791c8a1c408603a90b77a83b54f6c844cb4b06d94e7fc6c800e165acd66147e80ec45a567f6ce66d05ec0cae679dceeb890017", "3940c1e92da4582ff6f92a776aeb14d014d384eeb30f660dacf70a14a23fd31e91212701334e2ce1acf5199dc84f4d61ddbe6571bca5af874b4c9226c26e650995d157644e1848b96ed6c2102d5489a050e71d29a5a66ece11de5fb5c9558d54da28fe45b0bc4db4e5b88030bfc4a352b4b7068eccf656bae7ad6a35615315fc7c49d4200388d5eca67c2e822e069336c69b40db67e0f3c81209c50f3216a4b89fb3ae1b984b7851a2ec6f68ab12b101ab120e1ea7313bb93b5a0f71185c7fea017ddb92769861c29dba4fbc432280d5dff21b36d1c4c790128b22699950bb18bf74c448cdfe547d8ed4f657d8005fdc0cd7a050c2d46050a44c4376355858981fbe8b184288276e7a93eabc899c4a", "f039c6689eaeef0456685200feaab9d54bbd9acde4410a3b6f4321296f4a8ca2604b49727d8892c57e005d799b2a38e85e809f20146e08eec75169691c8d4f54a0d51a1e1c7b381e0474eb02f994be9415ef3ffcbd2343f0601e1f3b172a1d494f838824e4df570f8e3b0c04e27966e36c82abd352d07054ef7bd36b84c63f9369afe7ed79b94f953873006b920c3fa251a771de1b63da927058ade119aa898b8c97e42a606b2f6df1e2d957c22f7593c1e2002f4252f4c9ae4bf773499e5cfcfe14dfc1ede26508953f88553bf4a76a802f6a0068d59295b01503fd9a600067624203e880fdf53933b96e1f4d9eb3f4e363dd8165a278ff667a41ee42b9892b077cefff92b93441f7be74cf10e6cd"); diff --git a/src/test/denialofservice_tests.cpp b/src/test/denialofservice_tests.cpp index 5668ead1fb..0bfe6eecd9 100644 --- a/src/test/denialofservice_tests.cpp +++ b/src/test/denialofservice_tests.cpp @@ -53,7 +53,7 @@ BOOST_AUTO_TEST_CASE(outbound_slow_chain_eviction) const CChainParams& chainparams = Params(); auto connman = std::make_unique<CConnman>(0x1337, 0x1337, *m_node.addrman); auto peerLogic = PeerManager::make(chainparams, *connman, *m_node.addrman, nullptr, - *m_node.scheduler, *m_node.chainman, *m_node.mempool, false); + *m_node.chainman, *m_node.mempool, false); // Mock an outbound peer CAddress addr1(ip(0xa0b0c001), NODE_NONE); @@ -121,7 +121,7 @@ BOOST_AUTO_TEST_CASE(stale_tip_peer_management) const CChainParams& chainparams = Params(); auto connman = std::make_unique<ConnmanTestMsg>(0x1337, 0x1337, *m_node.addrman); auto peerLogic = PeerManager::make(chainparams, *connman, *m_node.addrman, nullptr, - *m_node.scheduler, *m_node.chainman, *m_node.mempool, false); + *m_node.chainman, *m_node.mempool, false); constexpr int max_outbound_full_relay = MAX_OUTBOUND_FULL_RELAY_CONNECTIONS; CConnman::Options options; @@ -194,7 +194,7 @@ BOOST_AUTO_TEST_CASE(peer_discouragement) auto banman = std::make_unique<BanMan>(m_args.GetDataDirBase() / "banlist", nullptr, DEFAULT_MISBEHAVING_BANTIME); auto connman = std::make_unique<ConnmanTestMsg>(0x1337, 0x1337, *m_node.addrman); auto peerLogic = PeerManager::make(chainparams, *connman, *m_node.addrman, banman.get(), - *m_node.scheduler, *m_node.chainman, *m_node.mempool, false); + *m_node.chainman, *m_node.mempool, false); CNetAddr tor_netaddr; BOOST_REQUIRE( @@ -288,7 +288,7 @@ BOOST_AUTO_TEST_CASE(DoS_bantime) auto banman = std::make_unique<BanMan>(m_args.GetDataDirBase() / "banlist", nullptr, DEFAULT_MISBEHAVING_BANTIME); auto connman = std::make_unique<CConnman>(0x1337, 0x1337, *m_node.addrman); auto peerLogic = PeerManager::make(chainparams, *connman, *m_node.addrman, banman.get(), - *m_node.scheduler, *m_node.chainman, *m_node.mempool, false); + *m_node.chainman, *m_node.mempool, false); banman->ClearBanned(); int64_t nStartTime = GetTime(); diff --git a/src/test/fuzz/addrman.cpp b/src/test/fuzz/addrman.cpp index 344d1dde8e..95aa53bff4 100644 --- a/src/test/fuzz/addrman.cpp +++ b/src/test/fuzz/addrman.cpp @@ -12,6 +12,7 @@ #include <time.h> #include <util/asmap.h> +#include <cassert> #include <cstdint> #include <optional> #include <string> @@ -25,10 +26,201 @@ void initialize_addrman() class CAddrManDeterministic : public CAddrMan { public: - void MakeDeterministic(const uint256& random_seed) + FuzzedDataProvider& m_fuzzed_data_provider; + + explicit CAddrManDeterministic(FuzzedDataProvider& fuzzed_data_provider) + : CAddrMan(/* deterministic */ true, /* consistency_check_ratio */ 0) + , m_fuzzed_data_provider(fuzzed_data_provider) + { + WITH_LOCK(cs, insecure_rand = FastRandomContext{ConsumeUInt256(fuzzed_data_provider)}); + if (fuzzed_data_provider.ConsumeBool()) { + m_asmap = ConsumeRandomLengthBitVector(fuzzed_data_provider); + if (!SanityCheckASMap(m_asmap)) { + m_asmap.clear(); + } + } + } + + /** + * Generate a random address. Always returns a valid address. + */ + CNetAddr RandAddr() EXCLUSIVE_LOCKS_REQUIRED(cs) + { + CNetAddr addr; + if (m_fuzzed_data_provider.remaining_bytes() > 1 && m_fuzzed_data_provider.ConsumeBool()) { + addr = ConsumeNetAddr(m_fuzzed_data_provider); + } else { + // The networks [1..6] correspond to CNetAddr::BIP155Network (private). + static const std::map<uint8_t, uint8_t> net_len_map = {{1, ADDR_IPV4_SIZE}, + {2, ADDR_IPV6_SIZE}, + {4, ADDR_TORV3_SIZE}, + {5, ADDR_I2P_SIZE}, + {6, ADDR_CJDNS_SIZE}}; + uint8_t net = insecure_rand.randrange(5) + 1; // [1..5] + if (net == 3) { + net = 6; + } + + CDataStream s(SER_NETWORK, PROTOCOL_VERSION | ADDRV2_FORMAT); + + s << net; + s << insecure_rand.randbytes(net_len_map.at(net)); + + s >> addr; + } + + // Return a dummy IPv4 5.5.5.5 if we generated an invalid address. + if (!addr.IsValid()) { + in_addr v4_addr = {}; + v4_addr.s_addr = 0x05050505; + addr = CNetAddr{v4_addr}; + } + + return addr; + } + + /** + * Fill this addrman with lots of addresses from lots of sources. + */ + void Fill() + { + LOCK(cs); + + // Add some of the addresses directly to the "tried" table. + + // 0, 1, 2, 3 corresponding to 0%, 100%, 50%, 33% + const size_t n = m_fuzzed_data_provider.ConsumeIntegralInRange<size_t>(0, 3); + + const size_t num_sources = m_fuzzed_data_provider.ConsumeIntegralInRange<size_t>(10, 50); + CNetAddr prev_source; + // Use insecure_rand inside the loops instead of m_fuzzed_data_provider because when + // the latter is exhausted it just returns 0. + for (size_t i = 0; i < num_sources; ++i) { + const auto source = RandAddr(); + const size_t num_addresses = insecure_rand.randrange(500) + 1; // [1..500] + + for (size_t j = 0; j < num_addresses; ++j) { + const auto addr = CAddress{CService{RandAddr(), 8333}, NODE_NETWORK}; + const auto time_penalty = insecure_rand.randrange(100000001); +#if 1 + // 2.83 sec to fill. + if (n > 0 && mapInfo.size() % n == 0 && mapAddr.find(addr) == mapAddr.end()) { + // Add to the "tried" table (if the bucket slot is free). + const CAddrInfo dummy{addr, source}; + const int bucket = dummy.GetTriedBucket(nKey, m_asmap); + const int bucket_pos = dummy.GetBucketPosition(nKey, false, bucket); + if (vvTried[bucket][bucket_pos] == -1) { + int id; + CAddrInfo* addr_info = Create(addr, source, &id); + vvTried[bucket][bucket_pos] = id; + addr_info->fInTried = true; + ++nTried; + } + } else { + // Add to the "new" table. + Add_(addr, source, time_penalty); + } +#else + // 261.91 sec to fill. + Add_(addr, source, time_penalty); + if (n > 0 && mapInfo.size() % n == 0) { + Good_(addr, false, GetTime()); + } +#endif + // Add 10% of the addresses from more than one source. + if (insecure_rand.randrange(10) == 0 && prev_source.IsValid()) { + Add_(addr, prev_source, time_penalty); + } + } + prev_source = source; + } + } + + /** + * Compare with another AddrMan. + * This compares: + * - the values in `mapInfo` (the keys aka ids are ignored) + * - vvNew entries refer to the same addresses + * - vvTried entries refer to the same addresses + */ + bool operator==(const CAddrManDeterministic& other) { - WITH_LOCK(cs, insecure_rand = FastRandomContext{random_seed}); - Clear(); + LOCK2(cs, other.cs); + + if (mapInfo.size() != other.mapInfo.size() || nNew != other.nNew || + nTried != other.nTried) { + return false; + } + + // Check that all values in `mapInfo` are equal to all values in `other.mapInfo`. + // Keys may be different. + + using CAddrInfoHasher = std::function<size_t(const CAddrInfo&)>; + using CAddrInfoEq = std::function<bool(const CAddrInfo&, const CAddrInfo&)>; + + CNetAddrHash netaddr_hasher; + + CAddrInfoHasher addrinfo_hasher = [&netaddr_hasher](const CAddrInfo& a) { + return netaddr_hasher(static_cast<CNetAddr>(a)) ^ netaddr_hasher(a.source) ^ + a.nLastSuccess ^ a.nAttempts ^ a.nRefCount ^ a.fInTried; + }; + + CAddrInfoEq addrinfo_eq = [](const CAddrInfo& lhs, const CAddrInfo& rhs) { + return static_cast<CNetAddr>(lhs) == static_cast<CNetAddr>(rhs) && + lhs.source == rhs.source && lhs.nLastSuccess == rhs.nLastSuccess && + lhs.nAttempts == rhs.nAttempts && lhs.nRefCount == rhs.nRefCount && + lhs.fInTried == rhs.fInTried; + }; + + using Addresses = std::unordered_set<CAddrInfo, CAddrInfoHasher, CAddrInfoEq>; + + const size_t num_addresses{mapInfo.size()}; + + Addresses addresses{num_addresses, addrinfo_hasher, addrinfo_eq}; + for (const auto& [id, addr] : mapInfo) { + addresses.insert(addr); + } + + Addresses other_addresses{num_addresses, addrinfo_hasher, addrinfo_eq}; + for (const auto& [id, addr] : other.mapInfo) { + other_addresses.insert(addr); + } + + if (addresses != other_addresses) { + return false; + } + + auto IdsReferToSameAddress = [&](int id, int other_id) EXCLUSIVE_LOCKS_REQUIRED(cs, other.cs) { + if (id == -1 && other_id == -1) { + return true; + } + if ((id == -1 && other_id != -1) || (id != -1 && other_id == -1)) { + return false; + } + return mapInfo.at(id) == other.mapInfo.at(other_id); + }; + + // Check that `vvNew` contains the same addresses as `other.vvNew`. Notice - `vvNew[i][j]` + // contains just an id and the address is to be found in `mapInfo.at(id)`. The ids + // themselves may differ between `vvNew` and `other.vvNew`. + for (size_t i = 0; i < ADDRMAN_NEW_BUCKET_COUNT; ++i) { + for (size_t j = 0; j < ADDRMAN_BUCKET_SIZE; ++j) { + if (!IdsReferToSameAddress(vvNew[i][j], other.vvNew[i][j])) { + return false; + } + } + } + + // Same for `vvTried`. + for (size_t i = 0; i < ADDRMAN_TRIED_BUCKET_COUNT; ++i) { + for (size_t j = 0; j < ADDRMAN_BUCKET_SIZE; ++j) { + if (!IdsReferToSameAddress(vvTried[i][j], other.vvTried[i][j])) { + return false; + } + } + } + + return true; } }; @@ -36,45 +228,29 @@ FUZZ_TARGET_INIT(addrman, initialize_addrman) { FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size()); SetMockTime(ConsumeTime(fuzzed_data_provider)); - CAddrManDeterministic addr_man; - addr_man.MakeDeterministic(ConsumeUInt256(fuzzed_data_provider)); - if (fuzzed_data_provider.ConsumeBool()) { - addr_man.m_asmap = ConsumeRandomLengthBitVector(fuzzed_data_provider); - if (!SanityCheckASMap(addr_man.m_asmap)) { - addr_man.m_asmap.clear(); - } - } + auto addr_man_ptr = std::make_unique<CAddrManDeterministic>(fuzzed_data_provider); if (fuzzed_data_provider.ConsumeBool()) { const std::vector<uint8_t> serialized_data{ConsumeRandomLengthByteVector(fuzzed_data_provider)}; CDataStream ds(serialized_data, SER_DISK, INIT_PROTO_VERSION); const auto ser_version{fuzzed_data_provider.ConsumeIntegral<int32_t>()}; ds.SetVersion(ser_version); try { - ds >> addr_man; + ds >> *addr_man_ptr; } catch (const std::ios_base::failure&) { - addr_man.Clear(); + addr_man_ptr = std::make_unique<CAddrManDeterministic>(fuzzed_data_provider); } } + CAddrManDeterministic& addr_man = *addr_man_ptr; while (fuzzed_data_provider.ConsumeBool()) { CallOneOf( fuzzed_data_provider, [&] { - addr_man.Clear(); - }, - [&] { addr_man.ResolveCollisions(); }, [&] { (void)addr_man.SelectTriedCollision(); }, [&] { - const std::optional<CAddress> opt_address = ConsumeDeserializable<CAddress>(fuzzed_data_provider); - const std::optional<CNetAddr> opt_net_addr = ConsumeDeserializable<CNetAddr>(fuzzed_data_provider); - if (opt_address && opt_net_addr) { - addr_man.Add(*opt_address, *opt_net_addr, fuzzed_data_provider.ConsumeIntegralInRange<int64_t>(0, 100000000)); - } - }, - [&] { std::vector<CAddress> addresses; while (fuzzed_data_provider.ConsumeBool()) { const std::optional<CAddress> opt_address = ConsumeDeserializable<CAddress>(fuzzed_data_provider); @@ -123,3 +299,21 @@ FUZZ_TARGET_INIT(addrman, initialize_addrman) CDataStream data_stream(SER_NETWORK, PROTOCOL_VERSION); data_stream << const_addr_man; } + +// Check that serialize followed by unserialize produces the same addrman. +FUZZ_TARGET_INIT(addrman_serdeser, initialize_addrman) +{ + FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size()); + SetMockTime(ConsumeTime(fuzzed_data_provider)); + + CAddrManDeterministic addr_man1{fuzzed_data_provider}; + CAddrManDeterministic addr_man2{fuzzed_data_provider}; + addr_man2.m_asmap = addr_man1.m_asmap; + + CDataStream data_stream(SER_NETWORK, PROTOCOL_VERSION); + + addr_man1.Fill(); + data_stream << addr_man1; + data_stream >> addr_man2; + assert(addr_man1 == addr_man2); +} diff --git a/src/test/fuzz/banman.cpp b/src/test/fuzz/banman.cpp index de211f601f..46a9f623ac 100644 --- a/src/test/fuzz/banman.cpp +++ b/src/test/fuzz/banman.cpp @@ -108,9 +108,7 @@ FUZZ_TARGET_INIT(banman, initialize_banman) BanMan ban_man_read{banlist_file, /* client_interface */ nullptr, /* default_ban_time */ 0}; banmap_t banmap_read; ban_man_read.GetBanned(banmap_read); - // Assert temporarily disabled to allow the remainder of the fuzz test to run while a - // fix is being worked on. See https://github.com/bitcoin/bitcoin/pull/22517 - (void)(banmap == banmap_read); + assert(banmap == banmap_read); } } fs::remove(banlist_file.string() + ".json"); diff --git a/src/test/fuzz/connman.cpp b/src/test/fuzz/connman.cpp index bbec5943af..0e323ddc20 100644 --- a/src/test/fuzz/connman.cpp +++ b/src/test/fuzz/connman.cpp @@ -25,7 +25,7 @@ FUZZ_TARGET_INIT(connman, initialize_connman) { FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()}; SetMockTime(ConsumeTime(fuzzed_data_provider)); - CAddrMan addrman; + CAddrMan addrman(/* deterministic */ false, /* consistency_check_ratio */ 0); CConnman connman{fuzzed_data_provider.ConsumeIntegral<uint64_t>(), fuzzed_data_provider.ConsumeIntegral<uint64_t>(), addrman, fuzzed_data_provider.ConsumeBool()}; CNetAddr random_netaddr; CNode random_node = ConsumeNode(fuzzed_data_provider); diff --git a/src/test/fuzz/data_stream.cpp b/src/test/fuzz/data_stream.cpp index 473caec6ff..53400082ab 100644 --- a/src/test/fuzz/data_stream.cpp +++ b/src/test/fuzz/data_stream.cpp @@ -21,6 +21,6 @@ FUZZ_TARGET_INIT(data_stream_addr_man, initialize_data_stream_addr_man) { FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()}; CDataStream data_stream = ConsumeDataStream(fuzzed_data_provider); - CAddrMan addr_man; + CAddrMan addr_man(/* deterministic */ false, /* consistency_check_ratio */ 0); CAddrDB::Read(addr_man, data_stream); } diff --git a/src/test/fuzz/deserialize.cpp b/src/test/fuzz/deserialize.cpp index d5b56cb7cd..49503e8dc6 100644 --- a/src/test/fuzz/deserialize.cpp +++ b/src/test/fuzz/deserialize.cpp @@ -188,7 +188,7 @@ FUZZ_TARGET_DESERIALIZE(blockmerkleroot, { BlockMerkleRoot(block, &mutated); }) FUZZ_TARGET_DESERIALIZE(addrman_deserialize, { - CAddrMan am; + CAddrMan am(/* deterministic */ false, /* consistency_check_ratio */ 0); DeserializeFromFuzzingInput(buffer, am); }) FUZZ_TARGET_DESERIALIZE(blockheader_deserialize, { diff --git a/src/test/fuzz/fuzz.h b/src/test/fuzz/fuzz.h index 2bad77bdc1..ce8fd660aa 100644 --- a/src/test/fuzz/fuzz.h +++ b/src/test/fuzz/fuzz.h @@ -11,6 +11,9 @@ #include <functional> #include <string_view> +#define LIMITED_WHILE(condition, limit) \ + for (unsigned _count{limit}; (condition) && _count; --_count) + using FuzzBufferType = Span<const uint8_t>; using TypeTestOneInput = std::function<void(FuzzBufferType)>; diff --git a/src/test/fuzz/kitchen_sink.cpp b/src/test/fuzz/kitchen_sink.cpp index 908e9a1c83..82f3a306c5 100644 --- a/src/test/fuzz/kitchen_sink.cpp +++ b/src/test/fuzz/kitchen_sink.cpp @@ -13,6 +13,7 @@ #include <array> #include <cstdint> +#include <optional> #include <vector> namespace { @@ -46,11 +47,10 @@ FUZZ_TARGET(kitchen_sink) const OutputType output_type = fuzzed_data_provider.PickValueInArray(OUTPUT_TYPES); const std::string& output_type_string = FormatOutputType(output_type); - OutputType output_type_parsed; - const bool parsed = ParseOutputType(output_type_string, output_type_parsed); + const std::optional<OutputType> parsed = ParseOutputType(output_type_string); assert(parsed); - assert(output_type == output_type_parsed); - (void)ParseOutputType(fuzzed_data_provider.ConsumeRandomLengthString(64), output_type_parsed); + assert(output_type == parsed.value()); + (void)ParseOutputType(fuzzed_data_provider.ConsumeRandomLengthString(64)); const std::vector<uint8_t> bytes = ConsumeRandomLengthByteVector(fuzzed_data_provider); const std::vector<bool> bits = BytesToBits(bytes); diff --git a/src/test/fuzz/script_sign.cpp b/src/test/fuzz/script_sign.cpp index fe850a6959..684324c36e 100644 --- a/src/test/fuzz/script_sign.cpp +++ b/src/test/fuzz/script_sign.cpp @@ -13,6 +13,7 @@ #include <test/fuzz/FuzzedDataProvider.h> #include <test/fuzz/fuzz.h> #include <test/fuzz/util.h> +#include <util/translation.h> #include <cassert> #include <cstdint> @@ -135,7 +136,7 @@ FUZZ_TARGET_INIT(script_sign, initialize_script_sign) } coins[*outpoint] = *coin; } - std::map<int, std::string> input_errors; + std::map<int, bilingual_str> input_errors; (void)SignTransaction(sign_transaction_tx_to, &provider, coins, fuzzed_data_provider.ConsumeIntegral<int>(), input_errors); } } diff --git a/src/test/fuzz/string.cpp b/src/test/fuzz/string.cpp index 286375f7ae..0c1b45b86c 100644 --- a/src/test/fuzz/string.cpp +++ b/src/test/fuzz/string.cpp @@ -66,8 +66,7 @@ FUZZ_TARGET(string) (void)ParseNonRFCJSONValue(random_string_1); } catch (const std::runtime_error&) { } - OutputType output_type; - (void)ParseOutputType(random_string_1, output_type); + (void)ParseOutputType(random_string_1); (void)RemovePrefix(random_string_1, random_string_2); (void)ResolveErrMsg(random_string_1, random_string_2); try { diff --git a/src/test/fuzz/system.cpp b/src/test/fuzz/system.cpp index b25dcfcd3b..0f53939eac 100644 --- a/src/test/fuzz/system.cpp +++ b/src/test/fuzz/system.cpp @@ -31,7 +31,8 @@ FUZZ_TARGET(system) SetupHelpOptions(args_manager); } - while (fuzzed_data_provider.ConsumeBool()) { + LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 3000) + { CallOneOf( fuzzed_data_provider, [&] { diff --git a/src/test/miner_tests.cpp b/src/test/miner_tests.cpp index e20c5e4e8f..7f44dcf20e 100644 --- a/src/test/miner_tests.cpp +++ b/src/test/miner_tests.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2020 The Bitcoin Core developers +// Copyright (c) 2011-2021 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. @@ -16,6 +16,7 @@ #include <util/system.h> #include <util/time.h> #include <validation.h> +#include <versionbits.h> #include <test/util/setup_common.h> @@ -51,36 +52,25 @@ BlockAssembler MinerTestingSetup::AssemblerForTest(const CChainParams& params) constexpr static struct { unsigned char extranonce; unsigned int nonce; -} blockinfo[] = { - {4, 0xa4a3e223}, {2, 0x15c32f9e}, {1, 0x0375b547}, {1, 0x7004a8a5}, - {2, 0xce440296}, {2, 0x52cfe198}, {1, 0x77a72cd0}, {2, 0xbb5d6f84}, - {2, 0x83f30c2c}, {1, 0x48a73d5b}, {1, 0xef7dcd01}, {2, 0x6809c6c4}, - {2, 0x0883ab3c}, {1, 0x087bbbe2}, {2, 0x2104a814}, {2, 0xdffb6daa}, - {1, 0xee8a0a08}, {2, 0xba4237c1}, {1, 0xa70349dc}, {1, 0x344722bb}, - {3, 0xd6294733}, {2, 0xec9f5c94}, {2, 0xca2fbc28}, {1, 0x6ba4f406}, - {2, 0x015d4532}, {1, 0x6e119b7c}, {2, 0x43e8f314}, {2, 0x27962f38}, - {2, 0xb571b51b}, {2, 0xb36bee23}, {2, 0xd17924a8}, {2, 0x6bc212d9}, - {1, 0x630d4948}, {2, 0x9a4c4ebb}, {2, 0x554be537}, {1, 0xd63ddfc7}, - {2, 0xa10acc11}, {1, 0x759a8363}, {2, 0xfb73090d}, {1, 0xe82c6a34}, - {1, 0xe33e92d7}, {3, 0x658ef5cb}, {2, 0xba32ff22}, {5, 0x0227a10c}, - {1, 0xa9a70155}, {5, 0xd096d809}, {1, 0x37176174}, {1, 0x830b8d0f}, - {1, 0xc6e3910e}, {2, 0x823f3ca8}, {1, 0x99850849}, {1, 0x7521fb81}, - {1, 0xaacaabab}, {1, 0xd645a2eb}, {5, 0x7aea1781}, {5, 0x9d6e4b78}, - {1, 0x4ce90fd8}, {1, 0xabdc832d}, {6, 0x4a34f32a}, {2, 0xf2524c1c}, - {2, 0x1bbeb08a}, {1, 0xad47f480}, {1, 0x9f026aeb}, {1, 0x15a95049}, - {2, 0xd1cb95b2}, {2, 0xf84bbda5}, {1, 0x0fa62cd1}, {1, 0xe05f9169}, - {1, 0x78d194a9}, {5, 0x3e38147b}, {5, 0x737ba0d4}, {1, 0x63378e10}, - {1, 0x6d5f91cf}, {2, 0x88612eb8}, {2, 0xe9639484}, {1, 0xb7fabc9d}, - {2, 0x19b01592}, {1, 0x5a90dd31}, {2, 0x5bd7e028}, {2, 0x94d00323}, - {1, 0xa9b9c01a}, {1, 0x3a40de61}, {1, 0x56e7eec7}, {5, 0x859f7ef6}, - {1, 0xfd8e5630}, {1, 0x2b0c9f7f}, {1, 0xba700e26}, {1, 0x7170a408}, - {1, 0x70de86a8}, {1, 0x74d64cd5}, {1, 0x49e738a1}, {2, 0x6910b602}, - {0, 0x643c565f}, {1, 0x54264b3f}, {2, 0x97ea6396}, {2, 0x55174459}, - {2, 0x03e8779a}, {1, 0x98f34d8f}, {1, 0xc07b2b07}, {1, 0xdfe29668}, - {1, 0x3141c7c1}, {1, 0xb3b595f4}, {1, 0x735abf08}, {5, 0x623bfbce}, - {2, 0xd351e722}, {1, 0xf4ca48c9}, {1, 0x5b19c670}, {1, 0xa164bf0e}, - {2, 0xbbbeb305}, {2, 0xfe1c810a}, -}; +} BLOCKINFO[]{{8, 582909131}, {0, 971462344}, {2, 1169481553}, {6, 66147495}, {7, 427785981}, {8, 80538907}, + {8, 207348013}, {2, 1951240923}, {4, 215054351}, {1, 491520534}, {8, 1282281282}, {4, 639565734}, + {3, 248274685}, {8, 1160085976}, {6, 396349768}, {5, 393780549}, {5, 1096899528}, {4, 965381630}, + {0, 728758712}, {5, 318638310}, {3, 164591898}, {2, 274234550}, {2, 254411237}, {7, 561761812}, + {2, 268342573}, {0, 402816691}, {1, 221006382}, {6, 538872455}, {7, 393315655}, {4, 814555937}, + {7, 504879194}, {6, 467769648}, {3, 925972193}, {2, 200581872}, {3, 168915404}, {8, 430446262}, + {5, 773507406}, {3, 1195366164}, {0, 433361157}, {3, 297051771}, {0, 558856551}, {2, 501614039}, + {3, 528488272}, {2, 473587734}, {8, 230125274}, {2, 494084400}, {4, 357314010}, {8, 60361686}, + {7, 640624687}, {3, 480441695}, {8, 1424447925}, {4, 752745419}, {1, 288532283}, {6, 669170574}, + {5, 1900907591}, {3, 555326037}, {3, 1121014051}, {0, 545835650}, {8, 189196651}, {5, 252371575}, + {0, 199163095}, {6, 558895874}, {6, 1656839784}, {6, 815175452}, {6, 718677851}, {5, 544000334}, + {0, 340113484}, {6, 850744437}, {4, 496721063}, {8, 524715182}, {6, 574361898}, {6, 1642305743}, + {6, 355110149}, {5, 1647379658}, {8, 1103005356}, {7, 556460625}, {3, 1139533992}, {5, 304736030}, + {2, 361539446}, {2, 143720360}, {6, 201939025}, {7, 423141476}, {4, 574633709}, {3, 1412254823}, + {4, 873254135}, {0, 341817335}, {6, 53501687}, {3, 179755410}, {5, 172209688}, {8, 516810279}, + {4, 1228391489}, {8, 325372589}, {6, 550367589}, {0, 876291812}, {7, 412454120}, {7, 717202854}, + {2, 222677843}, {6, 251778867}, {7, 842004420}, {7, 194762829}, {4, 96668841}, {1, 925485796}, + {0, 792342903}, {6, 678455063}, {6, 773251385}, {5, 186617471}, {6, 883189502}, {7, 396077336}, + {8, 254702874}, {0, 455592851}}; static CBlockIndex CreateBlockIndex(int nHeight, CBlockIndex* active_chain_tip) EXCLUSIVE_LOCKS_REQUIRED(cs_main) { @@ -220,20 +210,18 @@ BOOST_AUTO_TEST_CASE(CreateNewBlock_validity) // We can't make transactions until we have inputs // Therefore, load 110 blocks :) - static_assert(std::size(blockinfo) == 110, "Should have 110 blocks to import"); + static_assert(std::size(BLOCKINFO) == 110, "Should have 110 blocks to import"); int baseheight = 0; std::vector<CTransactionRef> txFirst; - for (const auto& bi : blockinfo) { + for (const auto& bi : BLOCKINFO) { CBlock *pblock = &pblocktemplate->block; // pointer for convenience { LOCK(cs_main); - pblock->nVersion = 1; + pblock->nVersion = VERSIONBITS_TOP_BITS; pblock->nTime = m_node.chainman->ActiveChain().Tip()->GetMedianTimePast()+1; CMutableTransaction txCoinbase(*pblock->vtx[0]); txCoinbase.nVersion = 1; - txCoinbase.vin[0].scriptSig = CScript(); - txCoinbase.vin[0].scriptSig.push_back(bi.extranonce); - txCoinbase.vin[0].scriptSig.push_back(m_node.chainman->ActiveChain().Height()); + txCoinbase.vin[0].scriptSig = CScript{} << (m_node.chainman->ActiveChain().Height() + 1) << bi.extranonce; txCoinbase.vout.resize(1); // Ignore the (optional) segwit commitment added by CreateNewBlock (as the hardcoded nonces don't account for this) txCoinbase.vout[0].scriptPubKey = CScript(); pblock->vtx[0] = MakeTransactionRef(std::move(txCoinbase)); diff --git a/src/test/net_tests.cpp b/src/test/net_tests.cpp index acbbf357d2..803a7b8b15 100644 --- a/src/test/net_tests.cpp +++ b/src/test/net_tests.cpp @@ -2,8 +2,6 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. -#include <addrdb.h> -#include <addrman.h> #include <chainparams.h> #include <clientversion.h> #include <cstdint> @@ -29,65 +27,6 @@ using namespace std::literals; -class CAddrManSerializationMock : public CAddrMan -{ -public: - virtual void Serialize(CDataStream& s) const = 0; - - //! Ensure that bucket placement is always the same for testing purposes. - void MakeDeterministic() - { - LOCK(cs); - nKey.SetNull(); - insecure_rand = FastRandomContext(true); - } -}; - -class CAddrManUncorrupted : public CAddrManSerializationMock -{ -public: - void Serialize(CDataStream& s) const override - { - CAddrMan::Serialize(s); - } -}; - -class CAddrManCorrupted : public CAddrManSerializationMock -{ -public: - void Serialize(CDataStream& s) const override - { - // Produces corrupt output that claims addrman has 20 addrs when it only has one addr. - unsigned char nVersion = 1; - s << nVersion; - s << ((unsigned char)32); - s << nKey; - s << 10; // nNew - s << 10; // nTried - - int nUBuckets = ADDRMAN_NEW_BUCKET_COUNT ^ (1 << 30); - s << nUBuckets; - - CService serv; - BOOST_CHECK(Lookup("252.1.1.1", serv, 7777, false)); - CAddress addr = CAddress(serv, NODE_NONE); - CNetAddr resolved; - BOOST_CHECK(LookupHost("252.2.2.2", resolved, false)); - CAddrInfo info = CAddrInfo(addr, resolved); - s << info; - } -}; - -static CDataStream AddrmanToStream(const CAddrManSerializationMock& _addrman) -{ - CDataStream ssPeersIn(SER_DISK, CLIENT_VERSION); - ssPeersIn << Params().MessageStart(); - ssPeersIn << _addrman; - std::string str = ssPeersIn.str(); - std::vector<unsigned char> vchData(str.begin(), str.end()); - return CDataStream(vchData, SER_DISK, CLIENT_VERSION); -} - BOOST_FIXTURE_TEST_SUITE(net_tests, BasicTestingSetup) BOOST_AUTO_TEST_CASE(cnode_listen_port) @@ -102,82 +41,6 @@ BOOST_AUTO_TEST_CASE(cnode_listen_port) BOOST_CHECK(port == altPort); } -BOOST_AUTO_TEST_CASE(caddrdb_read) -{ - CAddrManUncorrupted addrmanUncorrupted; - addrmanUncorrupted.MakeDeterministic(); - - CService addr1, addr2, addr3; - BOOST_CHECK(Lookup("250.7.1.1", addr1, 8333, false)); - BOOST_CHECK(Lookup("250.7.2.2", addr2, 9999, false)); - BOOST_CHECK(Lookup("250.7.3.3", addr3, 9999, false)); - BOOST_CHECK(Lookup("250.7.3.3"s, addr3, 9999, false)); - BOOST_CHECK(!Lookup("250.7.3.3\0example.com"s, addr3, 9999, false)); - - // Add three addresses to new table. - CService source; - BOOST_CHECK(Lookup("252.5.1.1", source, 8333, false)); - BOOST_CHECK(addrmanUncorrupted.Add(CAddress(addr1, NODE_NONE), source)); - BOOST_CHECK(addrmanUncorrupted.Add(CAddress(addr2, NODE_NONE), source)); - BOOST_CHECK(addrmanUncorrupted.Add(CAddress(addr3, NODE_NONE), source)); - - // Test that the de-serialization does not throw an exception. - CDataStream ssPeers1 = AddrmanToStream(addrmanUncorrupted); - bool exceptionThrown = false; - CAddrMan addrman1; - - BOOST_CHECK(addrman1.size() == 0); - try { - unsigned char pchMsgTmp[4]; - ssPeers1 >> pchMsgTmp; - ssPeers1 >> addrman1; - } catch (const std::exception&) { - exceptionThrown = true; - } - - BOOST_CHECK(addrman1.size() == 3); - BOOST_CHECK(exceptionThrown == false); - - // Test that CAddrDB::Read creates an addrman with the correct number of addrs. - CDataStream ssPeers2 = AddrmanToStream(addrmanUncorrupted); - - CAddrMan addrman2; - BOOST_CHECK(addrman2.size() == 0); - BOOST_CHECK(CAddrDB::Read(addrman2, ssPeers2)); - BOOST_CHECK(addrman2.size() == 3); -} - - -BOOST_AUTO_TEST_CASE(caddrdb_read_corrupted) -{ - CAddrManCorrupted addrmanCorrupted; - addrmanCorrupted.MakeDeterministic(); - - // Test that the de-serialization of corrupted addrman throws an exception. - CDataStream ssPeers1 = AddrmanToStream(addrmanCorrupted); - bool exceptionThrown = false; - CAddrMan addrman1; - BOOST_CHECK(addrman1.size() == 0); - try { - unsigned char pchMsgTmp[4]; - ssPeers1 >> pchMsgTmp; - ssPeers1 >> addrman1; - } catch (const std::exception&) { - exceptionThrown = true; - } - // Even through de-serialization failed addrman is not left in a clean state. - BOOST_CHECK(addrman1.size() == 1); - BOOST_CHECK(exceptionThrown); - - // Test that CAddrDB::Read leaves addrman in a clean state if de-serialization fails. - CDataStream ssPeers2 = AddrmanToStream(addrmanCorrupted); - - CAddrMan addrman2; - BOOST_CHECK(addrman2.size() == 0); - BOOST_CHECK(!CAddrDB::Read(addrman2, ssPeers2)); - BOOST_CHECK(addrman2.size() == 0); -} - BOOST_AUTO_TEST_CASE(cnode_simple_test) { SOCKET hSocket = INVALID_SOCKET; diff --git a/src/test/script_tests.cpp b/src/test/script_tests.cpp index 56e2aa63b9..2c39cbffb9 100644 --- a/src/test/script_tests.cpp +++ b/src/test/script_tests.cpp @@ -1160,7 +1160,7 @@ SignatureData CombineSignatures(const CTxOut& txout, const CMutableTransaction& SignatureData data; data.MergeSignatureData(scriptSig1); data.MergeSignatureData(scriptSig2); - ProduceSignature(DUMMY_SIGNING_PROVIDER, MutableTransactionSignatureCreator(&tx, 0, txout.nValue), txout.scriptPubKey, data); + ProduceSignature(DUMMY_SIGNING_PROVIDER, MutableTransactionSignatureCreator(&tx, 0, txout.nValue, SIGHASH_DEFAULT), txout.scriptPubKey, data); return data; } diff --git a/src/test/serfloat_tests.cpp b/src/test/serfloat_tests.cpp index 7876c0bcda..15612e2950 100644 --- a/src/test/serfloat_tests.cpp +++ b/src/test/serfloat_tests.cpp @@ -102,11 +102,12 @@ BOOST_AUTO_TEST_CASE(double_serfloat_tests) { Python code to generate the below hashes: def reversed_hex(x): - return binascii.hexlify(''.join(reversed(x))) + return bytes(reversed(x)).hex() + def dsha256(x): return hashlib.sha256(hashlib.sha256(x).digest()).digest() - reversed_hex(dsha256(''.join(struct.pack('<d', x) for x in range(0,1000)))) == '43d0c82591953c4eafe114590d392676a01585d25b25d433557f0d7878b23f96' + reversed_hex(dsha256(b''.join(struct.pack('<d', x) for x in range(0,1000)))) == '43d0c82591953c4eafe114590d392676a01585d25b25d433557f0d7878b23f96' */ BOOST_AUTO_TEST_CASE(doubles) { diff --git a/src/test/transaction_tests.cpp b/src/test/transaction_tests.cpp index 571f792a53..97fd0600fa 100644 --- a/src/test/transaction_tests.cpp +++ b/src/test/transaction_tests.cpp @@ -561,7 +561,7 @@ SignatureData CombineSignatures(const CMutableTransaction& input1, const CMutabl SignatureData sigdata; sigdata = DataFromTransaction(input1, 0, tx->vout[0]); sigdata.MergeSignatureData(DataFromTransaction(input2, 0, tx->vout[0])); - ProduceSignature(DUMMY_SIGNING_PROVIDER, MutableTransactionSignatureCreator(&input1, 0, tx->vout[0].nValue), tx->vout[0].scriptPubKey, sigdata); + ProduceSignature(DUMMY_SIGNING_PROVIDER, MutableTransactionSignatureCreator(&input1, 0, tx->vout[0].nValue, SIGHASH_ALL), tx->vout[0].scriptPubKey, sigdata); return sigdata; } diff --git a/src/test/util/setup_common.cpp b/src/test/util/setup_common.cpp index 5334c4623e..c9bb863a7b 100644 --- a/src/test/util/setup_common.cpp +++ b/src/test/util/setup_common.cpp @@ -193,11 +193,11 @@ TestingSetup::TestingSetup(const std::string& chainName, const std::vector<const throw std::runtime_error(strprintf("ActivateBestChain failed. (%s)", state.ToString())); } - m_node.addrman = std::make_unique<CAddrMan>(); + m_node.addrman = std::make_unique<CAddrMan>(/* deterministic */ false, /* consistency_check_ratio */ 0); m_node.banman = std::make_unique<BanMan>(m_args.GetDataDirBase() / "banlist", nullptr, DEFAULT_MISBEHAVING_BANTIME); m_node.connman = std::make_unique<CConnman>(0x1337, 0x1337, *m_node.addrman); // Deterministic randomness for tests. m_node.peerman = PeerManager::make(chainparams, *m_node.connman, *m_node.addrman, - m_node.banman.get(), *m_node.scheduler, *m_node.chainman, + m_node.banman.get(), *m_node.chainman, *m_node.mempool, false); { CConnman::Options options; @@ -292,7 +292,7 @@ CMutableTransaction TestChain100Setup::CreateValidMempoolTransaction(CTransactio input_coins.insert({outpoint_to_spend, utxo_to_spend}); // - Default signature hashing type int nHashType = SIGHASH_ALL; - std::map<int, std::string> input_errors; + std::map<int, bilingual_str> input_errors; assert(SignTransaction(mempool_txn, &keystore, input_coins, nHashType, input_errors)); // If submit=true, add transaction to the mempool. diff --git a/src/test/util/wallet.cpp b/src/test/util/wallet.cpp index fd6012e9fe..061659818f 100644 --- a/src/test/util/wallet.cpp +++ b/src/test/util/wallet.cpp @@ -8,6 +8,7 @@ #include <outputtype.h> #include <script/standard.h> #ifdef ENABLE_WALLET +#include <util/translation.h> #include <wallet/wallet.h> #endif @@ -18,7 +19,7 @@ std::string getnewaddress(CWallet& w) { constexpr auto output_type = OutputType::BECH32; CTxDestination dest; - std::string error; + bilingual_str error; if (!w.GetNewDestination(output_type, "", dest, error)) assert(false); return EncodeDestination(dest); diff --git a/src/txmempool.cpp b/src/txmempool.cpp index c5a4bbf1b0..d5a888ac67 100644 --- a/src/txmempool.cpp +++ b/src/txmempool.cpp @@ -151,33 +151,17 @@ void CTxMemPool::UpdateTransactionsFromBlock(const std::vector<uint256> &vHashes } } -bool CTxMemPool::CalculateMemPoolAncestors(const CTxMemPoolEntry &entry, setEntries &setAncestors, uint64_t limitAncestorCount, uint64_t limitAncestorSize, uint64_t limitDescendantCount, uint64_t limitDescendantSize, std::string &errString, bool fSearchForParents /* = true */) const +bool CTxMemPool::CalculateAncestorsAndCheckLimits(size_t entry_size, + size_t entry_count, + setEntries& setAncestors, + CTxMemPoolEntry::Parents& staged_ancestors, + uint64_t limitAncestorCount, + uint64_t limitAncestorSize, + uint64_t limitDescendantCount, + uint64_t limitDescendantSize, + std::string &errString) const { - CTxMemPoolEntry::Parents staged_ancestors; - const CTransaction &tx = entry.GetTx(); - - if (fSearchForParents) { - // Get parents of this transaction that are in the mempool - // GetMemPoolParents() is only valid for entries in the mempool, so we - // iterate mapTx to find parents. - for (unsigned int i = 0; i < tx.vin.size(); i++) { - std::optional<txiter> piter = GetIter(tx.vin[i].prevout.hash); - if (piter) { - staged_ancestors.insert(**piter); - if (staged_ancestors.size() + 1 > limitAncestorCount) { - errString = strprintf("too many unconfirmed parents [limit: %u]", limitAncestorCount); - return false; - } - } - } - } else { - // If we're not searching for parents, we require this to be an - // entry in the mempool already. - txiter it = mapTx.iterator_to(entry); - staged_ancestors = it->GetMemPoolParentsConst(); - } - - size_t totalSizeWithAncestors = entry.GetTxSize(); + size_t totalSizeWithAncestors = entry_size; while (!staged_ancestors.empty()) { const CTxMemPoolEntry& stage = staged_ancestors.begin()->get(); @@ -187,10 +171,10 @@ bool CTxMemPool::CalculateMemPoolAncestors(const CTxMemPoolEntry &entry, setEntr staged_ancestors.erase(stage); totalSizeWithAncestors += stageit->GetTxSize(); - if (stageit->GetSizeWithDescendants() + entry.GetTxSize() > limitDescendantSize) { + if (stageit->GetSizeWithDescendants() + entry_size > limitDescendantSize) { errString = strprintf("exceeds descendant size limit for tx %s [limit: %u]", stageit->GetTx().GetHash().ToString(), limitDescendantSize); return false; - } else if (stageit->GetCountWithDescendants() + 1 > limitDescendantCount) { + } else if (stageit->GetCountWithDescendants() + entry_count > limitDescendantCount) { errString = strprintf("too many descendants for tx %s [limit: %u]", stageit->GetTx().GetHash().ToString(), limitDescendantCount); return false; } else if (totalSizeWithAncestors > limitAncestorSize) { @@ -206,7 +190,7 @@ bool CTxMemPool::CalculateMemPoolAncestors(const CTxMemPoolEntry &entry, setEntr if (setAncestors.count(parent_it) == 0) { staged_ancestors.insert(parent); } - if (staged_ancestors.size() + setAncestors.size() + 1 > limitAncestorCount) { + if (staged_ancestors.size() + setAncestors.size() + entry_count > limitAncestorCount) { errString = strprintf("too many unconfirmed ancestors [limit: %u]", limitAncestorCount); return false; } @@ -216,6 +200,80 @@ bool CTxMemPool::CalculateMemPoolAncestors(const CTxMemPoolEntry &entry, setEntr return true; } +bool CTxMemPool::CheckPackageLimits(const Package& package, + uint64_t limitAncestorCount, + uint64_t limitAncestorSize, + uint64_t limitDescendantCount, + uint64_t limitDescendantSize, + std::string &errString) const +{ + CTxMemPoolEntry::Parents staged_ancestors; + size_t total_size = 0; + for (const auto& tx : package) { + total_size += GetVirtualTransactionSize(*tx); + for (const auto& input : tx->vin) { + std::optional<txiter> piter = GetIter(input.prevout.hash); + if (piter) { + staged_ancestors.insert(**piter); + if (staged_ancestors.size() + package.size() > limitAncestorCount) { + errString = strprintf("too many unconfirmed parents [limit: %u]", limitAncestorCount); + return false; + } + } + } + } + // When multiple transactions are passed in, the ancestors and descendants of all transactions + // considered together must be within limits even if they are not interdependent. This may be + // stricter than the limits for each individual transaction. + setEntries setAncestors; + const auto ret = CalculateAncestorsAndCheckLimits(total_size, package.size(), + setAncestors, staged_ancestors, + limitAncestorCount, limitAncestorSize, + limitDescendantCount, limitDescendantSize, errString); + // It's possible to overestimate the ancestor/descendant totals. + if (!ret) errString.insert(0, "possibly "); + return ret; +} + +bool CTxMemPool::CalculateMemPoolAncestors(const CTxMemPoolEntry &entry, + setEntries &setAncestors, + uint64_t limitAncestorCount, + uint64_t limitAncestorSize, + uint64_t limitDescendantCount, + uint64_t limitDescendantSize, + std::string &errString, + bool fSearchForParents /* = true */) const +{ + CTxMemPoolEntry::Parents staged_ancestors; + const CTransaction &tx = entry.GetTx(); + + if (fSearchForParents) { + // Get parents of this transaction that are in the mempool + // GetMemPoolParents() is only valid for entries in the mempool, so we + // iterate mapTx to find parents. + for (unsigned int i = 0; i < tx.vin.size(); i++) { + std::optional<txiter> piter = GetIter(tx.vin[i].prevout.hash); + if (piter) { + staged_ancestors.insert(**piter); + if (staged_ancestors.size() + 1 > limitAncestorCount) { + errString = strprintf("too many unconfirmed parents [limit: %u]", limitAncestorCount); + return false; + } + } + } + } else { + // If we're not searching for parents, we require this to already be an + // entry in the mempool and use the entry's cached parents. + txiter it = mapTx.iterator_to(entry); + staged_ancestors = it->GetMemPoolParentsConst(); + } + + return CalculateAncestorsAndCheckLimits(entry.GetTxSize(), /* entry_count */ 1, + setAncestors, staged_ancestors, + limitAncestorCount, limitAncestorSize, + limitDescendantCount, limitDescendantSize, errString); +} + void CTxMemPool::UpdateAncestorsOf(bool add, txiter it, setEntries &setAncestors) { CTxMemPoolEntry::Parents parents = it->GetMemPoolParents(); diff --git a/src/txmempool.h b/src/txmempool.h index ae4b16d377..0a84a6e6b1 100644 --- a/src/txmempool.h +++ b/src/txmempool.h @@ -18,6 +18,7 @@ #include <coins.h> #include <indirectmap.h> #include <policy/feerate.h> +#include <policy/packages.h> #include <primitives/transaction.h> #include <random.h> #include <sync.h> @@ -585,6 +586,25 @@ private: */ std::set<uint256> m_unbroadcast_txids GUARDED_BY(cs); + + /** + * Helper function to calculate all in-mempool ancestors of staged_ancestors and apply ancestor + * and descendant limits (including staged_ancestors thsemselves, entry_size and entry_count). + * param@[in] entry_size Virtual size to include in the limits. + * param@[in] entry_count How many entries to include in the limits. + * param@[in] staged_ancestors Should contain entries in the mempool. + * param@[out] setAncestors Will be populated with all mempool ancestors. + */ + bool CalculateAncestorsAndCheckLimits(size_t entry_size, + size_t entry_count, + setEntries& setAncestors, + CTxMemPoolEntry::Parents &staged_ancestors, + uint64_t limitAncestorCount, + uint64_t limitAncestorSize, + uint64_t limitDescendantCount, + uint64_t limitDescendantSize, + std::string &errString) const EXCLUSIVE_LOCKS_REQUIRED(cs); + public: indirectmap<COutPoint, const CTransaction*> mapNextTx GUARDED_BY(cs); std::map<uint256, CAmount> mapDeltas GUARDED_BY(cs); @@ -681,6 +701,28 @@ public: */ bool CalculateMemPoolAncestors(const CTxMemPoolEntry& entry, setEntries& setAncestors, uint64_t limitAncestorCount, uint64_t limitAncestorSize, uint64_t limitDescendantCount, uint64_t limitDescendantSize, std::string& errString, bool fSearchForParents = true) const EXCLUSIVE_LOCKS_REQUIRED(cs); + /** Calculate all in-mempool ancestors of a set of transactions not already in the mempool and + * check ancestor and descendant limits. Heuristics are used to estimate the ancestor and + * descendant count of all entries if the package were to be added to the mempool. The limits + * are applied to the union of all package transactions. For example, if the package has 3 + * transactions and limitAncestorCount = 25, the union of all 3 sets of ancestors (including the + * transactions themselves) must be <= 22. + * @param[in] package Transaction package being evaluated for acceptance + * to mempool. The transactions need not be direct + * ancestors/descendants of each other. + * @param[in] limitAncestorCount Max number of txns including ancestors. + * @param[in] limitAncestorSize Max virtual size including ancestors. + * @param[in] limitDescendantCount Max number of txns including descendants. + * @param[in] limitDescendantSize Max virtual size including descendants. + * @param[out] errString Populated with error reason if a limit is hit. + */ + bool CheckPackageLimits(const Package& package, + uint64_t limitAncestorCount, + uint64_t limitAncestorSize, + uint64_t limitDescendantCount, + uint64_t limitDescendantSize, + std::string &errString) const EXCLUSIVE_LOCKS_REQUIRED(cs); + /** Populate setDescendants with all in-mempool descendants of hash. * Assumes that setDescendants includes all in-mempool descendants of anything * already in it. */ diff --git a/src/util/string.h b/src/util/string.h index b26facc502..5617e4acc1 100644 --- a/src/util/string.h +++ b/src/util/string.h @@ -65,6 +65,14 @@ inline std::string Join(const std::vector<std::string>& list, const std::string& } /** + * Create an unordered multi-line list of items. + */ +inline std::string MakeUnorderedList(const std::vector<std::string>& items) +{ + return Join(items, "\n", [](const std::string& item) { return "- " + item; }); +} + +/** * Check if a string does not contain any embedded NUL (\0) characters */ [[nodiscard]] inline bool ValidAsCString(const std::string& str) noexcept diff --git a/src/util/system.cpp b/src/util/system.cpp index 85a23731a2..4e16a83c87 100644 --- a/src/util/system.cpp +++ b/src/util/system.cpp @@ -502,11 +502,11 @@ bool ArgsManager::InitSettings(std::string& error) std::vector<std::string> errors; if (!ReadSettingsFile(&errors)) { - error = strprintf("Failed loading settings file:\n- %s\n", Join(errors, "\n- ")); + error = strprintf("Failed loading settings file:\n%s\n", MakeUnorderedList(errors)); return false; } if (!WriteSettingsFile(&errors)) { - error = strprintf("Failed saving settings file:\n- %s\n", Join(errors, "\n- ")); + error = strprintf("Failed saving settings file:\n%s\n", MakeUnorderedList(errors)); return false; } return true; diff --git a/src/util/system.h b/src/util/system.h index 3547bad585..3c1399629c 100644 --- a/src/util/system.h +++ b/src/util/system.h @@ -205,6 +205,7 @@ protected: */ bool UseDefaultSection(const std::string& arg) const EXCLUSIVE_LOCKS_REQUIRED(cs_args); + public: /** * Get setting value. * @@ -219,7 +220,6 @@ protected: */ std::vector<util::SettingsValue> GetSettingsList(const std::string& arg) const; -public: ArgsManager(); ~ArgsManager(); diff --git a/src/util/translation.h b/src/util/translation.h index 99899ef3c2..62388b568c 100644 --- a/src/util/translation.h +++ b/src/util/translation.h @@ -28,6 +28,12 @@ struct bilingual_str { { return original.empty(); } + + void clear() + { + original.clear(); + translated.clear(); + } }; inline bilingual_str operator+(bilingual_str lhs, const bilingual_str& rhs) diff --git a/src/validation.cpp b/src/validation.cpp index 1b3d00bc6d..ec457da5cc 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -1079,6 +1079,19 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptMultipleTransactions(const std:: m_viewmempool.PackageAddTransaction(ws.m_ptx); } + // Apply package mempool ancestor/descendant limits. Skip if there is only one transaction, + // because it's unnecessary. Also, CPFP carve out can increase the limit for individual + // transactions, but this exemption is not extended to packages in CheckPackageLimits(). + std::string err_string; + if (txns.size() > 1 && + !m_pool.CheckPackageLimits(txns, m_limit_ancestors, m_limit_ancestor_size, m_limit_descendants, + m_limit_descendant_size, err_string)) { + // All transactions must have individually passed mempool ancestor and descendant limits + // inside of PreChecks(), so this is separate from an individual transaction error. + package_state.Invalid(PackageValidationResult::PCKG_POLICY, "package-mempool-limits", err_string); + return PackageMempoolAcceptResult(package_state, std::move(results)); + } + for (Workspace& ws : workspaces) { PrecomputedTransactionData txdata; if (!PolicyScriptChecks(args, ws, txdata)) { diff --git a/src/validation.h b/src/validation.h index 9d8d7c06a9..b80fa9d328 100644 --- a/src/validation.h +++ b/src/validation.h @@ -199,7 +199,8 @@ struct PackageMempoolAcceptResult /** * Map from wtxid to finished MempoolAcceptResults. The client is responsible * for keeping track of the transaction objects themselves. If a result is not - * present, it means validation was unfinished for that transaction. + * present, it means validation was unfinished for that transaction. If there + * was a package-wide error (see result in m_state), m_tx_results will be empty. */ std::map<const uint256, const MempoolAcceptResult> m_tx_results; @@ -227,7 +228,8 @@ MempoolAcceptResult AcceptToMemoryPool(CChainState& active_chainstate, CTxMemPoo * @param[in] txns Group of transactions which may be independent or contain * parent-child dependencies. The transactions must not conflict * with each other, i.e., must not spend the same inputs. If any -* dependencies exist, parents must appear before children. +* dependencies exist, parents must appear anywhere in the list +* before their children. * @returns a PackageMempoolAcceptResult which includes a MempoolAcceptResult for each transaction. * If a transaction fails, validation will exit early and some results may be missing. */ diff --git a/src/wallet/coinselection.cpp b/src/wallet/coinselection.cpp index 6d502e1df1..25b1ee07e4 100644 --- a/src/wallet/coinselection.cpp +++ b/src/wallet/coinselection.cpp @@ -195,7 +195,7 @@ static void ApproximateBestSubset(const std::vector<OutputGroup>& groups, const //the selection random. if (nPass == 0 ? insecure_rand.randbool() : !vfIncluded[i]) { - nTotal += groups[i].m_value; + nTotal += groups[i].GetSelectionAmount(); vfIncluded[i] = true; if (nTotal >= nTargetValue) { @@ -205,7 +205,7 @@ static void ApproximateBestSubset(const std::vector<OutputGroup>& groups, const nBest = nTotal; vfBest = vfIncluded; } - nTotal -= groups[i].m_value; + nTotal -= groups[i].GetSelectionAmount(); vfIncluded[i] = false; } } diff --git a/src/wallet/context.h b/src/wallet/context.h index a83591154f..a382fb9021 100644 --- a/src/wallet/context.h +++ b/src/wallet/context.h @@ -5,11 +5,22 @@ #ifndef BITCOIN_WALLET_CONTEXT_H #define BITCOIN_WALLET_CONTEXT_H +#include <sync.h> + +#include <functional> +#include <list> +#include <memory> +#include <vector> + class ArgsManager; +class CWallet; namespace interfaces { class Chain; +class Wallet; } // namespace interfaces +using LoadWalletFn = std::function<void(std::unique_ptr<interfaces::Wallet> wallet)>; + //! WalletContext struct containing references to state shared between CWallet //! instances, like the reference to the chain interface, and the list of opened //! wallets. @@ -22,7 +33,10 @@ class Chain; //! behavior. struct WalletContext { interfaces::Chain* chain{nullptr}; - ArgsManager* args{nullptr}; + ArgsManager* args{nullptr}; // Currently a raw pointer because the memory is not managed by this struct + Mutex wallets_mutex; + std::vector<std::shared_ptr<CWallet>> wallets GUARDED_BY(wallets_mutex); + std::list<LoadWalletFn> wallet_load_fns GUARDED_BY(wallets_mutex); //! Declare default constructor and destructor that are not inline, so code //! instantiating the WalletContext struct doesn't need to #include class diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index e33adf94c9..0d4b98ecaf 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -16,6 +16,7 @@ #include <uint256.h> #include <util/check.h> #include <util/system.h> +#include <util/translation.h> #include <util/ui_change_type.h> #include <wallet/context.h> #include <wallet/feebumper.h> @@ -109,7 +110,7 @@ WalletTxOut MakeWalletTxOut(const CWallet& wallet, class WalletImpl : public Wallet { public: - explicit WalletImpl(const std::shared_ptr<CWallet>& wallet) : m_wallet(wallet) {} + explicit WalletImpl(WalletContext& context, const std::shared_ptr<CWallet>& wallet) : m_context(context), m_wallet(wallet) {} bool encryptWallet(const SecureString& wallet_passphrase) override { @@ -130,7 +131,7 @@ public: bool getNewDestination(const OutputType type, const std::string label, CTxDestination& dest) override { LOCK(m_wallet->cs_wallet); - std::string error; + bilingual_str error; return m_wallet->GetNewDestination(type, label, dest, error); } bool getPubKey(const CScript& script, const CKeyID& address, CPubKey& pub_key) override @@ -457,7 +458,7 @@ public: CAmount getDefaultMaxTxFee() override { return m_wallet->m_default_max_tx_fee; } void remove() override { - RemoveWallet(m_wallet, false /* load_on_start */); + RemoveWallet(m_context, m_wallet, false /* load_on_start */); } bool isLegacy() override { return m_wallet->IsLegacy(); } std::unique_ptr<Handler> handleUnload(UnloadFn fn) override @@ -493,6 +494,7 @@ public: } CWallet* wallet() override { return m_wallet.get(); } + WalletContext& m_context; std::shared_ptr<CWallet> m_wallet; }; @@ -504,7 +506,7 @@ public: m_context.chain = &chain; m_context.args = &args; } - ~WalletClientImpl() override { UnloadWallets(); } + ~WalletClientImpl() override { UnloadWallets(m_context); } //! ChainClient methods void registerRpcs() override @@ -518,11 +520,11 @@ public: m_rpc_handlers.emplace_back(m_context.chain->handleRpc(m_rpc_commands.back())); } } - bool verify() override { return VerifyWallets(*m_context.chain); } - bool load() override { return LoadWallets(*m_context.chain); } - void start(CScheduler& scheduler) override { return StartWallets(scheduler, *Assert(m_context.args)); } - void flush() override { return FlushWallets(); } - void stop() override { return StopWallets(); } + bool verify() override { return VerifyWallets(m_context); } + bool load() override { return LoadWallets(m_context); } + void start(CScheduler& scheduler) override { return StartWallets(m_context, scheduler); } + void flush() override { return FlushWallets(m_context); } + void stop() override { return StopWallets(m_context); } void setMockTime(int64_t time) override { return SetMockTime(time); } //! WalletClient methods @@ -534,14 +536,14 @@ public: options.require_create = true; options.create_flags = wallet_creation_flags; options.create_passphrase = passphrase; - return MakeWallet(CreateWallet(*m_context.chain, name, true /* load_on_start */, options, status, error, warnings)); + return MakeWallet(m_context, CreateWallet(m_context, name, true /* load_on_start */, options, status, error, warnings)); } std::unique_ptr<Wallet> loadWallet(const std::string& name, bilingual_str& error, std::vector<bilingual_str>& warnings) override { DatabaseOptions options; DatabaseStatus status; options.require_existing = true; - return MakeWallet(LoadWallet(*m_context.chain, name, true /* load_on_start */, options, status, error, warnings)); + return MakeWallet(m_context, LoadWallet(m_context, name, true /* load_on_start */, options, status, error, warnings)); } std::string getWalletDir() override { @@ -558,15 +560,16 @@ public: std::vector<std::unique_ptr<Wallet>> getWallets() override { std::vector<std::unique_ptr<Wallet>> wallets; - for (const auto& wallet : GetWallets()) { - wallets.emplace_back(MakeWallet(wallet)); + for (const auto& wallet : GetWallets(m_context)) { + wallets.emplace_back(MakeWallet(m_context, wallet)); } return wallets; } std::unique_ptr<Handler> handleLoadWallet(LoadWalletFn fn) override { - return HandleLoadWallet(std::move(fn)); + return HandleLoadWallet(m_context, std::move(fn)); } + WalletContext* context() override { return &m_context; } WalletContext m_context; const std::vector<std::string> m_wallet_filenames; @@ -577,7 +580,7 @@ public: } // namespace wallet namespace interfaces { -std::unique_ptr<Wallet> MakeWallet(const std::shared_ptr<CWallet>& wallet) { return wallet ? std::make_unique<wallet::WalletImpl>(wallet) : nullptr; } +std::unique_ptr<Wallet> MakeWallet(WalletContext& context, const std::shared_ptr<CWallet>& wallet) { return wallet ? std::make_unique<wallet::WalletImpl>(context, wallet) : nullptr; } std::unique_ptr<WalletClient> MakeWalletClient(Chain& chain, ArgsManager& args) { diff --git a/src/wallet/load.cpp b/src/wallet/load.cpp index dbf9fd46b6..88e9b9c78f 100644 --- a/src/wallet/load.cpp +++ b/src/wallet/load.cpp @@ -11,13 +11,15 @@ #include <util/string.h> #include <util/system.h> #include <util/translation.h> +#include <wallet/context.h> #include <wallet/wallet.h> #include <wallet/walletdb.h> #include <univalue.h> -bool VerifyWallets(interfaces::Chain& chain) +bool VerifyWallets(WalletContext& context) { + interfaces::Chain& chain = *context.chain; if (gArgs.IsArgSet("-walletdir")) { fs::path wallet_dir = gArgs.GetArg("-walletdir", ""); boost::system::error_code error; @@ -50,18 +52,20 @@ bool VerifyWallets(interfaces::Chain& chain) options.require_existing = true; options.verify = false; if (MakeWalletDatabase("", options, status, error_string)) { - gArgs.LockSettings([&](util::Settings& settings) { - util::SettingsValue wallets(util::SettingsValue::VARR); - wallets.push_back(""); // Default wallet name is "" - settings.rw_settings["wallet"] = wallets; - }); + util::SettingsValue wallets(util::SettingsValue::VARR); + wallets.push_back(""); // Default wallet name is "" + // Pass write=false because no need to write file and probably + // better not to. If unnamed wallet needs to be added next startup + // and the setting is empty, this code will just run again. + chain.updateRwSetting("wallet", wallets, /* write= */ false); } } // Keep track of each wallet absolute path to detect duplicates. std::set<fs::path> wallet_paths; - for (const auto& wallet_file : gArgs.GetArgs("-wallet")) { + for (const auto& wallet : chain.getSettingsList("wallet")) { + const auto& wallet_file = wallet.get_str(); const fs::path path = fsbridge::AbsPathJoin(GetWalletDir(), wallet_file); if (!wallet_paths.insert(path).second) { @@ -87,11 +91,13 @@ bool VerifyWallets(interfaces::Chain& chain) return true; } -bool LoadWallets(interfaces::Chain& chain) +bool LoadWallets(WalletContext& context) { + interfaces::Chain& chain = *context.chain; try { std::set<fs::path> wallet_paths; - for (const std::string& name : gArgs.GetArgs("-wallet")) { + for (const auto& wallet : chain.getSettingsList("wallet")) { + const auto& name = wallet.get_str(); if (!wallet_paths.insert(name).second) { continue; } @@ -106,13 +112,13 @@ bool LoadWallets(interfaces::Chain& chain) continue; } chain.initMessage(_("Loading wallet…").translated); - std::shared_ptr<CWallet> pwallet = database ? CWallet::Create(&chain, name, std::move(database), options.create_flags, error, warnings) : nullptr; + std::shared_ptr<CWallet> pwallet = database ? CWallet::Create(context, name, std::move(database), options.create_flags, error, warnings) : nullptr; if (!warnings.empty()) chain.initWarning(Join(warnings, Untranslated("\n"))); if (!pwallet) { chain.initError(error); return false; } - AddWallet(pwallet); + AddWallet(context, pwallet); } return true; } catch (const std::runtime_error& e) { @@ -121,41 +127,41 @@ bool LoadWallets(interfaces::Chain& chain) } } -void StartWallets(CScheduler& scheduler, const ArgsManager& args) +void StartWallets(WalletContext& context, CScheduler& scheduler) { - for (const std::shared_ptr<CWallet>& pwallet : GetWallets()) { + for (const std::shared_ptr<CWallet>& pwallet : GetWallets(context)) { pwallet->postInitProcess(); } // Schedule periodic wallet flushes and tx rebroadcasts - if (args.GetBoolArg("-flushwallet", DEFAULT_FLUSHWALLET)) { - scheduler.scheduleEvery(MaybeCompactWalletDB, std::chrono::milliseconds{500}); + if (context.args->GetBoolArg("-flushwallet", DEFAULT_FLUSHWALLET)) { + scheduler.scheduleEvery([&context] { MaybeCompactWalletDB(context); }, std::chrono::milliseconds{500}); } - scheduler.scheduleEvery(MaybeResendWalletTxs, std::chrono::milliseconds{1000}); + scheduler.scheduleEvery([&context] { MaybeResendWalletTxs(context); }, std::chrono::milliseconds{1000}); } -void FlushWallets() +void FlushWallets(WalletContext& context) { - for (const std::shared_ptr<CWallet>& pwallet : GetWallets()) { + for (const std::shared_ptr<CWallet>& pwallet : GetWallets(context)) { pwallet->Flush(); } } -void StopWallets() +void StopWallets(WalletContext& context) { - for (const std::shared_ptr<CWallet>& pwallet : GetWallets()) { + for (const std::shared_ptr<CWallet>& pwallet : GetWallets(context)) { pwallet->Close(); } } -void UnloadWallets() +void UnloadWallets(WalletContext& context) { - auto wallets = GetWallets(); + auto wallets = GetWallets(context); while (!wallets.empty()) { auto wallet = wallets.back(); wallets.pop_back(); std::vector<bilingual_str> warnings; - RemoveWallet(wallet, std::nullopt, warnings); + RemoveWallet(context, wallet, /* load_on_startup= */ std::nullopt, warnings); UnloadWallet(std::move(wallet)); } } diff --git a/src/wallet/load.h b/src/wallet/load.h index 7910f0d6e1..e207bc2e09 100644 --- a/src/wallet/load.h +++ b/src/wallet/load.h @@ -11,27 +11,28 @@ class ArgsManager; class CScheduler; +struct WalletContext; namespace interfaces { class Chain; } // namespace interfaces //! Responsible for reading and validating the -wallet arguments and verifying the wallet database. -bool VerifyWallets(interfaces::Chain& chain); +bool VerifyWallets(WalletContext& context); //! Load wallet databases. -bool LoadWallets(interfaces::Chain& chain); +bool LoadWallets(WalletContext& context); //! Complete startup of wallets. -void StartWallets(CScheduler& scheduler, const ArgsManager& args); +void StartWallets(WalletContext& context, CScheduler& scheduler); //! Flush all wallets in preparation for shutdown. -void FlushWallets(); +void FlushWallets(WalletContext& context); //! Stop all wallets. Wallets will be flushed first. -void StopWallets(); +void StopWallets(WalletContext& context); //! Close all wallets. -void UnloadWallets(); +void UnloadWallets(WalletContext& context); #endif // BITCOIN_WALLET_LOAD_H diff --git a/src/wallet/rpcdump.cpp b/src/wallet/rpcdump.cpp index ac60504419..72c60c8fe2 100644 --- a/src/wallet/rpcdump.cpp +++ b/src/wallet/rpcdump.cpp @@ -3,6 +3,7 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include <chain.h> +#include <clientversion.h> #include <core_io.h> #include <interfaces/chain.h> #include <key_io.h> @@ -783,7 +784,7 @@ RPCHelpMan dumpwallet() std::sort(vKeyBirth.begin(), vKeyBirth.end()); // produce output - file << strprintf("# Wallet dump created by Bitcoin %s\n", CLIENT_BUILD); + file << strprintf("# Wallet dump created by %s %s\n", PACKAGE_NAME, FormatFullVersion()); file << strprintf("# * Created on %s\n", FormatISO8601DateTime(GetTime())); file << strprintf("# * Best block at time of backup was %i (%s),\n", wallet.GetLastBlockHeight(), wallet.GetLastBlockHash().ToString()); file << strprintf("# mined on %s\n", FormatISO8601DateTime(block_time)); @@ -1760,8 +1761,10 @@ RPCHelpMan listdescriptors() { return RPCHelpMan{ "listdescriptors", - "\nList descriptors imported into a descriptor-enabled wallet.", - {}, + "\nList descriptors imported into a descriptor-enabled wallet.\n", + { + {"private", RPCArg::Type::BOOL, RPCArg::Default{false}, "Show private descriptors."} + }, RPCResult{RPCResult::Type::OBJ, "", "", { {RPCResult::Type::STR, "wallet_name", "Name of wallet this operation was performed on"}, {RPCResult::Type::ARR, "descriptors", "Array of descriptor objects", @@ -1781,6 +1784,7 @@ RPCHelpMan listdescriptors() }}, RPCExamples{ HelpExampleCli("listdescriptors", "") + HelpExampleRpc("listdescriptors", "") + + HelpExampleCli("listdescriptors", "true") + HelpExampleRpc("listdescriptors", "true") }, [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { @@ -1791,6 +1795,11 @@ RPCHelpMan listdescriptors() throw JSONRPCError(RPC_WALLET_ERROR, "listdescriptors is not available for non-descriptor wallets"); } + const bool priv = !request.params[0].isNull() && request.params[0].get_bool(); + if (priv) { + EnsureWalletIsUnlocked(*wallet); + } + LOCK(wallet->cs_wallet); UniValue descriptors(UniValue::VARR); @@ -1804,8 +1813,9 @@ RPCHelpMan listdescriptors() LOCK(desc_spk_man->cs_desc_man); const auto& wallet_descriptor = desc_spk_man->GetWalletDescriptor(); std::string descriptor; - if (!desc_spk_man->GetDescriptorString(descriptor)) { - throw JSONRPCError(RPC_WALLET_ERROR, "Can't get normalized descriptor string."); + + if (!desc_spk_man->GetDescriptorString(descriptor, priv)) { + throw JSONRPCError(RPC_WALLET_ERROR, "Can't get descriptor string."); } spk.pushKV("desc", descriptor); spk.pushKV("timestamp", wallet_descriptor.creation_time); diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index f1d5117415..916f811f9b 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -96,14 +96,16 @@ bool GetWalletNameFromJSONRPCRequest(const JSONRPCRequest& request, std::string& std::shared_ptr<CWallet> GetWalletForJSONRPCRequest(const JSONRPCRequest& request) { CHECK_NONFATAL(request.mode == JSONRPCRequest::EXECUTE); + WalletContext& context = EnsureWalletContext(request.context); + std::string wallet_name; if (GetWalletNameFromJSONRPCRequest(request, wallet_name)) { - std::shared_ptr<CWallet> pwallet = GetWallet(wallet_name); + std::shared_ptr<CWallet> pwallet = GetWallet(context, wallet_name); if (!pwallet) throw JSONRPCError(RPC_WALLET_NOT_FOUND, "Requested wallet does not exist or is not loaded"); return pwallet; } - std::vector<std::shared_ptr<CWallet>> wallets = GetWallets(); + std::vector<std::shared_ptr<CWallet>> wallets = GetWallets(context); if (wallets.size() == 1) { return wallets[0]; } @@ -266,18 +268,19 @@ static RPCHelpMan getnewaddress() OutputType output_type = pwallet->m_default_address_type; if (!request.params[1].isNull()) { - if (!ParseOutputType(request.params[1].get_str(), output_type)) { + std::optional<OutputType> parsed = ParseOutputType(request.params[1].get_str()); + if (!parsed) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Unknown address type '%s'", request.params[1].get_str())); - } - if (output_type == OutputType::BECH32M && pwallet->GetLegacyScriptPubKeyMan()) { + } else if (parsed.value() == OutputType::BECH32M && pwallet->GetLegacyScriptPubKeyMan()) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Legacy wallets cannot provide bech32m addresses"); } + output_type = parsed.value(); } CTxDestination dest; - std::string error; + bilingual_str error; if (!pwallet->GetNewDestination(output_type, label, dest, error)) { - throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, error); + throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, error.original); } return EncodeDestination(dest); @@ -313,18 +316,19 @@ static RPCHelpMan getrawchangeaddress() OutputType output_type = pwallet->m_default_change_type.value_or(pwallet->m_default_address_type); if (!request.params[0].isNull()) { - if (!ParseOutputType(request.params[0].get_str(), output_type)) { + std::optional<OutputType> parsed = ParseOutputType(request.params[0].get_str()); + if (!parsed) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Unknown address type '%s'", request.params[0].get_str())); - } - if (output_type == OutputType::BECH32M && pwallet->GetLegacyScriptPubKeyMan()) { + } else if (parsed.value() == OutputType::BECH32M && pwallet->GetLegacyScriptPubKeyMan()) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Legacy wallets cannot provide bech32m addresses"); } + output_type = parsed.value(); } CTxDestination dest; - std::string error; + bilingual_str error; if (!pwallet->GetNewChangeDestination(output_type, dest, error)) { - throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, error); + throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, error.original); } return EncodeDestination(dest); }, @@ -1007,12 +1011,13 @@ static RPCHelpMan addmultisigaddress() OutputType output_type = pwallet->m_default_address_type; if (!request.params[3].isNull()) { - if (!ParseOutputType(request.params[3].get_str(), output_type)) { + std::optional<OutputType> parsed = ParseOutputType(request.params[3].get_str()); + if (!parsed) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Unknown address type '%s'", request.params[3].get_str())); - } - if (output_type == OutputType::BECH32M) { + } else if (parsed.value() == OutputType::BECH32M) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Bech32m multisig addresses cannot be created with legacy wallets"); } + output_type = parsed.value(); } // Construct using pay-to-script-hash: @@ -2559,7 +2564,8 @@ static RPCHelpMan listwallets() { UniValue obj(UniValue::VARR); - for (const std::shared_ptr<CWallet>& wallet : GetWallets()) { + WalletContext& context = EnsureWalletContext(request.context); + for (const std::shared_ptr<CWallet>& wallet : GetWallets(context)) { LOCK(wallet->cs_wallet); obj.push_back(wallet->GetName()); } @@ -2569,6 +2575,37 @@ static RPCHelpMan listwallets() }; } +static std::tuple<std::shared_ptr<CWallet>, std::vector<bilingual_str>> LoadWalletHelper(WalletContext& context, UniValue load_on_start_param, const std::string wallet_name) +{ + DatabaseOptions options; + DatabaseStatus status; + options.require_existing = true; + bilingual_str error; + std::vector<bilingual_str> warnings; + std::optional<bool> load_on_start = load_on_start_param.isNull() ? std::nullopt : std::optional<bool>(load_on_start_param.get_bool()); + std::shared_ptr<CWallet> const wallet = LoadWallet(context, wallet_name, load_on_start, options, status, error, warnings); + + if (!wallet) { + // Map bad format to not found, since bad format is returned when the + // wallet directory exists, but doesn't contain a data file. + RPCErrorCode code = RPC_WALLET_ERROR; + switch (status) { + case DatabaseStatus::FAILED_NOT_FOUND: + case DatabaseStatus::FAILED_BAD_FORMAT: + code = RPC_WALLET_NOT_FOUND; + break; + case DatabaseStatus::FAILED_ALREADY_LOADED: + code = RPC_WALLET_ALREADY_LOADED; + break; + default: // RPC_WALLET_ERROR is returned for all other cases. + break; + } + throw JSONRPCError(code, error.original); + } + + return { wallet, warnings }; +} + static RPCHelpMan loadwallet() { return RPCHelpMan{"loadwallet", @@ -2595,30 +2632,7 @@ static RPCHelpMan loadwallet() WalletContext& context = EnsureWalletContext(request.context); const std::string name(request.params[0].get_str()); - DatabaseOptions options; - DatabaseStatus status; - options.require_existing = true; - bilingual_str error; - std::vector<bilingual_str> warnings; - std::optional<bool> load_on_start = request.params[1].isNull() ? std::nullopt : std::optional<bool>(request.params[1].get_bool()); - std::shared_ptr<CWallet> const wallet = LoadWallet(*context.chain, name, load_on_start, options, status, error, warnings); - if (!wallet) { - // Map bad format to not found, since bad format is returned when the - // wallet directory exists, but doesn't contain a data file. - RPCErrorCode code = RPC_WALLET_ERROR; - switch (status) { - case DatabaseStatus::FAILED_NOT_FOUND: - case DatabaseStatus::FAILED_BAD_FORMAT: - code = RPC_WALLET_NOT_FOUND; - break; - case DatabaseStatus::FAILED_ALREADY_LOADED: - code = RPC_WALLET_ALREADY_LOADED; - break; - default: // RPC_WALLET_ERROR is returned for all other cases. - break; - } - throw JSONRPCError(code, error.original); - } + auto [wallet, warnings] = LoadWalletHelper(context, request.params[1], name); UniValue obj(UniValue::VOBJ); obj.pushKV("name", wallet->GetName()); @@ -2777,7 +2791,7 @@ static RPCHelpMan createwallet() options.create_passphrase = passphrase; bilingual_str error; std::optional<bool> load_on_start = request.params[6].isNull() ? std::nullopt : std::optional<bool>(request.params[6].get_bool()); - std::shared_ptr<CWallet> wallet = CreateWallet(*context.chain, request.params[0].get_str(), load_on_start, options, status, error, warnings); + std::shared_ptr<CWallet> wallet = CreateWallet(context, request.params[0].get_str(), load_on_start, options, status, error, warnings); if (!wallet) { RPCErrorCode code = status == DatabaseStatus::FAILED_ENCRYPT ? RPC_WALLET_ENCRYPTION_FAILED : RPC_WALLET_ERROR; throw JSONRPCError(code, error.original); @@ -2792,6 +2806,68 @@ static RPCHelpMan createwallet() }; } +static RPCHelpMan restorewallet() +{ + return RPCHelpMan{ + "restorewallet", + "\nRestore and loads a wallet from backup.\n", + { + {"wallet_name", RPCArg::Type::STR, RPCArg::Optional::NO, "The name that will be applied to the restored wallet"}, + {"backup_file", RPCArg::Type::STR, RPCArg::Optional::NO, "The backup file that will be used to restore the wallet."}, + {"load_on_startup", RPCArg::Type::BOOL, RPCArg::Optional::OMITTED_NAMED_ARG, "Save wallet name to persistent settings and load on startup. True to add wallet to startup list, false to remove, null to leave unchanged."}, + }, + RPCResult{ + RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::STR, "name", "The wallet name if restored successfully."}, + {RPCResult::Type::STR, "warning", "Warning message if wallet was not loaded cleanly."}, + } + }, + RPCExamples{ + HelpExampleCli("restorewallet", "\"testwallet\" \"home\\backups\\backup-file.bak\"") + + HelpExampleRpc("restorewallet", "\"testwallet\" \"home\\backups\\backup-file.bak\"") + + HelpExampleCliNamed("restorewallet", {{"wallet_name", "testwallet"}, {"backup_file", "home\\backups\\backup-file.bak\""}, {"load_on_startup", true}}) + + HelpExampleRpcNamed("restorewallet", {{"wallet_name", "testwallet"}, {"backup_file", "home\\backups\\backup-file.bak\""}, {"load_on_startup", true}}) + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + + WalletContext& context = EnsureWalletContext(request.context); + + std::string backup_file = request.params[1].get_str(); + + if (!fs::exists(backup_file)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Backup file does not exist"); + } + + std::string wallet_name = request.params[0].get_str(); + + const fs::path wallet_path = fsbridge::AbsPathJoin(GetWalletDir(), wallet_name); + + if (fs::exists(wallet_path)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Wallet name already exists."); + } + + if (!TryCreateDirectories(wallet_path)) { + throw JSONRPCError(RPC_WALLET_ERROR, strprintf("Failed to create database path '%s'. Database already exists.", wallet_path.string())); + } + + auto wallet_file = wallet_path / "wallet.dat"; + + fs::copy_file(backup_file, wallet_file, fs::copy_option::fail_if_exists); + + auto [wallet, warnings] = LoadWalletHelper(context, request.params[2], wallet_name); + + UniValue obj(UniValue::VOBJ); + obj.pushKV("name", wallet->GetName()); + obj.pushKV("warning", Join(warnings, Untranslated("\n")).original); + + return obj; + +}, + }; +} + static RPCHelpMan unloadwallet() { return RPCHelpMan{"unloadwallet", @@ -2819,7 +2895,8 @@ static RPCHelpMan unloadwallet() wallet_name = request.params[0].get_str(); } - std::shared_ptr<CWallet> wallet = GetWallet(wallet_name); + WalletContext& context = EnsureWalletContext(request.context); + std::shared_ptr<CWallet> wallet = GetWallet(context, wallet_name); if (!wallet) { throw JSONRPCError(RPC_WALLET_NOT_FOUND, "Requested wallet does not exist or is not loaded"); } @@ -2829,7 +2906,7 @@ static RPCHelpMan unloadwallet() // is destroyed (see CheckUniqueFileid). std::vector<bilingual_str> warnings; std::optional<bool> load_on_start = request.params[1].isNull() ? std::nullopt : std::optional<bool>(request.params[1].get_bool()); - if (!RemoveWallet(wallet, load_on_start, warnings)) { + if (!RemoveWallet(context, wallet, load_on_start, warnings)) { throw JSONRPCError(RPC_MISC_ERROR, "Requested wallet already unloaded"); } @@ -3133,11 +3210,11 @@ void FundTransaction(CWallet& wallet, CMutableTransaction& tx, CAmount& fee_out, if (options.exists("changeAddress") || options.exists("change_address")) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot specify both change address and address type options"); } - OutputType out_type; - if (!ParseOutputType(options["change_type"].get_str(), out_type)) { + if (std::optional<OutputType> parsed = ParseOutputType(options["change_type"].get_str())) { + coinControl.m_change_type.emplace(parsed.value()); + } else { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Unknown change type '%s'", options["change_type"].get_str())); } - coinControl.m_change_type.emplace(out_type); } const UniValue include_watching_option = options.exists("include_watching") ? options["include_watching"] : options["includeWatching"]; @@ -3389,7 +3466,7 @@ RPCHelpMan signrawtransactionwithwallet() int nHashType = ParseSighashString(request.params[2]); // Script verification errors - std::map<int, std::string> input_errors; + std::map<int, bilingual_str> input_errors; bool complete = pwallet->SignTransaction(mtx, coins, nHashType, input_errors); UniValue result(UniValue::VOBJ); @@ -3872,7 +3949,7 @@ RPCHelpMan getaddressinfo() DescriptorScriptPubKeyMan* desc_spk_man = dynamic_cast<DescriptorScriptPubKeyMan*>(pwallet->GetScriptPubKeyMan(scriptPubKey)); if (desc_spk_man) { std::string desc_str; - if (desc_spk_man->GetDescriptorString(desc_str)) { + if (desc_spk_man->GetDescriptorString(desc_str, /* priv */ false)) { ret.pushKV("parent_desc", desc_str); } } @@ -4636,6 +4713,7 @@ static const CRPCCommand commands[] = { "wallet", &bumpfee, }, { "wallet", &psbtbumpfee, }, { "wallet", &createwallet, }, + { "wallet", &restorewallet, }, { "wallet", &dumpprivkey, }, { "wallet", &dumpwallet, }, { "wallet", &encryptwallet, }, diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp index 73433214f1..fe41f9b8cc 100644 --- a/src/wallet/scriptpubkeyman.cpp +++ b/src/wallet/scriptpubkeyman.cpp @@ -20,10 +20,10 @@ //! Value for the first BIP 32 hardened derivation. Can be used as a bit mask and as a value. See BIP 32 for more details. const uint32_t BIP32_HARDENED_KEY_LIMIT = 0x80000000; -bool LegacyScriptPubKeyMan::GetNewDestination(const OutputType type, CTxDestination& dest, std::string& error) +bool LegacyScriptPubKeyMan::GetNewDestination(const OutputType type, CTxDestination& dest, bilingual_str& error) { if (LEGACY_OUTPUT_TYPES.count(type) == 0) { - error = _("Error: Legacy wallets only support the \"legacy\", \"p2sh-segwit\", and \"bech32\" address types").translated; + error = _("Error: Legacy wallets only support the \"legacy\", \"p2sh-segwit\", and \"bech32\" address types"); return false; } assert(type != OutputType::BECH32M); @@ -34,7 +34,7 @@ bool LegacyScriptPubKeyMan::GetNewDestination(const OutputType type, CTxDestinat // Generate a new key that is added to wallet CPubKey new_key; if (!GetKeyFromPool(new_key, type)) { - error = _("Error: Keypool ran out, please call keypoolrefill first").translated; + error = _("Error: Keypool ran out, please call keypoolrefill first"); return false; } LearnRelatedScripts(new_key, type); @@ -295,22 +295,22 @@ bool LegacyScriptPubKeyMan::Encrypt(const CKeyingMaterial& master_key, WalletBat return true; } -bool LegacyScriptPubKeyMan::GetReservedDestination(const OutputType type, bool internal, CTxDestination& address, int64_t& index, CKeyPool& keypool, std::string& error) +bool LegacyScriptPubKeyMan::GetReservedDestination(const OutputType type, bool internal, CTxDestination& address, int64_t& index, CKeyPool& keypool, bilingual_str& error) { if (LEGACY_OUTPUT_TYPES.count(type) == 0) { - error = _("Error: Legacy wallets only support the \"legacy\", \"p2sh-segwit\", and \"bech32\" address types").translated; + error = _("Error: Legacy wallets only support the \"legacy\", \"p2sh-segwit\", and \"bech32\" address types"); return false; } assert(type != OutputType::BECH32M); LOCK(cs_KeyStore); if (!CanGetAddresses(internal)) { - error = _("Error: Keypool ran out, please call keypoolrefill first").translated; + error = _("Error: Keypool ran out, please call keypoolrefill first"); return false; } if (!ReserveKeyFromKeyPool(index, keypool, internal)) { - error = _("Error: Keypool ran out, please call keypoolrefill first").translated; + error = _("Error: Keypool ran out, please call keypoolrefill first"); return false; } address = GetDestinationForKey(keypool.vchPubKey, type); @@ -592,7 +592,7 @@ bool LegacyScriptPubKeyMan::CanProvide(const CScript& script, SignatureData& sig } } -bool LegacyScriptPubKeyMan::SignTransaction(CMutableTransaction& tx, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, std::string>& input_errors) const +bool LegacyScriptPubKeyMan::SignTransaction(CMutableTransaction& tx, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, bilingual_str>& input_errors) const { return ::SignTransaction(tx, this, coins, sighash, input_errors); } @@ -1613,11 +1613,11 @@ std::set<CKeyID> LegacyScriptPubKeyMan::GetKeys() const return set_address; } -bool DescriptorScriptPubKeyMan::GetNewDestination(const OutputType type, CTxDestination& dest, std::string& error) +bool DescriptorScriptPubKeyMan::GetNewDestination(const OutputType type, CTxDestination& dest, bilingual_str& error) { // Returns true if this descriptor supports getting new addresses. Conditions where we may be unable to fetch them (e.g. locked) are caught later if (!CanGetAddresses()) { - error = "No addresses available"; + error = _("No addresses available"); return false; } { @@ -1636,12 +1636,12 @@ bool DescriptorScriptPubKeyMan::GetNewDestination(const OutputType type, CTxDest std::vector<CScript> scripts_temp; if (m_wallet_descriptor.range_end <= m_max_cached_index && !TopUp(1)) { // We can't generate anymore keys - error = "Error: Keypool ran out, please call keypoolrefill first"; + error = _("Error: Keypool ran out, please call keypoolrefill first"); return false; } if (!m_wallet_descriptor.descriptor->ExpandFromCache(m_wallet_descriptor.next_index, m_wallet_descriptor.cache, scripts_temp, out_keys)) { // We can't generate anymore keys - error = "Error: Keypool ran out, please call keypoolrefill first"; + error = _("Error: Keypool ran out, please call keypoolrefill first"); return false; } @@ -1721,7 +1721,7 @@ bool DescriptorScriptPubKeyMan::Encrypt(const CKeyingMaterial& master_key, Walle return true; } -bool DescriptorScriptPubKeyMan::GetReservedDestination(const OutputType type, bool internal, CTxDestination& address, int64_t& index, CKeyPool& keypool, std::string& error) +bool DescriptorScriptPubKeyMan::GetReservedDestination(const OutputType type, bool internal, CTxDestination& address, int64_t& index, CKeyPool& keypool, bilingual_str& error) { LOCK(cs_desc_man); bool result = GetNewDestination(type, address, error); @@ -2046,7 +2046,7 @@ bool DescriptorScriptPubKeyMan::CanProvide(const CScript& script, SignatureData& return IsMine(script); } -bool DescriptorScriptPubKeyMan::SignTransaction(CMutableTransaction& tx, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, std::string>& input_errors) const +bool DescriptorScriptPubKeyMan::SignTransaction(CMutableTransaction& tx, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, bilingual_str>& input_errors) const { std::unique_ptr<FlatSigningProvider> keys = std::make_unique<FlatSigningProvider>(); for (const auto& coin_pair : coins) { @@ -2258,13 +2258,20 @@ const std::vector<CScript> DescriptorScriptPubKeyMan::GetScriptPubKeys() const return script_pub_keys; } -bool DescriptorScriptPubKeyMan::GetDescriptorString(std::string& out) const +bool DescriptorScriptPubKeyMan::GetDescriptorString(std::string& out, const bool priv) const { LOCK(cs_desc_man); FlatSigningProvider provider; provider.keys = GetKeys(); + if (priv) { + // For the private version, always return the master key to avoid + // exposing child private keys. The risk implications of exposing child + // private keys together with the parent xpub may be non-obvious for users. + return m_wallet_descriptor.descriptor->ToPrivateString(provider, out); + } + return m_wallet_descriptor.descriptor->ToNormalizedString(provider, out, &m_wallet_descriptor.cache); } diff --git a/src/wallet/scriptpubkeyman.h b/src/wallet/scriptpubkeyman.h index e329e0cf8f..93e1886102 100644 --- a/src/wallet/scriptpubkeyman.h +++ b/src/wallet/scriptpubkeyman.h @@ -174,14 +174,14 @@ protected: public: explicit ScriptPubKeyMan(WalletStorage& storage) : m_storage(storage) {} virtual ~ScriptPubKeyMan() {}; - virtual bool GetNewDestination(const OutputType type, CTxDestination& dest, std::string& error) { return false; } + virtual bool GetNewDestination(const OutputType type, CTxDestination& dest, bilingual_str& error) { return false; } virtual isminetype IsMine(const CScript& script) const { return ISMINE_NO; } //! Check that the given decryption key is valid for this ScriptPubKeyMan, i.e. it decrypts all of the keys handled by it. virtual bool CheckDecryptionKey(const CKeyingMaterial& master_key, bool accept_no_keys = false) { return false; } virtual bool Encrypt(const CKeyingMaterial& master_key, WalletBatch* batch) { return false; } - virtual bool GetReservedDestination(const OutputType type, bool internal, CTxDestination& address, int64_t& index, CKeyPool& keypool, std::string& error) { return false; } + virtual bool GetReservedDestination(const OutputType type, bool internal, CTxDestination& address, int64_t& index, CKeyPool& keypool, bilingual_str& error) { return false; } virtual void KeepDestination(int64_t index, const OutputType& type) {} virtual void ReturnDestination(int64_t index, bool internal, const CTxDestination& addr) {} @@ -230,7 +230,7 @@ public: virtual bool CanProvide(const CScript& script, SignatureData& sigdata) { return false; } /** Creates new signatures and adds them to the transaction. Returns whether all inputs were signed */ - virtual bool SignTransaction(CMutableTransaction& tx, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, std::string>& input_errors) const { return false; } + virtual bool SignTransaction(CMutableTransaction& tx, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, bilingual_str>& input_errors) const { return false; } /** Sign a message with the given script */ virtual SigningResult SignMessage(const std::string& message, const PKHash& pkhash, std::string& str_sig) const { return SigningResult::SIGNING_FAILED; }; /** Adds script and derivation path information to a PSBT, and optionally signs it. */ @@ -355,13 +355,13 @@ private: public: using ScriptPubKeyMan::ScriptPubKeyMan; - bool GetNewDestination(const OutputType type, CTxDestination& dest, std::string& error) override; + bool GetNewDestination(const OutputType type, CTxDestination& dest, bilingual_str& error) override; isminetype IsMine(const CScript& script) const override; bool CheckDecryptionKey(const CKeyingMaterial& master_key, bool accept_no_keys = false) override; bool Encrypt(const CKeyingMaterial& master_key, WalletBatch* batch) override; - bool GetReservedDestination(const OutputType type, bool internal, CTxDestination& address, int64_t& index, CKeyPool& keypool, std::string& error) override; + bool GetReservedDestination(const OutputType type, bool internal, CTxDestination& address, int64_t& index, CKeyPool& keypool, bilingual_str& error) override; void KeepDestination(int64_t index, const OutputType& type) override; void ReturnDestination(int64_t index, bool internal, const CTxDestination&) override; @@ -396,7 +396,7 @@ public: bool CanProvide(const CScript& script, SignatureData& sigdata) override; - bool SignTransaction(CMutableTransaction& tx, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, std::string>& input_errors) const override; + bool SignTransaction(CMutableTransaction& tx, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, bilingual_str>& input_errors) const override; SigningResult SignMessage(const std::string& message, const PKHash& pkhash, std::string& str_sig) const override; TransactionError FillPSBT(PartiallySignedTransaction& psbt, const PrecomputedTransactionData& txdata, int sighash_type = 1 /* SIGHASH_ALL */, bool sign = true, bool bip32derivs = false, int* n_signed = nullptr) const override; @@ -559,13 +559,13 @@ public: mutable RecursiveMutex cs_desc_man; - bool GetNewDestination(const OutputType type, CTxDestination& dest, std::string& error) override; + bool GetNewDestination(const OutputType type, CTxDestination& dest, bilingual_str& error) override; isminetype IsMine(const CScript& script) const override; bool CheckDecryptionKey(const CKeyingMaterial& master_key, bool accept_no_keys = false) override; bool Encrypt(const CKeyingMaterial& master_key, WalletBatch* batch) override; - bool GetReservedDestination(const OutputType type, bool internal, CTxDestination& address, int64_t& index, CKeyPool& keypool, std::string& error) override; + bool GetReservedDestination(const OutputType type, bool internal, CTxDestination& address, int64_t& index, CKeyPool& keypool, bilingual_str& error) override; void ReturnDestination(int64_t index, bool internal, const CTxDestination& addr) override; // Tops up the descriptor cache and m_map_script_pub_keys. The cache is stored in the wallet file @@ -601,7 +601,7 @@ public: bool CanProvide(const CScript& script, SignatureData& sigdata) override; - bool SignTransaction(CMutableTransaction& tx, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, std::string>& input_errors) const override; + bool SignTransaction(CMutableTransaction& tx, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, bilingual_str>& input_errors) const override; SigningResult SignMessage(const std::string& message, const PKHash& pkhash, std::string& str_sig) const override; TransactionError FillPSBT(PartiallySignedTransaction& psbt, const PrecomputedTransactionData& txdata, int sighash_type = 1 /* SIGHASH_ALL */, bool sign = true, bool bip32derivs = false, int* n_signed = nullptr) const override; @@ -621,7 +621,7 @@ public: const WalletDescriptor GetWalletDescriptor() const EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man); const std::vector<CScript> GetScriptPubKeys() const; - bool GetDescriptorString(std::string& out) const; + bool GetDescriptorString(std::string& out, const bool priv) const; void UpgradeDescriptorCache(); }; diff --git a/src/wallet/spend.cpp b/src/wallet/spend.cpp index 6a8df437ae..3bb7134b24 100644 --- a/src/wallet/spend.cpp +++ b/src/wallet/spend.cpp @@ -618,9 +618,9 @@ bool CWallet::CreateTransactionInternal( // Reserve a new key pair from key pool. If it fails, provide a dummy // destination in case we don't need change. CTxDestination dest; - std::string dest_err; + bilingual_str dest_err; if (!reservedest.GetReservedDestination(dest, true, dest_err)) { - error = strprintf(_("Transaction needs a change address, but we can't generate it. %s"), dest_err); + error = _("Transaction needs a change address, but we can't generate it.") + Untranslated(" ") + dest_err; } scriptChange = GetScriptForDestination(dest); // A valid destination implies a change script (and @@ -778,6 +778,10 @@ bool CWallet::CreateTransactionInternal( fee_needed = coin_selection_params.m_effective_feerate.GetFee(nBytes); } + // The only time that fee_needed should be less than the amount available for fees (in change_and_fee - change_amount) is when + // we are subtracting the fee from the outputs. If this occurs at any other time, it is a bug. + assert(coin_selection_params.m_subtract_fee_outputs || fee_needed <= change_and_fee - change_amount); + // Update nFeeRet in case fee_needed changed due to dropping the change output if (fee_needed <= change_and_fee - change_amount) { nFeeRet = change_and_fee - change_amount; diff --git a/src/wallet/test/coinselector_tests.cpp b/src/wallet/test/coinselector_tests.cpp index c65ebad52f..3488ae3526 100644 --- a/src/wallet/test/coinselector_tests.cpp +++ b/src/wallet/test/coinselector_tests.cpp @@ -7,6 +7,7 @@ #include <primitives/transaction.h> #include <random.h> #include <test/util/setup_common.h> +#include <util/translation.h> #include <wallet/coincontrol.h> #include <wallet/coinselection.h> #include <wallet/test/wallet_test_fixture.h> @@ -66,7 +67,7 @@ static void add_coin(CWallet& wallet, const CAmount& nValue, int nAge = 6*24, bo tx.vout[nInput].nValue = nValue; if (spendable) { CTxDestination dest; - std::string error; + bilingual_str error; const bool destination_ok = wallet.GetNewDestination(OutputType::BECH32, "", dest, error); assert(destination_ok); tx.vout[nInput].scriptPubKey = GetScriptForDestination(dest); diff --git a/src/wallet/test/spend_tests.cpp b/src/wallet/test/spend_tests.cpp index 66e7de4273..8821f680b3 100644 --- a/src/wallet/test/spend_tests.cpp +++ b/src/wallet/test/spend_tests.cpp @@ -54,7 +54,7 @@ BOOST_FIXTURE_TEST_CASE(SubtractFee, TestChain100Setup) // Send full input minus more than the fee amount to recipient, check // leftover input amount is paid to recipient not the miner (to_reduce == // -123). This overpays the recipient instead of overpaying the miner more - // than double the neccesary fee. + // than double the necessary fee. BOOST_CHECK_EQUAL(fee, check_tx(fee + 123)); } diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp index 75a08b6f74..9fcea5826b 100644 --- a/src/wallet/test/wallet_tests.cpp +++ b/src/wallet/test/wallet_tests.cpp @@ -20,6 +20,7 @@ #include <util/translation.h> #include <validation.h> #include <wallet/coincontrol.h> +#include <wallet/context.h> #include <wallet/test/util.h> #include <wallet/test/wallet_test_fixture.h> @@ -30,8 +31,6 @@ RPCHelpMan importmulti(); RPCHelpMan dumpwallet(); RPCHelpMan importwallet(); -extern RecursiveMutex cs_wallets; - // Ensure that fee levels defined in the wallet are at least as high // as the default levels for node policy. static_assert(DEFAULT_TRANSACTION_MINFEE >= DEFAULT_MIN_RELAY_TX_FEE, "wallet minimum fee is smaller than default relay fee"); @@ -39,15 +38,15 @@ static_assert(WALLET_INCREMENTAL_RELAY_FEE >= DEFAULT_INCREMENTAL_RELAY_FEE, "wa BOOST_FIXTURE_TEST_SUITE(wallet_tests, WalletTestingSetup) -static std::shared_ptr<CWallet> TestLoadWallet(interfaces::Chain* chain) +static std::shared_ptr<CWallet> TestLoadWallet(WalletContext& context) { DatabaseOptions options; DatabaseStatus status; bilingual_str error; std::vector<bilingual_str> warnings; auto database = MakeWalletDatabase("", options, status, error); - auto wallet = CWallet::Create(chain, "", std::move(database), options.create_flags, error, warnings); - if (chain) { + auto wallet = CWallet::Create(context, "", std::move(database), options.create_flags, error, warnings); + if (context.chain) { wallet->postInitProcess(); } return wallet; @@ -69,7 +68,7 @@ static CMutableTransaction TestSimpleSpend(const CTransaction& from, uint32_t in keystore.AddKey(key); std::map<COutPoint, Coin> coins; coins[mtx.vin[0].prevout].out = from.vout[index]; - std::map<int, std::string> input_errors; + std::map<int, bilingual_str> input_errors; BOOST_CHECK(SignTransaction(mtx, &keystore, coins, SIGHASH_ALL, input_errors)); return mtx; } @@ -200,7 +199,8 @@ BOOST_FIXTURE_TEST_CASE(importmulti_rescan, TestChain100Setup) std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(m_node.chain.get(), "", CreateDummyWalletDatabase()); wallet->SetupLegacyScriptPubKeyMan(); WITH_LOCK(wallet->cs_wallet, wallet->SetLastBlockProcessed(newTip->nHeight, newTip->GetBlockHash())); - AddWallet(wallet); + WalletContext context; + AddWallet(context, wallet); UniValue keys; keys.setArray(); UniValue key; @@ -218,6 +218,7 @@ BOOST_FIXTURE_TEST_CASE(importmulti_rescan, TestChain100Setup) key.pushKV("internal", UniValue(true)); keys.push_back(key); JSONRPCRequest request; + request.context = &context; request.params.setArray(); request.params.push_back(keys); @@ -231,7 +232,7 @@ BOOST_FIXTURE_TEST_CASE(importmulti_rescan, TestChain100Setup) "downloading and rescanning the relevant blocks (see -reindex and -rescan " "options).\"}},{\"success\":true}]", 0, oldTip->GetBlockTimeMax(), TIMESTAMP_WINDOW)); - RemoveWallet(wallet, std::nullopt); + RemoveWallet(context, wallet, /* load_on_startup= */ std::nullopt); } } @@ -258,6 +259,7 @@ BOOST_FIXTURE_TEST_CASE(importwallet_rescan, TestChain100Setup) // Import key into wallet and call dumpwallet to create backup file. { + WalletContext context; std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(m_node.chain.get(), "", CreateDummyWalletDatabase()); { auto spk_man = wallet->GetOrCreateLegacyScriptPubKeyMan(); @@ -265,15 +267,16 @@ BOOST_FIXTURE_TEST_CASE(importwallet_rescan, TestChain100Setup) spk_man->mapKeyMetadata[coinbaseKey.GetPubKey().GetID()].nCreateTime = KEY_TIME; spk_man->AddKeyPubKey(coinbaseKey, coinbaseKey.GetPubKey()); - AddWallet(wallet); + AddWallet(context, wallet); wallet->SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash()); } JSONRPCRequest request; + request.context = &context; request.params.setArray(); request.params.push_back(backup_file); ::dumpwallet().HandleRequest(request); - RemoveWallet(wallet, std::nullopt); + RemoveWallet(context, wallet, /* load_on_startup= */ std::nullopt); } // Call importwallet RPC and verify all blocks with timestamps >= BLOCK_TIME @@ -283,13 +286,15 @@ BOOST_FIXTURE_TEST_CASE(importwallet_rescan, TestChain100Setup) LOCK(wallet->cs_wallet); wallet->SetupLegacyScriptPubKeyMan(); + WalletContext context; JSONRPCRequest request; + request.context = &context; request.params.setArray(); request.params.push_back(backup_file); - AddWallet(wallet); + AddWallet(context, wallet); wallet->SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash()); ::importwallet().HandleRequest(request); - RemoveWallet(wallet, std::nullopt); + RemoveWallet(context, wallet, /* load_on_startup= */ std::nullopt); BOOST_CHECK_EQUAL(wallet->mapWallet.size(), 3U); BOOST_CHECK_EQUAL(m_coinbase_txns.size(), 103U); @@ -589,7 +594,7 @@ BOOST_FIXTURE_TEST_CASE(wallet_disableprivkeys, TestChain100Setup) wallet->SetWalletFlag(WALLET_FLAG_DISABLE_PRIVATE_KEYS); BOOST_CHECK(!wallet->TopUpKeyPool(1000)); CTxDestination dest; - std::string error; + bilingual_str error; BOOST_CHECK(!wallet->GetNewDestination(OutputType::BECH32, "", dest, error)); } @@ -679,7 +684,9 @@ BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup) { gArgs.ForceSetArg("-unsafesqlitesync", "1"); // Create new wallet with known key and unload it. - auto wallet = TestLoadWallet(m_node.chain.get()); + WalletContext context; + context.chain = m_node.chain.get(); + auto wallet = TestLoadWallet(context); CKey key; key.MakeNewKey(true); AddKey(*wallet, key); @@ -719,7 +726,7 @@ BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup) // Reload wallet and make sure new transactions are detected despite events // being blocked - wallet = TestLoadWallet(m_node.chain.get()); + wallet = TestLoadWallet(context); BOOST_CHECK(rescan_completed); BOOST_CHECK_EQUAL(addtx_count, 2); { @@ -746,20 +753,20 @@ BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup) // deadlock during the sync and simulates a new block notification happening // as soon as possible. addtx_count = 0; - auto handler = HandleLoadWallet([&](std::unique_ptr<interfaces::Wallet> wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet->wallet()->cs_wallet, cs_wallets) { + auto handler = HandleLoadWallet(context, [&](std::unique_ptr<interfaces::Wallet> wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet->wallet()->cs_wallet, context.wallets_mutex) { BOOST_CHECK(rescan_completed); m_coinbase_txns.push_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]); block_tx = TestSimpleSpend(*m_coinbase_txns[2], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey())); m_coinbase_txns.push_back(CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]); mempool_tx = TestSimpleSpend(*m_coinbase_txns[3], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey())); BOOST_CHECK(m_node.chain->broadcastTransaction(MakeTransactionRef(mempool_tx), DEFAULT_TRANSACTION_MAXFEE, false, error)); - LEAVE_CRITICAL_SECTION(cs_wallets); + LEAVE_CRITICAL_SECTION(context.wallets_mutex); LEAVE_CRITICAL_SECTION(wallet->wallet()->cs_wallet); SyncWithValidationInterfaceQueue(); ENTER_CRITICAL_SECTION(wallet->wallet()->cs_wallet); - ENTER_CRITICAL_SECTION(cs_wallets); + ENTER_CRITICAL_SECTION(context.wallets_mutex); }); - wallet = TestLoadWallet(m_node.chain.get()); + wallet = TestLoadWallet(context); BOOST_CHECK_EQUAL(addtx_count, 4); { LOCK(wallet->cs_wallet); @@ -773,7 +780,8 @@ BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup) BOOST_FIXTURE_TEST_CASE(CreateWalletWithoutChain, BasicTestingSetup) { - auto wallet = TestLoadWallet(nullptr); + WalletContext context; + auto wallet = TestLoadWallet(context); BOOST_CHECK(wallet); UnloadWallet(std::move(wallet)); } @@ -781,7 +789,9 @@ BOOST_FIXTURE_TEST_CASE(CreateWalletWithoutChain, BasicTestingSetup) BOOST_FIXTURE_TEST_CASE(ZapSelectTx, TestChain100Setup) { gArgs.ForceSetArg("-unsafesqlitesync", "1"); - auto wallet = TestLoadWallet(m_node.chain.get()); + WalletContext context; + context.chain = m_node.chain.get(); + auto wallet = TestLoadWallet(context); CKey key; key.MakeNewKey(true); AddKey(*wallet, key); diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 9a61ca698d..cf869fac0c 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -33,6 +33,7 @@ #include <util/string.h> #include <util/translation.h> #include <wallet/coincontrol.h> +#include <wallet/context.h> #include <wallet/fees.h> #include <wallet/external_signer_scriptpubkeyman.h> @@ -54,10 +55,6 @@ const std::map<uint64_t,std::string> WALLET_FLAG_CAVEATS{ }, }; -RecursiveMutex cs_wallets; -static std::vector<std::shared_ptr<CWallet>> vpwallets GUARDED_BY(cs_wallets); -static std::list<LoadWalletFn> g_load_wallet_fns GUARDED_BY(cs_wallets); - bool AddWalletSetting(interfaces::Chain& chain, const std::string& wallet_name) { util::SettingsValue setting_value = chain.getRwSetting("wallet"); @@ -94,19 +91,29 @@ static void UpdateWalletSetting(interfaces::Chain& chain, } } -bool AddWallet(const std::shared_ptr<CWallet>& wallet) +/** + * Refresh mempool status so the wallet is in an internally consistent state and + * immediately knows the transaction's status: Whether it can be considered + * trusted and is eligible to be abandoned ... + */ +static void RefreshMempoolStatus(CWalletTx& tx, interfaces::Chain& chain) { - LOCK(cs_wallets); + tx.fInMempool = chain.isInMempool(tx.GetHash()); +} + +bool AddWallet(WalletContext& context, const std::shared_ptr<CWallet>& wallet) +{ + LOCK(context.wallets_mutex); assert(wallet); - std::vector<std::shared_ptr<CWallet>>::const_iterator i = std::find(vpwallets.begin(), vpwallets.end(), wallet); - if (i != vpwallets.end()) return false; - vpwallets.push_back(wallet); + std::vector<std::shared_ptr<CWallet>>::const_iterator i = std::find(context.wallets.begin(), context.wallets.end(), wallet); + if (i != context.wallets.end()) return false; + context.wallets.push_back(wallet); wallet->ConnectScriptPubKeyManNotifiers(); wallet->NotifyCanGetAddressesChanged(); return true; } -bool RemoveWallet(const std::shared_ptr<CWallet>& wallet, std::optional<bool> load_on_start, std::vector<bilingual_str>& warnings) +bool RemoveWallet(WalletContext& context, const std::shared_ptr<CWallet>& wallet, std::optional<bool> load_on_start, std::vector<bilingual_str>& warnings) { assert(wallet); @@ -115,10 +122,10 @@ bool RemoveWallet(const std::shared_ptr<CWallet>& wallet, std::optional<bool> lo // Unregister with the validation interface which also drops shared ponters. wallet->m_chain_notifications_handler.reset(); - LOCK(cs_wallets); - std::vector<std::shared_ptr<CWallet>>::iterator i = std::find(vpwallets.begin(), vpwallets.end(), wallet); - if (i == vpwallets.end()) return false; - vpwallets.erase(i); + LOCK(context.wallets_mutex); + std::vector<std::shared_ptr<CWallet>>::iterator i = std::find(context.wallets.begin(), context.wallets.end(), wallet); + if (i == context.wallets.end()) return false; + context.wallets.erase(i); // Write the wallet setting UpdateWalletSetting(chain, name, load_on_start, warnings); @@ -126,32 +133,32 @@ bool RemoveWallet(const std::shared_ptr<CWallet>& wallet, std::optional<bool> lo return true; } -bool RemoveWallet(const std::shared_ptr<CWallet>& wallet, std::optional<bool> load_on_start) +bool RemoveWallet(WalletContext& context, const std::shared_ptr<CWallet>& wallet, std::optional<bool> load_on_start) { std::vector<bilingual_str> warnings; - return RemoveWallet(wallet, load_on_start, warnings); + return RemoveWallet(context, wallet, load_on_start, warnings); } -std::vector<std::shared_ptr<CWallet>> GetWallets() +std::vector<std::shared_ptr<CWallet>> GetWallets(WalletContext& context) { - LOCK(cs_wallets); - return vpwallets; + LOCK(context.wallets_mutex); + return context.wallets; } -std::shared_ptr<CWallet> GetWallet(const std::string& name) +std::shared_ptr<CWallet> GetWallet(WalletContext& context, const std::string& name) { - LOCK(cs_wallets); - for (const std::shared_ptr<CWallet>& wallet : vpwallets) { + LOCK(context.wallets_mutex); + for (const std::shared_ptr<CWallet>& wallet : context.wallets) { if (wallet->GetName() == name) return wallet; } return nullptr; } -std::unique_ptr<interfaces::Handler> HandleLoadWallet(LoadWalletFn load_wallet) +std::unique_ptr<interfaces::Handler> HandleLoadWallet(WalletContext& context, LoadWalletFn load_wallet) { - LOCK(cs_wallets); - auto it = g_load_wallet_fns.emplace(g_load_wallet_fns.end(), std::move(load_wallet)); - return interfaces::MakeHandler([it] { LOCK(cs_wallets); g_load_wallet_fns.erase(it); }); + LOCK(context.wallets_mutex); + auto it = context.wallet_load_fns.emplace(context.wallet_load_fns.end(), std::move(load_wallet)); + return interfaces::MakeHandler([&context, it] { LOCK(context.wallets_mutex); context.wallet_load_fns.erase(it); }); } static Mutex g_loading_wallet_mutex; @@ -203,7 +210,7 @@ void UnloadWallet(std::shared_ptr<CWallet>&& wallet) } namespace { -std::shared_ptr<CWallet> LoadWalletInternal(interfaces::Chain& chain, const std::string& name, std::optional<bool> load_on_start, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error, std::vector<bilingual_str>& warnings) +std::shared_ptr<CWallet> LoadWalletInternal(WalletContext& context, const std::string& name, std::optional<bool> load_on_start, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error, std::vector<bilingual_str>& warnings) { try { std::unique_ptr<WalletDatabase> database = MakeWalletDatabase(name, options, status, error); @@ -212,18 +219,18 @@ std::shared_ptr<CWallet> LoadWalletInternal(interfaces::Chain& chain, const std: return nullptr; } - chain.initMessage(_("Loading wallet…").translated); - std::shared_ptr<CWallet> wallet = CWallet::Create(&chain, name, std::move(database), options.create_flags, error, warnings); + context.chain->initMessage(_("Loading wallet…").translated); + std::shared_ptr<CWallet> wallet = CWallet::Create(context, name, std::move(database), options.create_flags, error, warnings); if (!wallet) { error = Untranslated("Wallet loading failed.") + Untranslated(" ") + error; status = DatabaseStatus::FAILED_LOAD; return nullptr; } - AddWallet(wallet); + AddWallet(context, wallet); wallet->postInitProcess(); // Write the wallet setting - UpdateWalletSetting(chain, name, load_on_start, warnings); + UpdateWalletSetting(*context.chain, name, load_on_start, warnings); return wallet; } catch (const std::runtime_error& e) { @@ -234,7 +241,7 @@ std::shared_ptr<CWallet> LoadWalletInternal(interfaces::Chain& chain, const std: } } // namespace -std::shared_ptr<CWallet> LoadWallet(interfaces::Chain& chain, const std::string& name, std::optional<bool> load_on_start, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error, std::vector<bilingual_str>& warnings) +std::shared_ptr<CWallet> LoadWallet(WalletContext& context, const std::string& name, std::optional<bool> load_on_start, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error, std::vector<bilingual_str>& warnings) { auto result = WITH_LOCK(g_loading_wallet_mutex, return g_loading_wallet_set.insert(name)); if (!result.second) { @@ -242,12 +249,12 @@ std::shared_ptr<CWallet> LoadWallet(interfaces::Chain& chain, const std::string& status = DatabaseStatus::FAILED_LOAD; return nullptr; } - auto wallet = LoadWalletInternal(chain, name, load_on_start, options, status, error, warnings); + auto wallet = LoadWalletInternal(context, name, load_on_start, options, status, error, warnings); WITH_LOCK(g_loading_wallet_mutex, g_loading_wallet_set.erase(result.first)); return wallet; } -std::shared_ptr<CWallet> CreateWallet(interfaces::Chain& chain, const std::string& name, std::optional<bool> load_on_start, DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error, std::vector<bilingual_str>& warnings) +std::shared_ptr<CWallet> CreateWallet(WalletContext& context, const std::string& name, std::optional<bool> load_on_start, DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error, std::vector<bilingual_str>& warnings) { uint64_t wallet_creation_flags = options.create_flags; const SecureString& passphrase = options.create_passphrase; @@ -292,8 +299,8 @@ std::shared_ptr<CWallet> CreateWallet(interfaces::Chain& chain, const std::strin } // Make the wallet - chain.initMessage(_("Loading wallet…").translated); - std::shared_ptr<CWallet> wallet = CWallet::Create(&chain, name, std::move(database), wallet_creation_flags, error, warnings); + context.chain->initMessage(_("Loading wallet…").translated); + std::shared_ptr<CWallet> wallet = CWallet::Create(context, name, std::move(database), wallet_creation_flags, error, warnings); if (!wallet) { error = Untranslated("Wallet creation failed.") + Untranslated(" ") + error; status = DatabaseStatus::FAILED_CREATE; @@ -335,11 +342,11 @@ std::shared_ptr<CWallet> CreateWallet(interfaces::Chain& chain, const std::strin wallet->Lock(); } } - AddWallet(wallet); + AddWallet(context, wallet); wallet->postInitProcess(); // Write the wallet settings - UpdateWalletSetting(chain, name, load_on_start, warnings); + UpdateWalletSetting(*context.chain, name, load_on_start, warnings); status = DatabaseStatus::SUCCESS; return wallet; @@ -803,10 +810,7 @@ bool CWallet::MarkReplaced(const uint256& originalHash, const uint256& newHash) wtx.mapValue["replaced_by_txid"] = newHash.ToString(); // Refresh mempool status without waiting for transactionRemovedFromMempool - // notification so the wallet is in an internally consistent state and - // immediately knows the old transaction should not be considered trusted - // and is eligible to be abandoned - wtx.fInMempool = chain().isInMempool(originalHash); + RefreshMempoolStatus(wtx, chain()); WalletBatch batch(GetDatabase()); @@ -1206,7 +1210,7 @@ void CWallet::transactionAddedToMempool(const CTransactionRef& tx, uint64_t memp auto it = mapWallet.find(tx->GetHash()); if (it != mapWallet.end()) { - it->second.fInMempool = true; + RefreshMempoolStatus(it->second, chain()); } } @@ -1214,7 +1218,7 @@ void CWallet::transactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRe LOCK(cs_wallet); auto it = mapWallet.find(tx->GetHash()); if (it != mapWallet.end()) { - it->second.fInMempool = false; + RefreshMempoolStatus(it->second, chain()); } // Handle transactions that were removed from the mempool because they // conflict with transactions in a newly connected block. @@ -1795,9 +1799,9 @@ void CWallet::ResendWalletTransactions() /** @} */ // end of mapWallet -void MaybeResendWalletTxs() +void MaybeResendWalletTxs(WalletContext& context) { - for (const std::shared_ptr<CWallet>& pwallet : GetWallets()) { + for (const std::shared_ptr<CWallet>& pwallet : GetWallets(context)) { pwallet->ResendWalletTransactions(); } } @@ -1822,11 +1826,11 @@ bool CWallet::SignTransaction(CMutableTransaction& tx) const const CWalletTx& wtx = mi->second; coins[input.prevout] = Coin(wtx.tx->vout[input.prevout.n], wtx.m_confirm.block_height, wtx.IsCoinBase()); } - std::map<int, std::string> input_errors; + std::map<int, bilingual_str> input_errors; return SignTransaction(tx, coins, SIGHASH_DEFAULT, input_errors); } -bool CWallet::SignTransaction(CMutableTransaction& tx, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, std::string>& input_errors) const +bool CWallet::SignTransaction(CMutableTransaction& tx, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, bilingual_str>& input_errors) const { // Try to sign with all ScriptPubKeyMans for (ScriptPubKeyMan* spk_man : GetAllScriptPubKeyMans()) { @@ -2128,7 +2132,7 @@ bool CWallet::TopUpKeyPool(unsigned int kpSize) return res; } -bool CWallet::GetNewDestination(const OutputType type, const std::string label, CTxDestination& dest, std::string& error) +bool CWallet::GetNewDestination(const OutputType type, const std::string label, CTxDestination& dest, bilingual_str& error) { LOCK(cs_wallet); error.clear(); @@ -2138,7 +2142,7 @@ bool CWallet::GetNewDestination(const OutputType type, const std::string label, spk_man->TopUp(); result = spk_man->GetNewDestination(type, dest, error); } else { - error = strprintf(_("Error: No %s addresses available."), FormatOutputType(type)).translated; + error = strprintf(_("Error: No %s addresses available."), FormatOutputType(type)); } if (result) { SetAddressBook(dest, label, "receive"); @@ -2147,7 +2151,7 @@ bool CWallet::GetNewDestination(const OutputType type, const std::string label, return result; } -bool CWallet::GetNewChangeDestination(const OutputType type, CTxDestination& dest, std::string& error) +bool CWallet::GetNewChangeDestination(const OutputType type, CTxDestination& dest, bilingual_str& error) { LOCK(cs_wallet); error.clear(); @@ -2200,11 +2204,11 @@ std::set<CTxDestination> CWallet::GetLabelAddresses(const std::string& label) co return result; } -bool ReserveDestination::GetReservedDestination(CTxDestination& dest, bool internal, std::string& error) +bool ReserveDestination::GetReservedDestination(CTxDestination& dest, bool internal, bilingual_str& error) { m_spk_man = pwallet->GetScriptPubKeyMan(type, internal); if (!m_spk_man) { - error = strprintf(_("Error: No %s addresses available."), FormatOutputType(type)).translated; + error = strprintf(_("Error: No %s addresses available."), FormatOutputType(type)); return false; } @@ -2502,8 +2506,9 @@ std::unique_ptr<WalletDatabase> MakeWalletDatabase(const std::string& name, cons return MakeDatabase(wallet_path, options, status, error_string); } -std::shared_ptr<CWallet> CWallet::Create(interfaces::Chain* chain, const std::string& name, std::unique_ptr<WalletDatabase> database, uint64_t wallet_creation_flags, bilingual_str& error, std::vector<bilingual_str>& warnings) +std::shared_ptr<CWallet> CWallet::Create(WalletContext& context, const std::string& name, std::unique_ptr<WalletDatabase> database, uint64_t wallet_creation_flags, bilingual_str& error, std::vector<bilingual_str>& warnings) { + interfaces::Chain* chain = context.chain; const std::string& walletFile = database->Filename(); int64_t nStart = GetTimeMillis(); @@ -2586,19 +2591,21 @@ std::shared_ptr<CWallet> CWallet::Create(interfaces::Chain* chain, const std::st } if (!gArgs.GetArg("-addresstype", "").empty()) { - if (!ParseOutputType(gArgs.GetArg("-addresstype", ""), walletInstance->m_default_address_type)) { + std::optional<OutputType> parsed = ParseOutputType(gArgs.GetArg("-addresstype", "")); + if (!parsed) { error = strprintf(_("Unknown address type '%s'"), gArgs.GetArg("-addresstype", "")); return nullptr; } + walletInstance->m_default_address_type = parsed.value(); } if (!gArgs.GetArg("-changetype", "").empty()) { - OutputType out_type; - if (!ParseOutputType(gArgs.GetArg("-changetype", ""), out_type)) { + std::optional<OutputType> parsed = ParseOutputType(gArgs.GetArg("-changetype", "")); + if (!parsed) { error = strprintf(_("Unknown change type '%s'"), gArgs.GetArg("-changetype", "")); return nullptr; } - walletInstance->m_default_change_type = out_type; + walletInstance->m_default_change_type = parsed.value(); } if (gArgs.IsArgSet("-mintxfee")) { @@ -2713,9 +2720,9 @@ std::shared_ptr<CWallet> CWallet::Create(interfaces::Chain* chain, const std::st } { - LOCK(cs_wallets); - for (auto& load_wallet : g_load_wallet_fns) { - load_wallet(interfaces::MakeWallet(walletInstance)); + LOCK(context.wallets_mutex); + for (auto& load_wallet : context.wallet_load_fns) { + load_wallet(interfaces::MakeWallet(context, walletInstance)); } } diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 3997751f52..a1bbf66136 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -42,6 +42,8 @@ #include <boost/signals2/signal.hpp> +struct WalletContext; + using LoadWalletFn = std::function<void(std::unique_ptr<interfaces::Wallet> wallet)>; struct bilingual_str; @@ -53,14 +55,14 @@ struct bilingual_str; //! by the shared pointer deleter. void UnloadWallet(std::shared_ptr<CWallet>&& wallet); -bool AddWallet(const std::shared_ptr<CWallet>& wallet); -bool RemoveWallet(const std::shared_ptr<CWallet>& wallet, std::optional<bool> load_on_start, std::vector<bilingual_str>& warnings); -bool RemoveWallet(const std::shared_ptr<CWallet>& wallet, std::optional<bool> load_on_start); -std::vector<std::shared_ptr<CWallet>> GetWallets(); -std::shared_ptr<CWallet> GetWallet(const std::string& name); -std::shared_ptr<CWallet> LoadWallet(interfaces::Chain& chain, const std::string& name, std::optional<bool> load_on_start, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error, std::vector<bilingual_str>& warnings); -std::shared_ptr<CWallet> CreateWallet(interfaces::Chain& chain, const std::string& name, std::optional<bool> load_on_start, DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error, std::vector<bilingual_str>& warnings); -std::unique_ptr<interfaces::Handler> HandleLoadWallet(LoadWalletFn load_wallet); +bool AddWallet(WalletContext& context, const std::shared_ptr<CWallet>& wallet); +bool RemoveWallet(WalletContext& context, const std::shared_ptr<CWallet>& wallet, std::optional<bool> load_on_start, std::vector<bilingual_str>& warnings); +bool RemoveWallet(WalletContext& context, const std::shared_ptr<CWallet>& wallet, std::optional<bool> load_on_start); +std::vector<std::shared_ptr<CWallet>> GetWallets(WalletContext& context); +std::shared_ptr<CWallet> GetWallet(WalletContext& context, const std::string& name); +std::shared_ptr<CWallet> LoadWallet(WalletContext& context, const std::string& name, std::optional<bool> load_on_start, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error, std::vector<bilingual_str>& warnings); +std::shared_ptr<CWallet> CreateWallet(WalletContext& context, const std::string& name, std::optional<bool> load_on_start, DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error, std::vector<bilingual_str>& warnings); +std::unique_ptr<interfaces::Handler> HandleLoadWallet(WalletContext& context, LoadWalletFn load_wallet); std::unique_ptr<WalletDatabase> MakeWalletDatabase(const std::string& name, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error); //! -paytxfee default @@ -183,7 +185,7 @@ public: } //! Reserve an address - bool GetReservedDestination(CTxDestination& pubkey, bool internal, std::string& error); + bool GetReservedDestination(CTxDestination& pubkey, bool internal, bilingual_str& error); //! Return reserved address void ReturnDestination(); //! Keep the address. Do not return it's key to the keypool when this object goes out of scope @@ -563,7 +565,7 @@ public: /** Fetch the inputs and sign with SIGHASH_ALL. */ bool SignTransaction(CMutableTransaction& tx) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); /** Sign the tx given the input coins and sighash. */ - bool SignTransaction(CMutableTransaction& tx, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, std::string>& input_errors) const; + bool SignTransaction(CMutableTransaction& tx, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, bilingual_str>& input_errors) const; SigningResult SignMessage(const std::string& message, const PKHash& pkhash, std::string& str_sig) const; /** @@ -665,8 +667,8 @@ public: */ void MarkDestinationsDirty(const std::set<CTxDestination>& destinations) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); - bool GetNewDestination(const OutputType type, const std::string label, CTxDestination& dest, std::string& error); - bool GetNewChangeDestination(const OutputType type, CTxDestination& dest, std::string& error); + bool GetNewDestination(const OutputType type, const std::string label, CTxDestination& dest, bilingual_str& error); + bool GetNewChangeDestination(const OutputType type, CTxDestination& dest, bilingual_str& error); isminetype IsMine(const CTxDestination& dest) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); isminetype IsMine(const CScript& script) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); @@ -772,7 +774,7 @@ public: bool MarkReplaced(const uint256& originalHash, const uint256& newHash); /* Initializes the wallet, returns a new CWallet instance or a null pointer in case of an error */ - static std::shared_ptr<CWallet> Create(interfaces::Chain* chain, const std::string& name, std::unique_ptr<WalletDatabase> database, uint64_t wallet_creation_flags, bilingual_str& error, std::vector<bilingual_str>& warnings); + static std::shared_ptr<CWallet> Create(WalletContext& context, const std::string& name, std::unique_ptr<WalletDatabase> database, uint64_t wallet_creation_flags, bilingual_str& error, std::vector<bilingual_str>& warnings); /** * Wallet post-init setup @@ -919,7 +921,7 @@ public: * Called periodically by the schedule thread. Prompts individual wallets to resend * their transactions. Actual rebroadcast schedule is managed by the wallets themselves. */ -void MaybeResendWalletTxs(); +void MaybeResendWalletTxs(WalletContext& context); /** RAII object to check and reserve a wallet rescan */ class WalletRescanReserver diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp index 1e5d8dfa3a..2fabe65a93 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -1004,14 +1004,14 @@ DBErrors WalletBatch::ZapSelectTx(std::vector<uint256>& vTxHashIn, std::vector<u return DBErrors::LOAD_OK; } -void MaybeCompactWalletDB() +void MaybeCompactWalletDB(WalletContext& context) { static std::atomic<bool> fOneThread(false); if (fOneThread.exchange(true)) { return; } - for (const std::shared_ptr<CWallet>& pwallet : GetWallets()) { + for (const std::shared_ptr<CWallet>& pwallet : GetWallets(context)) { WalletDatabase& dbh = pwallet->GetDatabase(); unsigned int nUpdateCounter = dbh.nUpdateCounter; diff --git a/src/wallet/walletdb.h b/src/wallet/walletdb.h index 9b775eb481..25c2ec5909 100644 --- a/src/wallet/walletdb.h +++ b/src/wallet/walletdb.h @@ -31,6 +31,7 @@ static const bool DEFAULT_FLUSHWALLET = true; struct CBlockLocator; +struct WalletContext; class CKeyPool; class CMasterKey; class CScript; @@ -279,7 +280,7 @@ private: }; //! Compacts BDB state so that wallet.dat is self-contained (if there are changes) -void MaybeCompactWalletDB(); +void MaybeCompactWalletDB(WalletContext& context); //! Callback for filtering key types to deserialize in ReadKeyValue using KeyFilterFn = std::function<bool(const std::string&)>; diff --git a/test/functional/feature_asmap.py b/test/functional/feature_asmap.py index 5fcecb4882..704dd6126b 100755 --- a/test/functional/feature_asmap.py +++ b/test/functional/feature_asmap.py @@ -31,8 +31,8 @@ ASMAP = '../../src/test/data/asmap.raw' # path to unit test skeleton asmap VERSION = 'fec61fa21a9f46f3b17bdcd660d7f4cd90b966aad3aec593c99b35f0aca15853' def expected_messages(filename): - return ['Opened asmap file "{}" (59 bytes) from disk'.format(filename), - 'Using asmap version {} for IP bucketing'.format(VERSION)] + return [f'Opened asmap file "{filename}" (59 bytes) from disk', + f'Using asmap version {VERSION} for IP bucketing'] class AsmapTest(BitcoinTestFramework): def set_test_params(self): @@ -50,7 +50,7 @@ class AsmapTest(BitcoinTestFramework): filename = os.path.join(self.datadir, 'my-map-file.map') shutil.copyfile(self.asmap_raw, filename) with self.node.assert_debug_log(expected_messages(filename)): - self.start_node(0, ['-asmap={}'.format(filename)]) + self.start_node(0, [f'-asmap={filename}']) os.remove(filename) def test_asmap_with_relative_path(self): @@ -60,13 +60,13 @@ class AsmapTest(BitcoinTestFramework): filename = os.path.join(self.datadir, name) shutil.copyfile(self.asmap_raw, filename) with self.node.assert_debug_log(expected_messages(filename)): - self.start_node(0, ['-asmap={}'.format(name)]) + self.start_node(0, [f'-asmap={name}']) os.remove(filename) def test_default_asmap(self): shutil.copyfile(self.asmap_raw, self.default_asmap) for arg in ['-asmap', '-asmap=']: - self.log.info('Test bitcoind {} (using default map file)'.format(arg)) + self.log.info(f'Test bitcoind {arg} (using default map file)') self.stop_node(0) with self.node.assert_debug_log(expected_messages(self.default_asmap)): self.start_node(0, [arg]) @@ -75,7 +75,7 @@ class AsmapTest(BitcoinTestFramework): def test_default_asmap_with_missing_file(self): self.log.info('Test bitcoind -asmap with missing default map file') self.stop_node(0) - msg = "Error: Could not find asmap file \"{}\"".format(self.default_asmap) + msg = f"Error: Could not find asmap file \"{self.default_asmap}\"" self.node.assert_start_raises_init_error(extra_args=['-asmap'], expected_msg=msg) def test_empty_asmap(self): @@ -83,7 +83,7 @@ class AsmapTest(BitcoinTestFramework): self.stop_node(0) with open(self.default_asmap, "w", encoding="utf-8") as f: f.write("") - msg = "Error: Could not parse asmap file \"{}\"".format(self.default_asmap) + msg = f"Error: Could not parse asmap file \"{self.default_asmap}\"" self.node.assert_start_raises_init_error(extra_args=['-asmap'], expected_msg=msg) os.remove(self.default_asmap) diff --git a/test/functional/feature_backwards_compatibility.py b/test/functional/feature_backwards_compatibility.py index e0ba835f99..978080703e 100755 --- a/test/functional/feature_backwards_compatibility.py +++ b/test/functional/feature_backwards_compatibility.py @@ -366,7 +366,7 @@ class BackwardsCompatibilityTest(BitcoinTestFramework): assert_equal(load_res['warning'], '') wallet = node_master.get_wallet_rpc("u1_v16") info = wallet.getaddressinfo(v16_addr) - descriptor = "wpkh([" + info["hdmasterfingerprint"] + hdkeypath[1:] + "]" + v16_pubkey + ")" + descriptor = f"wpkh([{info['hdmasterfingerprint']}{hdkeypath[1:]}]{v16_pubkey})" assert_equal(info["desc"], descsum_create(descriptor)) # Now copy that same wallet back to 0.16 to make sure no automatic upgrade breaks it @@ -389,7 +389,7 @@ class BackwardsCompatibilityTest(BitcoinTestFramework): node_master.loadwallet("u1_v17") wallet = node_master.get_wallet_rpc("u1_v17") info = wallet.getaddressinfo(address) - descriptor = "wpkh([" + info["hdmasterfingerprint"] + hdkeypath[1:] + "]" + pubkey + ")" + descriptor = f"wpkh([{info['hdmasterfingerprint']}{hdkeypath[1:]}]{pubkey})" assert_equal(info["desc"], descsum_create(descriptor)) # Now copy that same wallet back to 0.17 to make sure no automatic upgrade breaks it diff --git a/test/functional/feature_blocksdir.py b/test/functional/feature_blocksdir.py index 7bfad52c24..d3276e64ee 100755 --- a/test/functional/feature_blocksdir.py +++ b/test/functional/feature_blocksdir.py @@ -24,10 +24,10 @@ class BlocksdirTest(BitcoinTestFramework): initialize_datadir(self.options.tmpdir, 0, self.chain) self.log.info("Starting with nonexistent blocksdir ...") blocksdir_path = os.path.join(self.options.tmpdir, 'blocksdir') - self.nodes[0].assert_start_raises_init_error(["-blocksdir=" + blocksdir_path], 'Error: Specified blocks directory "{}" does not exist.'.format(blocksdir_path)) + self.nodes[0].assert_start_raises_init_error([f"-blocksdir={blocksdir_path}"], f'Error: Specified blocks directory "{blocksdir_path}" does not exist.') os.mkdir(blocksdir_path) self.log.info("Starting with existing blocksdir ...") - self.start_node(0, ["-blocksdir=" + blocksdir_path]) + self.start_node(0, [f"-blocksdir={blocksdir_path}"]) self.log.info("mining blocks..") self.nodes[0].generatetoaddress(10, self.nodes[0].get_deterministic_priv_key().address) assert os.path.isfile(os.path.join(blocksdir_path, self.chain, "blocks", "blk00000.dat")) diff --git a/test/functional/feature_cltv.py b/test/functional/feature_cltv.py index 7c14f5d5a6..dc5bffe33e 100755 --- a/test/functional/feature_cltv.py +++ b/test/functional/feature_cltv.py @@ -135,7 +135,7 @@ class BIP65Test(BitcoinTestFramework): block.nVersion = 3 block.solve() - with self.nodes[0].assert_debug_log(expected_msgs=['{}, bad-version(0x00000003)'.format(block.hash)]): + with self.nodes[0].assert_debug_log(expected_msgs=[f'{block.hash}, bad-version(0x00000003)']): peer.send_and_ping(msg_block(block)) assert_equal(int(self.nodes[0].getbestblockhash(), 16), tip) peer.sync_with_ping() @@ -173,8 +173,7 @@ class BIP65Test(BitcoinTestFramework): block.hashMerkleRoot = block.calc_merkle_root() block.solve() - with self.nodes[0].assert_debug_log(expected_msgs=['CheckInputScripts on {} failed with {}'.format( - block.vtx[-1].hash, expected_cltv_reject_reason)]): + with self.nodes[0].assert_debug_log(expected_msgs=[f'CheckInputScripts on {block.vtx[-1].hash} failed with {expected_cltv_reject_reason}']): peer.send_and_ping(msg_block(block)) assert_equal(int(self.nodes[0].getbestblockhash(), 16), tip) peer.sync_with_ping() diff --git a/test/functional/feature_config_args.py b/test/functional/feature_config_args.py index 9e99cd549d..0daa0ba3d8 100755 --- a/test/functional/feature_config_args.py +++ b/test/functional/feature_config_args.py @@ -24,7 +24,7 @@ class ConfArgsTest(BitcoinTestFramework): inc_conf_file_path = os.path.join(self.nodes[0].datadir, 'include.conf') with open(os.path.join(self.nodes[0].datadir, 'bitcoin.conf'), 'a', encoding='utf-8') as conf: - conf.write('includeconf={}\n'.format(inc_conf_file_path)) + conf.write(f'includeconf={inc_conf_file_path}\n') self.nodes[0].assert_start_raises_init_error( expected_msg='Error: Error parsing command line arguments: Invalid parameter -dash_cli=1', @@ -43,13 +43,13 @@ class ConfArgsTest(BitcoinTestFramework): if self.is_wallet_compiled(): with open(inc_conf_file_path, 'w', encoding='utf8') as conf: conf.write("wallet=foo\n") - self.nodes[0].assert_start_raises_init_error(expected_msg='Error: Config setting for -wallet only applied on %s network when in [%s] section.' % (self.chain, self.chain)) + self.nodes[0].assert_start_raises_init_error(expected_msg=f'Error: Config setting for -wallet only applied on {self.chain} network when in [{self.chain}] section.') main_conf_file_path = os.path.join(self.options.tmpdir, 'node0', 'bitcoin_main.conf') - util.write_config(main_conf_file_path, n=0, chain='', extra_config='includeconf={}\n'.format(inc_conf_file_path)) + util.write_config(main_conf_file_path, n=0, chain='', extra_config=f'includeconf={inc_conf_file_path}\n') with open(inc_conf_file_path, 'w', encoding='utf-8') as conf: conf.write('acceptnonstdtxn=1\n') - self.nodes[0].assert_start_raises_init_error(extra_args=["-conf={}".format(main_conf_file_path)], expected_msg='Error: acceptnonstdtxn is not currently supported for main chain') + self.nodes[0].assert_start_raises_init_error(extra_args=[f"-conf={main_conf_file_path}"], expected_msg='Error: acceptnonstdtxn is not currently supported for main chain') with open(inc_conf_file_path, 'w', encoding='utf-8') as conf: conf.write('nono\n') @@ -69,14 +69,14 @@ class ConfArgsTest(BitcoinTestFramework): inc_conf_file2_path = os.path.join(self.nodes[0].datadir, 'include2.conf') with open(os.path.join(self.nodes[0].datadir, 'bitcoin.conf'), 'a', encoding='utf-8') as conf: - conf.write('includeconf={}\n'.format(inc_conf_file2_path)) + conf.write(f'includeconf={inc_conf_file2_path}\n') with open(inc_conf_file_path, 'w', encoding='utf-8') as conf: conf.write('testnot.datadir=1\n') with open(inc_conf_file2_path, 'w', encoding='utf-8') as conf: conf.write('[testnet]\n') self.restart_node(0) - self.nodes[0].stop_node(expected_stderr='Warning: ' + inc_conf_file_path + ':1 Section [testnot] is not recognized.' + os.linesep + inc_conf_file2_path + ':1 Section [testnet] is not recognized.') + self.nodes[0].stop_node(expected_stderr=f'Warning: {inc_conf_file_path}:1 Section [testnot] is not recognized.{os.linesep}{inc_conf_file2_path}:1 Section [testnet] is not recognized.') with open(inc_conf_file_path, 'w', encoding='utf-8') as conf: conf.write('') # clear @@ -105,8 +105,8 @@ class ConfArgsTest(BitcoinTestFramework): 'Command-line arg: rpcpassword=****', 'Command-line arg: rpcuser=****', 'Command-line arg: torpassword=****', - 'Config file arg: %s="1"' % self.chain, - 'Config file arg: [%s] server="1"' % self.chain, + f'Config file arg: {self.chain}="1"', + f'Config file arg: [{self.chain}] server="1"', ], unexpected_msgs=[ 'alice:f7efda5c189b999524f151318c0c86$d5b51b3beffbc0', @@ -235,7 +235,7 @@ class ConfArgsTest(BitcoinTestFramework): # Check that using -datadir argument on non-existent directory fails self.nodes[0].datadir = new_data_dir - self.nodes[0].assert_start_raises_init_error(['-datadir=' + new_data_dir], 'Error: Specified data directory "' + new_data_dir + '" does not exist.') + self.nodes[0].assert_start_raises_init_error([f'-datadir={new_data_dir}'], f'Error: Specified data directory "{new_data_dir}" does not exist.') # Check that using non-existent datadir in conf file fails conf_file = os.path.join(default_data_dir, "bitcoin.conf") @@ -243,10 +243,10 @@ class ConfArgsTest(BitcoinTestFramework): # datadir needs to be set before [chain] section conf_file_contents = open(conf_file, encoding='utf8').read() with open(conf_file, 'w', encoding='utf8') as f: - f.write("datadir=" + new_data_dir + "\n") + f.write(f"datadir={new_data_dir}\n") f.write(conf_file_contents) - self.nodes[0].assert_start_raises_init_error(['-conf=' + conf_file], 'Error: Error reading configuration file: specified data directory "' + new_data_dir + '" does not exist.') + self.nodes[0].assert_start_raises_init_error([f'-conf={conf_file}'], f'Error: Error reading configuration file: specified data directory "{new_data_dir}" does not exist.') # Check that an explicitly specified config file that cannot be opened fails none_existent_conf_file = os.path.join(default_data_dir, "none_existent_bitcoin.conf") @@ -254,14 +254,14 @@ class ConfArgsTest(BitcoinTestFramework): # Create the directory and ensure the config file now works os.mkdir(new_data_dir) - self.start_node(0, ['-conf='+conf_file]) + self.start_node(0, [f'-conf={conf_file}']) self.stop_node(0) assert os.path.exists(os.path.join(new_data_dir, self.chain, 'blocks')) # Ensure command line argument overrides datadir in conf os.mkdir(new_data_dir_2) self.nodes[0].datadir = new_data_dir_2 - self.start_node(0, ['-datadir='+new_data_dir_2, '-conf='+conf_file]) + self.start_node(0, [f'-datadir={new_data_dir_2}', f'-conf={conf_file}']) assert os.path.exists(os.path.join(new_data_dir_2, self.chain, 'blocks')) diff --git a/test/functional/feature_csv_activation.py b/test/functional/feature_csv_activation.py index 1ac1a0563f..bc0050d47a 100755 --- a/test/functional/feature_csv_activation.py +++ b/test/functional/feature_csv_activation.py @@ -247,7 +247,7 @@ class BIP68_112_113Test(BitcoinTestFramework): self.send_blocks(test_blocks) assert_equal(self.tipheight, CSV_ACTIVATION_HEIGHT - 2) - self.log.info("Height = {}, CSV not yet active (will activate for block {}, not {})".format(self.tipheight, CSV_ACTIVATION_HEIGHT, CSV_ACTIVATION_HEIGHT - 1)) + self.log.info(f"Height = {self.tipheight}, CSV not yet active (will activate for block {CSV_ACTIVATION_HEIGHT}, not {CSV_ACTIVATION_HEIGHT - 1})") assert not softfork_active(self.nodes[0], 'csv') # Test both version 1 and version 2 transactions for all tests diff --git a/test/functional/feature_dbcrash.py b/test/functional/feature_dbcrash.py index 6d8e5430f8..5bbcd20016 100755 --- a/test/functional/feature_dbcrash.py +++ b/test/functional/feature_dbcrash.py @@ -102,7 +102,7 @@ class ChainstateWriteCrashTest(BitcoinTestFramework): # perhaps we generated a test case that blew up our cache? # TODO: If this happens a lot, we should try to restart without -dbcrashratio # and make sure that recovery happens. - raise AssertionError("Unable to successfully restart node %d in allotted time", node_index) + raise AssertionError(f"Unable to successfully restart node {node_index} in allotted time") def submit_block_catch_error(self, node_index, block): """Try submitting a block to the given node. @@ -114,10 +114,10 @@ class ChainstateWriteCrashTest(BitcoinTestFramework): self.nodes[node_index].submitblock(block) return True except (http.client.CannotSendRequest, http.client.RemoteDisconnected) as e: - self.log.debug("node %d submitblock raised exception: %s", node_index, e) + self.log.debug(f"node {node_index} submitblock raised exception: {e}") return False except OSError as e: - self.log.debug("node %d submitblock raised OSError exception: errno=%s", node_index, e.errno) + self.log.debug(f"node {node_index} submitblock raised OSError exception: errno={e.errno}") if e.errno in [errno.EPIPE, errno.ECONNREFUSED, errno.ECONNRESET]: # The node has likely crashed return False @@ -142,15 +142,15 @@ class ChainstateWriteCrashTest(BitcoinTestFramework): # Deliver each block to each other node for i in range(3): nodei_utxo_hash = None - self.log.debug("Syncing blocks to node %d", i) + self.log.debug(f"Syncing blocks to node {i}") for (block_hash, block) in blocks: # Get the block from node3, and submit to node_i - self.log.debug("submitting block %s", block_hash) + self.log.debug(f"submitting block {block_hash}") if not self.submit_block_catch_error(i, block): # TODO: more carefully check that the crash is due to -dbcrashratio # (change the exit code perhaps, and check that here?) self.wait_for_node_exit(i, timeout=30) - self.log.debug("Restarting node %d after block hash %s", i, block_hash) + self.log.debug(f"Restarting node {i} after block hash {block_hash}") nodei_utxo_hash = self.restart_node(i, block_hash) assert nodei_utxo_hash is not None self.restart_counts[i] += 1 @@ -167,7 +167,7 @@ class ChainstateWriteCrashTest(BitcoinTestFramework): # - we only update the utxo cache after a node restart, since flushing # the cache is a no-op at that point if nodei_utxo_hash is not None: - self.log.debug("Checking txoutsetinfo matches for node %d", i) + self.log.debug(f"Checking txoutsetinfo matches for node {i}") assert_equal(nodei_utxo_hash, node3_utxo_hash) def verify_utxo_hash(self): @@ -218,14 +218,14 @@ class ChainstateWriteCrashTest(BitcoinTestFramework): # Start by creating a lot of utxos on node3 initial_height = self.nodes[3].getblockcount() utxo_list = create_confirmed_utxos(self.nodes[3].getnetworkinfo()['relayfee'], self.nodes[3], 5000) - self.log.info("Prepped %d utxo entries", len(utxo_list)) + self.log.info(f"Prepped {len(utxo_list)} utxo entries") # Sync these blocks with the other nodes block_hashes_to_sync = [] for height in range(initial_height + 1, self.nodes[3].getblockcount() + 1): block_hashes_to_sync.append(self.nodes[3].getblockhash(height)) - self.log.debug("Syncing %d blocks with other nodes", len(block_hashes_to_sync)) + self.log.debug(f"Syncing {len(block_hashes_to_sync)} blocks with other nodes") # Syncing the blocks could cause nodes to crash, so the test begins here. self.sync_node3blocks(block_hashes_to_sync) @@ -235,18 +235,18 @@ class ChainstateWriteCrashTest(BitcoinTestFramework): # each time through the loop, generate a bunch of transactions, # and then either mine a single new block on the tip, or some-sized reorg. for i in range(40): - self.log.info("Iteration %d, generating 2500 transactions %s", i, self.restart_counts) + self.log.info(f"Iteration {i}, generating 2500 transactions {self.restart_counts}") # Generate a bunch of small-ish transactions self.generate_small_transactions(self.nodes[3], 2500, utxo_list) # Pick a random block between current tip, and starting tip current_height = self.nodes[3].getblockcount() random_height = random.randint(starting_tip_height, current_height) - self.log.debug("At height %d, considering height %d", current_height, random_height) + self.log.debug(f"At height {current_height}, considering height {random_height}") if random_height > starting_tip_height: # Randomly reorg from this point with some probability (1/4 for # tip, 1/5 for tip-1, ...) if random.random() < 1.0 / (current_height + 4 - random_height): - self.log.debug("Invalidating block at height %d", random_height) + self.log.debug(f"Invalidating block at height {random_height}") self.nodes[3].invalidateblock(self.nodes[3].getblockhash(random_height)) # Now generate new blocks until we pass the old tip height @@ -258,10 +258,10 @@ class ChainstateWriteCrashTest(BitcoinTestFramework): # new address to avoid mining a block that has just been invalidated address=self.nodes[3].getnewaddress(), )) - self.log.debug("Syncing %d new blocks...", len(block_hashes)) + self.log.debug(f"Syncing {len(block_hashes)} new blocks...") self.sync_node3blocks(block_hashes) utxo_list = self.nodes[3].listunspent() - self.log.debug("Node3 utxo count: %d", len(utxo_list)) + self.log.debug(f"Node3 utxo count: {len(utxo_list)}") # Check that the utxo hashes agree with node3 # Useful side effect: each utxo cache gets flushed here, so that we @@ -269,7 +269,7 @@ class ChainstateWriteCrashTest(BitcoinTestFramework): self.verify_utxo_hash() # Check the test coverage - self.log.info("Restarted nodes: %s; crashes on restart: %d", self.restart_counts, self.crashed_on_restart) + self.log.info(f"Restarted nodes: {self.restart_counts}; crashes on restart: {self.crashed_on_restart}") # If no nodes were restarted, we didn't test anything. assert self.restart_counts != [0, 0, 0] @@ -280,7 +280,7 @@ class ChainstateWriteCrashTest(BitcoinTestFramework): # Warn if any of the nodes escaped restart. for i in range(3): if self.restart_counts[i] == 0: - self.log.warning("Node %d never crashed during utxo flush!", i) + self.log.warning(f"Node {i} never crashed during utxo flush!") if __name__ == "__main__": diff --git a/test/functional/feature_dersig.py b/test/functional/feature_dersig.py index eb027c554a..fd46385cd4 100755 --- a/test/functional/feature_dersig.py +++ b/test/functional/feature_dersig.py @@ -4,10 +4,11 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test BIP66 (DER SIG). -Test that the DERSIG soft-fork activates at (regtest) height 1251. +Test the DERSIG soft-fork activation on regtest. """ from test_framework.blocktools import ( + DERSIG_HEIGHT, create_block, create_coinbase, ) @@ -23,8 +24,6 @@ from test_framework.wallet import ( MiniWalletMode, ) -DERSIG_HEIGHT = 1251 - # A canonical signature consists of: # <30> <total len> <02> <len R> <R> <02> <len S> <S> <hashtype> @@ -90,8 +89,10 @@ class BIP66Test(BitcoinTestFramework): block.rehash() block.solve() + assert_equal(self.nodes[0].getblockcount(), DERSIG_HEIGHT - 2) self.test_dersig_info(is_active=False) # Not active as of current tip and next block does not need to obey rules peer.send_and_ping(msg_block(block)) + assert_equal(self.nodes[0].getblockcount(), DERSIG_HEIGHT - 1) self.test_dersig_info(is_active=True) # Not active as of current tip, but next block must obey rules assert_equal(self.nodes[0].getbestblockhash(), block.hash) @@ -103,7 +104,7 @@ class BIP66Test(BitcoinTestFramework): block.rehash() block.solve() - with self.nodes[0].assert_debug_log(expected_msgs=['{}, bad-version(0x00000002)'.format(block.hash)]): + with self.nodes[0].assert_debug_log(expected_msgs=[f'{block.hash}, bad-version(0x00000002)']): peer.send_and_ping(msg_block(block)) assert_equal(int(self.nodes[0].getbestblockhash(), 16), tip) peer.sync_with_ping() @@ -133,7 +134,7 @@ class BIP66Test(BitcoinTestFramework): block.rehash() block.solve() - with self.nodes[0].assert_debug_log(expected_msgs=['CheckInputScripts on {} failed with non-mandatory-script-verify-flag (Non-canonical DER signature)'.format(block.vtx[-1].hash)]): + with self.nodes[0].assert_debug_log(expected_msgs=[f'CheckInputScripts on {block.vtx[-1].hash} failed with non-mandatory-script-verify-flag (Non-canonical DER signature)']): peer.send_and_ping(msg_block(block)) assert_equal(int(self.nodes[0].getbestblockhash(), 16), tip) peer.sync_with_ping() diff --git a/test/functional/feature_fee_estimation.py b/test/functional/feature_fee_estimation.py index 5322b02414..a8f3e12e07 100755 --- a/test/functional/feature_fee_estimation.py +++ b/test/functional/feature_fee_estimation.py @@ -72,7 +72,7 @@ def small_txpuzzle_randfee(from_node, conflist, unconflist, amount, min_fee, fee total_in += t["amount"] tx.vin.append(CTxIn(COutPoint(int(t["txid"], 16), t["vout"]), b"")) if total_in <= amount + fee: - raise RuntimeError("Insufficient funds: need %d, have %d" % (amount + fee, total_in)) + raise RuntimeError(f"Insufficient funds: need {amount + fee}, have {total_in}") tx.vout.append(CTxOut(int((total_in - amount - fee) * COIN), P2SH_1)) tx.vout.append(CTxOut(int(amount * COIN), P2SH_2)) # These transactions don't need to be signed, but we still have to insert @@ -124,8 +124,7 @@ def check_raw_estimates(node, fees_seen): assert_greater_than(feerate, 0) if feerate + delta < min(fees_seen) or feerate - delta > max(fees_seen): - raise AssertionError("Estimated fee (%f) out of range (%f,%f)" - % (feerate, min(fees_seen), max(fees_seen))) + raise AssertionError(f"Estimated fee ({feerate}) out of range ({min(fees_seen)},{max(fees_seen)})") def check_smart_estimates(node, fees_seen): """Call estimatesmartfee and verify that the estimates meet certain invariants.""" @@ -138,11 +137,9 @@ def check_smart_estimates(node, fees_seen): assert_greater_than(feerate, 0) if feerate + delta < min(fees_seen) or feerate - delta > max(fees_seen): - raise AssertionError("Estimated fee (%f) out of range (%f,%f)" - % (feerate, min(fees_seen), max(fees_seen))) + raise AssertionError(f"Estimated fee ({feerate}) out of range ({min(fees_seen)},{max(fees_seen)})") if feerate - delta > last_feerate: - raise AssertionError("Estimated fee (%f) larger than last fee (%f) for lower number of confirms" - % (feerate, last_feerate)) + raise AssertionError(f"Estimated fee ({feerate}) larger than last fee ({last_feerate}) for lower number of confirms") last_feerate = feerate if i == 0: diff --git a/test/functional/feature_filelock.py b/test/functional/feature_filelock.py index 2798d11b0a..e09107802b 100755 --- a/test/functional/feature_filelock.py +++ b/test/functional/feature_filelock.py @@ -22,11 +22,11 @@ class FilelockTest(BitcoinTestFramework): def run_test(self): datadir = os.path.join(self.nodes[0].datadir, self.chain) - self.log.info("Using datadir {}".format(datadir)) + self.log.info(f"Using datadir {datadir}") self.log.info("Check that we can't start a second bitcoind instance using the same datadir") - expected_msg = "Error: Cannot obtain a lock on data directory {0}. {1} is probably already running.".format(datadir, self.config['environment']['PACKAGE_NAME']) - self.nodes[1].assert_start_raises_init_error(extra_args=['-datadir={}'.format(self.nodes[0].datadir), '-noserver'], expected_msg=expected_msg) + expected_msg = f"Error: Cannot obtain a lock on data directory {datadir}. {self.config['environment']['PACKAGE_NAME']} is probably already running." + self.nodes[1].assert_start_raises_init_error(extra_args=[f'-datadir={self.nodes[0].datadir}', '-noserver'], expected_msg=expected_msg) if self.is_wallet_compiled(): def check_wallet_filelock(descriptors): @@ -38,7 +38,7 @@ class FilelockTest(BitcoinTestFramework): expected_msg = "Error: SQLiteDatabase: Unable to obtain an exclusive lock on the database, is it being used by another bitcoind?" else: expected_msg = "Error: Error initializing wallet database environment" - self.nodes[1].assert_start_raises_init_error(extra_args=['-walletdir={}'.format(wallet_dir), '-wallet=' + wallet_name, '-noserver'], expected_msg=expected_msg, match=ErrorMatch.PARTIAL_REGEX) + self.nodes[1].assert_start_raises_init_error(extra_args=[f'-walletdir={wallet_dir}', f'-wallet={wallet_name}', '-noserver'], expected_msg=expected_msg, match=ErrorMatch.PARTIAL_REGEX) if self.is_bdb_compiled(): check_wallet_filelock(False) diff --git a/test/functional/feature_help.py b/test/functional/feature_help.py index babe9bfc80..837e95c128 100755 --- a/test/functional/feature_help.py +++ b/test/functional/feature_help.py @@ -40,14 +40,14 @@ class HelpTest(BitcoinTestFramework): # Node should exit immediately and output help to stdout. output, _ = self.get_node_output(ret_code_expected=0) assert b'Options' in output - self.log.info("Help text received: {} (...)".format(output[0:60])) + self.log.info(f"Help text received: {output[0:60]} (...)") self.log.info("Start bitcoin with -version for version information") self.nodes[0].start(extra_args=['-version']) # Node should exit immediately and output version to stdout. output, _ = self.get_node_output(ret_code_expected=0) assert b'version' in output - self.log.info("Version text received: {} (...)".format(output[0:60])) + self.log.info(f"Version text received: {output[0:60]} (...)") # Test that arguments not in the help results in an error self.log.info("Start bitcoind with -fakearg to make sure it does not start") @@ -55,7 +55,7 @@ class HelpTest(BitcoinTestFramework): # Node should exit immediately and output an error to stderr _, output = self.get_node_output(ret_code_expected=1) assert b'Error parsing command line arguments' in output - self.log.info("Error message received: {} (...)".format(output[0:60])) + self.log.info(f"Error message received: {output[0:60]} (...)") if __name__ == '__main__': diff --git a/test/functional/feature_loadblock.py b/test/functional/feature_loadblock.py index 14f64d63a2..e5db6de085 100755 --- a/test/functional/feature_loadblock.py +++ b/test/functional/feature_loadblock.py @@ -45,17 +45,17 @@ class LoadblockTest(BitcoinTestFramework): self.log.info("Create linearization config file") with open(cfg_file, "a", encoding="utf-8") as cfg: - cfg.write("datadir={}\n".format(data_dir)) - cfg.write("rpcuser={}\n".format(node_url.username)) - cfg.write("rpcpassword={}\n".format(node_url.password)) - cfg.write("port={}\n".format(node_url.port)) - cfg.write("host={}\n".format(node_url.hostname)) - cfg.write("output_file={}\n".format(bootstrap_file)) - cfg.write("max_height=100\n") - cfg.write("netmagic=fabfb5da\n") - cfg.write("input={}\n".format(blocks_dir)) - cfg.write("genesis={}\n".format(genesis_block)) - cfg.write("hashlist={}\n".format(hash_list.name)) + cfg.write(f"datadir={data_dir}\n") + cfg.write(f"rpcuser={node_url.username}\n") + cfg.write(f"rpcpassword={node_url.password}\n") + cfg.write(f"port={node_url.port}\n") + cfg.write(f"host={node_url.hostname}\n") + cfg.write(f"output_file={bootstrap_file}\n") + cfg.write(f"max_height=100\n") + cfg.write(f"netmagic=fabfb5da\n") + cfg.write(f"input={blocks_dir}\n") + cfg.write(f"genesis={genesis_block}\n") + cfg.write(f"hashlist={hash_list.name}\n") base_dir = self.config["environment"]["SRCDIR"] linearize_dir = os.path.join(base_dir, "contrib", "linearize") @@ -72,7 +72,7 @@ class LoadblockTest(BitcoinTestFramework): check=True) self.log.info("Restart second, unsynced node with bootstrap file") - self.restart_node(1, extra_args=["-loadblock=" + bootstrap_file]) + self.restart_node(1, extra_args=[f"-loadblock={bootstrap_file}"]) assert_equal(self.nodes[1].getblockcount(), 100) # start_node is blocking on all block files being imported assert_equal(self.nodes[1].getblockchaininfo()['blocks'], 100) diff --git a/test/functional/feature_logging.py b/test/functional/feature_logging.py index afcbcf099a..722219518a 100755 --- a/test/functional/feature_logging.py +++ b/test/functional/feature_logging.py @@ -29,7 +29,7 @@ class LoggingTest(BitcoinTestFramework): # test alternative log file name outside datadir tempname = os.path.join(self.options.tmpdir, "foo.log") - self.restart_node(0, ["-debuglogfile=%s" % tempname]) + self.restart_node(0, [f"-debuglogfile={tempname}"]) assert os.path.isfile(tempname) # check that invalid log (relative) will cause error @@ -37,26 +37,26 @@ class LoggingTest(BitcoinTestFramework): invalidname = os.path.join("foo", "foo.log") self.stop_node(0) exp_stderr = r"Error: Could not open debug log file \S+$" - self.nodes[0].assert_start_raises_init_error(["-debuglogfile=%s" % (invalidname)], exp_stderr, match=ErrorMatch.FULL_REGEX) + self.nodes[0].assert_start_raises_init_error([f"-debuglogfile={invalidname}"], exp_stderr, match=ErrorMatch.FULL_REGEX) assert not os.path.isfile(os.path.join(invdir, "foo.log")) # check that invalid log (relative) works after path exists self.stop_node(0) os.mkdir(invdir) - self.start_node(0, ["-debuglogfile=%s" % (invalidname)]) + self.start_node(0, [f"-debuglogfile={invalidname}"]) assert os.path.isfile(os.path.join(invdir, "foo.log")) # check that invalid log (absolute) will cause error self.stop_node(0) invdir = os.path.join(self.options.tmpdir, "foo") invalidname = os.path.join(invdir, "foo.log") - self.nodes[0].assert_start_raises_init_error(["-debuglogfile=%s" % invalidname], exp_stderr, match=ErrorMatch.FULL_REGEX) + self.nodes[0].assert_start_raises_init_error([f"-debuglogfile={invalidname}"], exp_stderr, match=ErrorMatch.FULL_REGEX) assert not os.path.isfile(os.path.join(invdir, "foo.log")) # check that invalid log (absolute) works after path exists self.stop_node(0) os.mkdir(invdir) - self.start_node(0, ["-debuglogfile=%s" % (invalidname)]) + self.start_node(0, [f"-debuglogfile={invalidname}"]) assert os.path.isfile(os.path.join(invdir, "foo.log")) # check that -nodebuglogfile disables logging @@ -67,7 +67,7 @@ class LoggingTest(BitcoinTestFramework): assert not os.path.isfile(default_log_path) # just sanity check no crash here - self.restart_node(0, ["-debuglogfile=%s" % os.devnull]) + self.restart_node(0, [f"-debuglogfile={os.devnull}"]) if __name__ == '__main__': diff --git a/test/functional/feature_minchainwork.py b/test/functional/feature_minchainwork.py index 803feb509a..3950690cf0 100755 --- a/test/functional/feature_minchainwork.py +++ b/test/functional/feature_minchainwork.py @@ -45,16 +45,16 @@ class MinimumChainWorkTest(BitcoinTestFramework): # Start building a chain on node0. node2 shouldn't be able to sync until node1's # minchainwork is exceeded starting_chain_work = REGTEST_WORK_PER_BLOCK # Genesis block's work - self.log.info("Testing relay across node %d (minChainWork = %d)", 1, self.node_min_work[1]) + self.log.info(f"Testing relay across node 1 (minChainWork = {self.node_min_work[1]})") starting_blockcount = self.nodes[2].getblockcount() num_blocks_to_generate = int((self.node_min_work[1] - starting_chain_work) / REGTEST_WORK_PER_BLOCK) - self.log.info("Generating %d blocks on node0", num_blocks_to_generate) + self.log.info(f"Generating {num_blocks_to_generate} blocks on node0") hashes = self.nodes[0].generatetoaddress(num_blocks_to_generate, self.nodes[0].get_deterministic_priv_key().address) - self.log.info("Node0 current chain work: %s", self.nodes[0].getblockheader(hashes[-1])['chainwork']) + self.log.info(f"Node0 current chain work: {self.nodes[0].getblockheader(hashes[-1])['chainwork']}") # Sleep a few seconds and verify that node2 didn't get any new blocks # or headers. We sleep, rather than sync_blocks(node0, node1) because @@ -63,7 +63,7 @@ class MinimumChainWorkTest(BitcoinTestFramework): time.sleep(3) self.log.info("Verifying node 2 has no more blocks than before") - self.log.info("Blockcounts: %s", [n.getblockcount() for n in self.nodes]) + self.log.info(f"Blockcounts: {[n.getblockcount() for n in self.nodes]}") # Node2 shouldn't have any new headers yet, because node1 should not # have relayed anything. assert_equal(len(self.nodes[2].getchaintips()), 1) @@ -84,7 +84,7 @@ class MinimumChainWorkTest(BitcoinTestFramework): # continue the test. self.sync_all() - self.log.info("Blockcounts: %s", [n.getblockcount() for n in self.nodes]) + self.log.info(f"Blockcounts: {[n.getblockcount() for n in self.nodes]}") if __name__ == '__main__': MinimumChainWorkTest().main() diff --git a/test/functional/feature_notifications.py b/test/functional/feature_notifications.py index 6fc8773ee3..3f8fe9ccd5 100755 --- a/test/functional/feature_notifications.py +++ b/test/functional/feature_notifications.py @@ -20,7 +20,7 @@ FILE_CHARS_DISALLOWED = '/\\?%*:|"<>' if os.name == 'nt' else '/' UNCONFIRMED_HASH_STRING = 'unconfirmed' def notify_outputname(walletname, txid): - return txid if os.name == 'nt' else '{}_{}'.format(walletname, txid) + return txid if os.name == 'nt' else f'{walletname}_{txid}' class NotificationsTest(BitcoinTestFramework): @@ -39,11 +39,11 @@ class NotificationsTest(BitcoinTestFramework): # -alertnotify and -blocknotify on node0, walletnotify on node1 self.extra_args = [[ - "-alertnotify=echo > {}".format(os.path.join(self.alertnotify_dir, '%s')), - "-blocknotify=echo > {}".format(os.path.join(self.blocknotify_dir, '%s')), + f"-alertnotify=echo > {os.path.join(self.alertnotify_dir, '%s')}", + f"-blocknotify=echo > {os.path.join(self.blocknotify_dir, '%s')}", ], [ "-rescan", - "-walletnotify=echo %h_%b > {}".format(os.path.join(self.walletnotify_dir, notify_outputname('%w', '%s'))), + f"-walletnotify=echo %h_%b > {os.path.join(self.walletnotify_dir, notify_outputname('%w', '%s'))}", ]] self.wallet_names = [self.default_wallet_name, self.wallet] super().setup_network() @@ -54,12 +54,12 @@ class NotificationsTest(BitcoinTestFramework): seed = "cTdGmKFWpbvpKQ7ejrdzqYT2hhjyb3GPHnLAK7wdi5Em67YLwSm9" xpriv = "tprv8ZgxMBicQKsPfHCsTwkiM1KT56RXbGGTqvc2hgqzycpwbHqqpcajQeMRZoBD35kW4RtyCemu6j34Ku5DEspmgjKdt2qe4SvRch5Kk8B8A2v" desc_imports = [{ - "desc": descsum_create("wpkh(" + xpriv + "/0/*)"), + "desc": descsum_create(f"wpkh({xpriv}/0/*)"), "timestamp": 0, "active": True, "keypool": True, },{ - "desc": descsum_create("wpkh(" + xpriv + "/1/*)"), + "desc": descsum_create(f"wpkh({xpriv}/1/*)"), "timestamp": 0, "active": True, "keypool": True, diff --git a/test/functional/feature_proxy.py b/test/functional/feature_proxy.py index 162814815e..2fb5e328f5 100755 --- a/test/functional/feature_proxy.py +++ b/test/functional/feature_proxy.py @@ -97,14 +97,14 @@ class ProxyTest(BitcoinTestFramework): # Note: proxies are not used to connect to local nodes. This is because the proxy to # use is based on CService.GetNetwork(), which returns NET_UNROUTABLE for localhost. args = [ - ['-listen', '-proxy=%s:%i' % (self.conf1.addr),'-proxyrandomize=1'], - ['-listen', '-proxy=%s:%i' % (self.conf1.addr),'-onion=%s:%i' % (self.conf2.addr), - '-i2psam=%s:%i' % (self.i2p_sam), '-i2pacceptincoming=0', '-proxyrandomize=0'], - ['-listen', '-proxy=%s:%i' % (self.conf2.addr),'-proxyrandomize=1'], + ['-listen', f'-proxy={self.conf1.addr[0]}:{self.conf1.addr[1]}','-proxyrandomize=1'], + ['-listen', f'-proxy={self.conf1.addr[0]}:{self.conf1.addr[1]}',f'-onion={self.conf2.addr[0]}:{self.conf2.addr[1]}', + f'-i2psam={self.i2p_sam[0]}:{self.i2p_sam[1]}', '-i2pacceptincoming=0', '-proxyrandomize=0'], + ['-listen', f'-proxy={self.conf2.addr[0]}:{self.conf2.addr[1]}','-proxyrandomize=1'], [] ] if self.have_ipv6: - args[3] = ['-listen', '-proxy=[%s]:%i' % (self.conf3.addr),'-proxyrandomize=0', '-noonion'] + args[3] = ['-listen', f'-proxy=[{self.conf3.addr[0]}]:{self.conf3.addr[1]}','-proxyrandomize=0', '-noonion'] self.add_nodes(self.num_nodes, extra_args=args) self.start_nodes() @@ -116,7 +116,7 @@ class ProxyTest(BitcoinTestFramework): def node_test(self, node, proxies, auth, test_onion=True): rv = [] addr = "15.61.23.23:1234" - self.log.debug("Test: outgoing IPv4 connection through node for address {}".format(addr)) + self.log.debug(f"Test: outgoing IPv4 connection through node for address {addr}") node.addnode(addr, "onetry") cmd = proxies[0].queue.get() assert isinstance(cmd, Socks5Command) @@ -132,7 +132,7 @@ class ProxyTest(BitcoinTestFramework): if self.have_ipv6: addr = "[1233:3432:2434:2343:3234:2345:6546:4534]:5443" - self.log.debug("Test: outgoing IPv6 connection through node for address {}".format(addr)) + self.log.debug(f"Test: outgoing IPv6 connection through node for address {addr}") node.addnode(addr, "onetry") cmd = proxies[1].queue.get() assert isinstance(cmd, Socks5Command) @@ -148,7 +148,7 @@ class ProxyTest(BitcoinTestFramework): if test_onion: addr = "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion:8333" - self.log.debug("Test: outgoing onion connection through node for address {}".format(addr)) + self.log.debug(f"Test: outgoing onion connection through node for address {addr}") node.addnode(addr, "onetry") cmd = proxies[2].queue.get() assert isinstance(cmd, Socks5Command) @@ -162,7 +162,7 @@ class ProxyTest(BitcoinTestFramework): self.network_test(node, addr, network=NET_ONION) addr = "node.noumenon:8333" - self.log.debug("Test: outgoing DNS name connection through node for address {}".format(addr)) + self.log.debug(f"Test: outgoing DNS name connection through node for address {addr}") node.addnode(addr, "onetry") cmd = proxies[3].queue.get() assert isinstance(cmd, Socks5Command) @@ -218,12 +218,12 @@ class ProxyTest(BitcoinTestFramework): n1 = networks_dict(self.nodes[1].getnetworkinfo()) assert_equal(NETWORKS, n1.keys()) for net in ['ipv4', 'ipv6']: - assert_equal(n1[net]['proxy'], '%s:%i' % (self.conf1.addr)) + assert_equal(n1[net]['proxy'], f'{self.conf1.addr[0]}:{self.conf1.addr[1]}') assert_equal(n1[net]['proxy_randomize_credentials'], False) - assert_equal(n1['onion']['proxy'], '%s:%i' % (self.conf2.addr)) + assert_equal(n1['onion']['proxy'], f'{self.conf2.addr[0]}:{self.conf2.addr[1]}') assert_equal(n1['onion']['proxy_randomize_credentials'], False) assert_equal(n1['onion']['reachable'], True) - assert_equal(n1['i2p']['proxy'], '%s:%i' % (self.i2p_sam)) + assert_equal(n1['i2p']['proxy'], f'{self.i2p_sam[0]}:{self.i2p_sam[1]}') assert_equal(n1['i2p']['proxy_randomize_credentials'], False) assert_equal(n1['i2p']['reachable'], True) @@ -234,7 +234,7 @@ class ProxyTest(BitcoinTestFramework): expected_proxy = '' expected_randomize = False else: - expected_proxy = '%s:%i' % (self.conf2.addr) + expected_proxy = f'{self.conf2.addr[0]}:{self.conf2.addr[1]}' expected_randomize = True assert_equal(n2[net]['proxy'], expected_proxy) assert_equal(n2[net]['proxy_randomize_credentials'], expected_randomize) @@ -248,7 +248,7 @@ class ProxyTest(BitcoinTestFramework): if net == NET_I2P: expected_proxy = '' else: - expected_proxy = '[%s]:%i' % (self.conf3.addr) + expected_proxy = f'[{self.conf3.addr[0]}]:{self.conf3.addr[1]}' assert_equal(n3[net]['proxy'], expected_proxy) assert_equal(n3[net]['proxy_randomize_credentials'], False) assert_equal(n3['onion']['reachable'], False) diff --git a/test/functional/feature_pruning.py b/test/functional/feature_pruning.py index cedb7b57ca..2f0868e733 100755 --- a/test/functional/feature_pruning.py +++ b/test/functional/feature_pruning.py @@ -127,10 +127,28 @@ class PruneTest(BitcoinTestFramework): self.sync_blocks(self.nodes[0:5]) + def test_invalid_command_line_options(self): + self.nodes[0].assert_start_raises_init_error( + expected_msg='Error: Prune cannot be configured with a negative value.', + extra_args=['-prune=-1'], + ) + self.nodes[0].assert_start_raises_init_error( + expected_msg='Error: Prune configured below the minimum of 550 MiB. Please use a higher number.', + extra_args=['-prune=549'], + ) + self.nodes[0].assert_start_raises_init_error( + expected_msg='Error: Prune mode is incompatible with -txindex.', + extra_args=['-prune=550', '-txindex'], + ) + self.nodes[0].assert_start_raises_init_error( + expected_msg='Error: Prune mode is incompatible with -coinstatsindex.', + extra_args=['-prune=550', '-coinstatsindex'], + ) + def test_height_min(self): assert os.path.isfile(os.path.join(self.prunedir, "blk00000.dat")), "blk00000.dat is missing, pruning too early" self.log.info("Success") - self.log.info("Though we're already using more than 550MiB, current usage: %d" % calc_usage(self.prunedir)) + self.log.info(f"Though we're already using more than 550MiB, current usage: {calc_usage(self.prunedir)}") self.log.info("Mining 25 more blocks should cause the first block file to be pruned") # Pruning doesn't run until we're allocating another chunk, 20 full blocks past the height cutoff will ensure this mine_large_blocks(self.nodes[0], 25) @@ -140,7 +158,7 @@ class PruneTest(BitcoinTestFramework): self.log.info("Success") usage = calc_usage(self.prunedir) - self.log.info("Usage should be below target: %d" % usage) + self.log.info(f"Usage should be below target: {usage}") assert_greater_than(550, usage) def create_chain_with_staleblocks(self): @@ -163,18 +181,18 @@ class PruneTest(BitcoinTestFramework): self.connect_nodes(0, 2) self.sync_blocks(self.nodes[0:3]) - self.log.info("Usage can be over target because of high stale rate: %d" % calc_usage(self.prunedir)) + self.log.info(f"Usage can be over target because of high stale rate: {calc_usage(self.prunedir)}") def reorg_test(self): # Node 1 will mine a 300 block chain starting 287 blocks back from Node 0 and Node 2's tip # This will cause Node 2 to do a reorg requiring 288 blocks of undo data to the reorg_test chain height = self.nodes[1].getblockcount() - self.log.info("Current block height: %d" % height) + self.log.info(f"Current block height: {height}") self.forkheight = height - 287 self.forkhash = self.nodes[1].getblockhash(self.forkheight) - self.log.info("Invalidating block %s at height %d" % (self.forkhash, self.forkheight)) + self.log.info(f"Invalidating block {self.forkhash} at height {self.forkheight}") self.nodes[1].invalidateblock(self.forkhash) # We've now switched to our previously mined-24 block fork on node 1, but that's not what we want @@ -186,7 +204,7 @@ class PruneTest(BitcoinTestFramework): curhash = self.nodes[1].getblockhash(self.forkheight - 1) assert self.nodes[1].getblockcount() == self.forkheight - 1 - self.log.info("New best height: %d" % self.nodes[1].getblockcount()) + self.log.info(f"New best height: {self.nodes[1].getblockcount()}") # Disconnect node1 and generate the new chain self.disconnect_nodes(0, 1) @@ -200,8 +218,8 @@ class PruneTest(BitcoinTestFramework): self.connect_nodes(1, 2) self.sync_blocks(self.nodes[0:3], timeout=120) - self.log.info("Verify height on node 2: %d" % self.nodes[2].getblockcount()) - self.log.info("Usage possibly still high because of stale blocks in block files: %d" % calc_usage(self.prunedir)) + self.log.info(f"Verify height on node 2: {self.nodes[2].getblockcount()}") + self.log.info(f"Usage possibly still high because of stale blocks in block files: {calc_usage(self.prunedir)}") self.log.info("Mine 220 more large blocks so we have requisite history") @@ -209,7 +227,7 @@ class PruneTest(BitcoinTestFramework): self.sync_blocks(self.nodes[0:3], timeout=120) usage = calc_usage(self.prunedir) - self.log.info("Usage should be below target: %d" % usage) + self.log.info(f"Usage should be below target: {usage}") assert_greater_than(550, usage) def reorg_back(self): @@ -217,7 +235,7 @@ class PruneTest(BitcoinTestFramework): assert_raises_rpc_error(-1, "Block not available (pruned data)", self.nodes[2].getblock, self.forkhash) with self.nodes[2].assert_debug_log(expected_msgs=['block verification stopping at height', '(pruning, no data)']): self.nodes[2].verifychain(checklevel=4, nblocks=0) - self.log.info("Will need to redownload block %d" % self.forkheight) + self.log.info(f"Will need to redownload block {self.forkheight}") # Verify that we have enough history to reorg back to the fork point # Although this is more than 288 blocks, because this chain was written more recently @@ -241,7 +259,7 @@ class PruneTest(BitcoinTestFramework): # At this point node 2 is within 288 blocks of the fork point so it will preserve its ability to reorg if self.nodes[2].getblockcount() < self.mainchainheight: blocks_to_mine = first_reorg_height + 1 - self.mainchainheight - self.log.info("Rewind node 0 to prev main chain to mine longer chain to trigger redownload. Blocks needed: %d" % blocks_to_mine) + self.log.info(f"Rewind node 0 to prev main chain to mine longer chain to trigger redownload. Blocks needed: {blocks_to_mine}") self.nodes[0].invalidateblock(curchainhash) assert_equal(self.nodes[0].getblockcount(), self.mainchainheight) assert_equal(self.nodes[0].getbestblockhash(), self.mainchainhash2) @@ -278,7 +296,7 @@ class PruneTest(BitcoinTestFramework): assert_equal(ret, node.getblockchaininfo()['pruneheight']) def has_block(index): - return os.path.isfile(os.path.join(self.nodes[node_number].datadir, self.chain, "blocks", "blk{:05}.dat".format(index))) + return os.path.isfile(os.path.join(self.nodes[node_number].datadir, self.chain, "blocks", f"blk{index:05}.dat")) # should not prune because chain tip of node 3 (995) < PruneAfterHeight (1000) assert_raises_rpc_error(-1, "Blockchain is too short for pruning", node.pruneblockchain, height(500)) @@ -453,6 +471,9 @@ class PruneTest(BitcoinTestFramework): self.log.info("Test wallet re-scan") self.wallet_test() + self.log.info("Test invalid pruning command line options") + self.test_invalid_command_line_options() + self.log.info("Done") if __name__ == '__main__': diff --git a/test/functional/feature_segwit.py b/test/functional/feature_segwit.py index cbd8521499..6f1cecce58 100755 --- a/test/functional/feature_segwit.py +++ b/test/functional/feature_segwit.py @@ -65,7 +65,7 @@ def find_spendable_utxo(node, min_value): if utxo['spendable']: return utxo - raise AssertionError("Unspent output equal or higher than %s not found" % min_value) + raise AssertionError(f"Unspent output equal or higher than {min_value} not found") txs_mined = {} # txindex from txid to blockhash diff --git a/test/functional/feature_settings.py b/test/functional/feature_settings.py index 5a0236401d..26048d37f6 100755 --- a/test/functional/feature_settings.py +++ b/test/functional/feature_settings.py @@ -83,7 +83,7 @@ class SettingsTest(BitcoinTestFramework): with altsettings.open("w") as fp: fp.write('{"key": "value"}') with node.assert_debug_log(expected_msgs=['Setting file arg: key = "value"']): - self.start_node(0, extra_args=["-settings={}".format(altsettings)]) + self.start_node(0, extra_args=[f"-settings={altsettings}"]) self.stop_node(0) diff --git a/test/functional/feature_versionbits_warning.py b/test/functional/feature_versionbits_warning.py index 1c9e237d78..a7ec5c48e3 100755 --- a/test/functional/feature_versionbits_warning.py +++ b/test/functional/feature_versionbits_warning.py @@ -21,7 +21,7 @@ VB_TOP_BITS = 0x20000000 VB_UNKNOWN_BIT = 27 # Choose a bit unassigned to any deployment VB_UNKNOWN_VERSION = VB_TOP_BITS | (1 << VB_UNKNOWN_BIT) -WARN_UNKNOWN_RULES_ACTIVE = "Unknown new rules activated (versionbit {})".format(VB_UNKNOWN_BIT) +WARN_UNKNOWN_RULES_ACTIVE = f"Unknown new rules activated (versionbit {VB_UNKNOWN_BIT})" VB_PATTERN = re.compile("Unknown new rules activated.*versionbit") class VersionBitsWarningTest(BitcoinTestFramework): @@ -34,7 +34,7 @@ class VersionBitsWarningTest(BitcoinTestFramework): # Open and close to create zero-length file with open(self.alert_filename, 'w', encoding='utf8'): pass - self.extra_args = [["-alertnotify=echo %s >> \"" + self.alert_filename + "\""]] + self.extra_args = [[f"-alertnotify=echo %s >> \"{self.alert_filename}\""]] self.setup_nodes() def send_blocks_with_version(self, peer, numblocks, version): diff --git a/test/functional/interface_bitcoin_cli.py b/test/functional/interface_bitcoin_cli.py index dfa448a1a8..4b5363ec49 100755 --- a/test/functional/interface_bitcoin_cli.py +++ b/test/functional/interface_bitcoin_cli.py @@ -87,12 +87,12 @@ class TestBitcoinCli(BitcoinTestFramework): user, password = get_auth_cookie(self.nodes[0].datadir, self.chain) self.log.info("Test -stdinrpcpass option") - assert_equal(BLOCKS, self.nodes[0].cli('-rpcuser={}'.format(user), '-stdinrpcpass', input=password).getblockcount()) - assert_raises_process_error(1, 'Incorrect rpcuser or rpcpassword', self.nodes[0].cli('-rpcuser={}'.format(user), '-stdinrpcpass', input='foo').echo) + assert_equal(BLOCKS, self.nodes[0].cli(f'-rpcuser={user}', '-stdinrpcpass', input=password).getblockcount()) + assert_raises_process_error(1, 'Incorrect rpcuser or rpcpassword', self.nodes[0].cli(f'-rpcuser={user}', '-stdinrpcpass', input='foo').echo) self.log.info("Test -stdin and -stdinrpcpass") - assert_equal(['foo', 'bar'], self.nodes[0].cli('-rpcuser={}'.format(user), '-stdin', '-stdinrpcpass', input=password + '\nfoo\nbar').echo()) - assert_raises_process_error(1, 'Incorrect rpcuser or rpcpassword', self.nodes[0].cli('-rpcuser={}'.format(user), '-stdin', '-stdinrpcpass', input='foo').echo) + assert_equal(['foo', 'bar'], self.nodes[0].cli(f'-rpcuser={user}', '-stdin', '-stdinrpcpass', input=f'{password}\nfoo\nbar').echo()) + assert_raises_process_error(1, 'Incorrect rpcuser or rpcpassword', self.nodes[0].cli(f'-rpcuser={user}', '-stdin', '-stdinrpcpass', input='foo').echo) self.log.info("Test connecting to a non-existing server") assert_raises_process_error(1, "Could not connect to the server", self.nodes[0].cli('-rpcport=1').echo) @@ -150,8 +150,8 @@ class TestBitcoinCli(BitcoinTestFramework): w1 = self.nodes[0].get_wallet_rpc(wallets[0]) w2 = self.nodes[0].get_wallet_rpc(wallets[1]) w3 = self.nodes[0].get_wallet_rpc(wallets[2]) - rpcwallet2 = '-rpcwallet={}'.format(wallets[1]) - rpcwallet3 = '-rpcwallet={}'.format(wallets[2]) + rpcwallet2 = f'-rpcwallet={wallets[1]}' + rpcwallet3 = f'-rpcwallet={wallets[2]}' w1.walletpassphrase(password, self.rpc_timeout) w2.encryptwallet(password) w1.sendtoaddress(w2.getnewaddress(), amounts[1]) @@ -162,7 +162,7 @@ class TestBitcoinCli(BitcoinTestFramework): self.log.info("Test -getinfo with multiple wallets and -rpcwallet returns specified wallet balance") for i in range(len(wallets)): - cli_get_info_string = self.nodes[0].cli('-getinfo', '-rpcwallet={}'.format(wallets[i])).send_cli() + cli_get_info_string = self.nodes[0].cli('-getinfo', f'-rpcwallet={wallets[i]}').send_cli() cli_get_info = cli_get_info_string_to_dict(cli_get_info_string) assert 'Balances' not in cli_get_info_string assert_equal(cli_get_info["Wallet"], wallets[i]) @@ -296,7 +296,7 @@ class TestBitcoinCli(BitcoinTestFramework): self.log.info("Test -version with node stopped") self.stop_node(0) cli_response = self.nodes[0].cli('-version').send_cli() - assert "{} RPC client version".format(self.config['environment']['PACKAGE_NAME']) in cli_response + assert f"{self.config['environment']['PACKAGE_NAME']} RPC client version" in cli_response self.log.info("Test -rpcwait option successfully waits for RPC connection") self.nodes[0].start() # start node without RPC connection diff --git a/test/functional/interface_http.py b/test/functional/interface_http.py index d007490f80..075224c011 100755 --- a/test/functional/interface_http.py +++ b/test/functional/interface_http.py @@ -24,8 +24,8 @@ class HTTPBasicsTest (BitcoinTestFramework): # lowlevel check for http persistent connection # ################################################# url = urllib.parse.urlparse(self.nodes[0].url) - authpair = url.username + ':' + url.password - headers = {"Authorization": "Basic " + str_to_b64str(authpair)} + authpair = f'{url.username}:{url.password}' + headers = {"Authorization": f"Basic {str_to_b64str(authpair)}"} conn = http.client.HTTPConnection(url.hostname, url.port) conn.connect() @@ -42,7 +42,7 @@ class HTTPBasicsTest (BitcoinTestFramework): conn.close() #same should be if we add keep-alive because this should be the std. behaviour - headers = {"Authorization": "Basic " + str_to_b64str(authpair), "Connection": "keep-alive"} + headers = {"Authorization": f"Basic {str_to_b64str(authpair)}", "Connection": "keep-alive"} conn = http.client.HTTPConnection(url.hostname, url.port) conn.connect() @@ -59,7 +59,7 @@ class HTTPBasicsTest (BitcoinTestFramework): conn.close() #now do the same with "Connection: close" - headers = {"Authorization": "Basic " + str_to_b64str(authpair), "Connection":"close"} + headers = {"Authorization": f"Basic {str_to_b64str(authpair)}", "Connection":"close"} conn = http.client.HTTPConnection(url.hostname, url.port) conn.connect() @@ -70,8 +70,8 @@ class HTTPBasicsTest (BitcoinTestFramework): #node1 (2nd node) is running with disabled keep-alive option urlNode1 = urllib.parse.urlparse(self.nodes[1].url) - authpair = urlNode1.username + ':' + urlNode1.password - headers = {"Authorization": "Basic " + str_to_b64str(authpair)} + authpair = f'{urlNode1.username}:{urlNode1.password}' + headers = {"Authorization": f"Basic {str_to_b64str(authpair)}"} conn = http.client.HTTPConnection(urlNode1.hostname, urlNode1.port) conn.connect() @@ -81,8 +81,8 @@ class HTTPBasicsTest (BitcoinTestFramework): #node2 (third node) is running with standard keep-alive parameters which means keep-alive is on urlNode2 = urllib.parse.urlparse(self.nodes[2].url) - authpair = urlNode2.username + ':' + urlNode2.password - headers = {"Authorization": "Basic " + str_to_b64str(authpair)} + authpair = f'{urlNode2.username}:{urlNode2.password}' + headers = {"Authorization": f"Basic {str_to_b64str(authpair)}"} conn = http.client.HTTPConnection(urlNode2.hostname, urlNode2.port) conn.connect() @@ -94,13 +94,13 @@ class HTTPBasicsTest (BitcoinTestFramework): # Check excessive request size conn = http.client.HTTPConnection(urlNode2.hostname, urlNode2.port) conn.connect() - conn.request('GET', '/' + ('x'*1000), '', headers) + conn.request('GET', f'/{"x"*1000}', '', headers) out1 = conn.getresponse() assert_equal(out1.status, http.client.NOT_FOUND) conn = http.client.HTTPConnection(urlNode2.hostname, urlNode2.port) conn.connect() - conn.request('GET', '/' + ('x'*10000), '', headers) + conn.request('GET', f'/{"x"*10000}', '', headers) out1 = conn.getresponse() assert_equal(out1.status, http.client.BAD_REQUEST) diff --git a/test/functional/interface_rest.py b/test/functional/interface_rest.py index 0cd6a7b0c6..47c95f5673 100755 --- a/test/functional/interface_rest.py +++ b/test/functional/interface_rest.py @@ -4,7 +4,6 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the REST API.""" -import binascii from decimal import Decimal from enum import Enum from io import BytesIO @@ -58,7 +57,7 @@ class RESTTest (BitcoinTestFramework): rest_uri += '.hex' conn = http.client.HTTPConnection(self.url.hostname, self.url.port) - self.log.debug('%s %s %s', http_method, rest_uri, body) + self.log.debug(f'{http_method} {rest_uri} {body}') if http_method == 'GET': conn.request('GET', rest_uri) elif http_method == 'POST': @@ -93,11 +92,11 @@ class RESTTest (BitcoinTestFramework): self.log.info("Test the /tx URI") - json_obj = self.test_rest_request("/tx/{}".format(txid)) + json_obj = self.test_rest_request(f"/tx/{txid}") assert_equal(json_obj['txid'], txid) # Check hex format response - hex_response = self.test_rest_request("/tx/{}".format(txid), req_type=ReqType.HEX, ret_type=RetType.OBJ) + hex_response = self.test_rest_request(f"/tx/{txid}", req_type=ReqType.HEX, ret_type=RetType.OBJ) assert_greater_than_or_equal(int(hex_response.getheader('content-length')), json_obj['size']*2) @@ -115,7 +114,7 @@ class RESTTest (BitcoinTestFramework): assert_equal(self.nodes[1].getbalance(), Decimal("0.1")) # Check chainTip response - json_obj = self.test_rest_request("/getutxos/{}-{}".format(*spending)) + json_obj = self.test_rest_request(f"/getutxos/{spending[0]}-{spending[1]}") assert_equal(json_obj['chaintipHash'], bb_hash) # Make sure there is one utxo @@ -124,7 +123,7 @@ class RESTTest (BitcoinTestFramework): self.log.info("Query a spent TXO using the /getutxos URI") - json_obj = self.test_rest_request("/getutxos/{}-{}".format(*spent)) + json_obj = self.test_rest_request(f"/getutxos/{spent[0]}-{spent[1]}") # Check chainTip response assert_equal(json_obj['chaintipHash'], bb_hash) @@ -137,7 +136,7 @@ class RESTTest (BitcoinTestFramework): self.log.info("Query two TXOs using the /getutxos URI") - json_obj = self.test_rest_request("/getutxos/{}-{}/{}-{}".format(*(spending + spent))) + json_obj = self.test_rest_request(f"/getutxos/{spending[0]}-{spending[1]}/{spent[0]}-{spent[1]}") assert_equal(len(json_obj['utxos']), 1) assert_equal(json_obj['bitmap'], "10") @@ -164,32 +163,32 @@ class RESTTest (BitcoinTestFramework): # do a tx and don't sync txid = self.nodes[0].sendtoaddress(self.nodes[1].getnewaddress(), 0.1) - json_obj = self.test_rest_request("/tx/{}".format(txid)) + json_obj = self.test_rest_request(f"/tx/{txid}") # get the spent output to later check for utxo (should be spent by then) spent = (json_obj['vin'][0]['txid'], json_obj['vin'][0]['vout']) # get n of 0.1 outpoint n, = filter_output_indices_by_value(json_obj['vout'], Decimal('0.1')) spending = (txid, n) - json_obj = self.test_rest_request("/getutxos/{}-{}".format(*spending)) + json_obj = self.test_rest_request(f"/getutxos/{spending[0]}-{spending[1]}") assert_equal(len(json_obj['utxos']), 0) - json_obj = self.test_rest_request("/getutxos/checkmempool/{}-{}".format(*spending)) + json_obj = self.test_rest_request(f"/getutxos/checkmempool/{spending[0]}-{spending[1]}") assert_equal(len(json_obj['utxos']), 1) - json_obj = self.test_rest_request("/getutxos/{}-{}".format(*spent)) + json_obj = self.test_rest_request(f"/getutxos/{spent[0]}-{spent[1]}") assert_equal(len(json_obj['utxos']), 1) - json_obj = self.test_rest_request("/getutxos/checkmempool/{}-{}".format(*spent)) + json_obj = self.test_rest_request(f"/getutxos/checkmempool/{spent[0]}-{spent[1]}") assert_equal(len(json_obj['utxos']), 0) self.nodes[0].generate(1) self.sync_all() - json_obj = self.test_rest_request("/getutxos/{}-{}".format(*spending)) + json_obj = self.test_rest_request(f"/getutxos/{spending[0]}-{spending[1]}") assert_equal(len(json_obj['utxos']), 1) - json_obj = self.test_rest_request("/getutxos/checkmempool/{}-{}".format(*spending)) + json_obj = self.test_rest_request(f"/getutxos/checkmempool/{spending[0]}-{spending[1]}") assert_equal(len(json_obj['utxos']), 1) # Do some invalid requests @@ -198,11 +197,11 @@ class RESTTest (BitcoinTestFramework): self.test_rest_request("/getutxos/checkmempool", http_method='POST', req_type=ReqType.JSON, status=400, ret_type=RetType.OBJ) # Test limits - long_uri = '/'.join(["{}-{}".format(txid, n_) for n_ in range(20)]) - self.test_rest_request("/getutxos/checkmempool/{}".format(long_uri), http_method='POST', status=400, ret_type=RetType.OBJ) + long_uri = '/'.join([f"{txid}-{n_}" for n_ in range(20)]) + self.test_rest_request(f"/getutxos/checkmempool/{long_uri}", http_method='POST', status=400, ret_type=RetType.OBJ) - long_uri = '/'.join(['{}-{}'.format(txid, n_) for n_ in range(15)]) - self.test_rest_request("/getutxos/checkmempool/{}".format(long_uri), http_method='POST', status=200) + long_uri = '/'.join([f'{txid}-{n_}' for n_ in range(15)]) + self.test_rest_request(f"/getutxos/checkmempool/{long_uri}", http_method='POST', status=200) self.nodes[0].generate(1) # generate block to not affect upcoming tests self.sync_all() @@ -216,42 +215,42 @@ class RESTTest (BitcoinTestFramework): # Check result if block is not in the active chain self.nodes[0].invalidateblock(bb_hash) - assert_equal(self.test_rest_request('/headers/1/{}'.format(bb_hash)), []) - self.test_rest_request('/block/{}'.format(bb_hash)) + assert_equal(self.test_rest_request(f'/headers/1/{bb_hash}'), []) + self.test_rest_request(f'/block/{bb_hash}') self.nodes[0].reconsiderblock(bb_hash) # Check binary format - response = self.test_rest_request("/block/{}".format(bb_hash), req_type=ReqType.BIN, ret_type=RetType.OBJ) + response = self.test_rest_request(f"/block/{bb_hash}", req_type=ReqType.BIN, ret_type=RetType.OBJ) assert_greater_than(int(response.getheader('content-length')), BLOCK_HEADER_SIZE) response_bytes = response.read() # Compare with block header - response_header = self.test_rest_request("/headers/1/{}".format(bb_hash), req_type=ReqType.BIN, ret_type=RetType.OBJ) + response_header = self.test_rest_request(f"/headers/1/{bb_hash}", req_type=ReqType.BIN, ret_type=RetType.OBJ) assert_equal(int(response_header.getheader('content-length')), BLOCK_HEADER_SIZE) response_header_bytes = response_header.read() assert_equal(response_bytes[:BLOCK_HEADER_SIZE], response_header_bytes) # Check block hex format - response_hex = self.test_rest_request("/block/{}".format(bb_hash), req_type=ReqType.HEX, ret_type=RetType.OBJ) + response_hex = self.test_rest_request(f"/block/{bb_hash}", req_type=ReqType.HEX, ret_type=RetType.OBJ) assert_greater_than(int(response_hex.getheader('content-length')), BLOCK_HEADER_SIZE*2) response_hex_bytes = response_hex.read().strip(b'\n') - assert_equal(binascii.hexlify(response_bytes), response_hex_bytes) + assert_equal(response_bytes.hex().encode(), response_hex_bytes) # Compare with hex block header - response_header_hex = self.test_rest_request("/headers/1/{}".format(bb_hash), req_type=ReqType.HEX, ret_type=RetType.OBJ) + response_header_hex = self.test_rest_request(f"/headers/1/{bb_hash}", req_type=ReqType.HEX, ret_type=RetType.OBJ) assert_greater_than(int(response_header_hex.getheader('content-length')), BLOCK_HEADER_SIZE*2) response_header_hex_bytes = response_header_hex.read(BLOCK_HEADER_SIZE*2) - assert_equal(binascii.hexlify(response_bytes[:BLOCK_HEADER_SIZE]), response_header_hex_bytes) + assert_equal(response_bytes[:BLOCK_HEADER_SIZE].hex().encode(), response_header_hex_bytes) # Check json format - block_json_obj = self.test_rest_request("/block/{}".format(bb_hash)) + block_json_obj = self.test_rest_request(f"/block/{bb_hash}") assert_equal(block_json_obj['hash'], bb_hash) - assert_equal(self.test_rest_request("/blockhashbyheight/{}".format(block_json_obj['height']))['blockhash'], bb_hash) + assert_equal(self.test_rest_request(f"/blockhashbyheight/{block_json_obj['height']}")['blockhash'], bb_hash) # Check hex/bin format - resp_hex = self.test_rest_request("/blockhashbyheight/{}".format(block_json_obj['height']), req_type=ReqType.HEX, ret_type=RetType.OBJ) + resp_hex = self.test_rest_request(f"/blockhashbyheight/{block_json_obj['height']}", req_type=ReqType.HEX, ret_type=RetType.OBJ) assert_equal(resp_hex.read().decode('utf-8').rstrip(), bb_hash) - resp_bytes = self.test_rest_request("/blockhashbyheight/{}".format(block_json_obj['height']), req_type=ReqType.BIN, ret_type=RetType.BYTES) + resp_bytes = self.test_rest_request(f"/blockhashbyheight/{block_json_obj['height']}", req_type=ReqType.BIN, ret_type=RetType.BYTES) blockhash = resp_bytes[::-1].hex() assert_equal(blockhash, bb_hash) @@ -265,7 +264,7 @@ class RESTTest (BitcoinTestFramework): self.test_rest_request("/blockhashbyheight/", ret_type=RetType.OBJ, status=400) # Compare with json block header - json_obj = self.test_rest_request("/headers/1/{}".format(bb_hash)) + json_obj = self.test_rest_request(f"/headers/1/{bb_hash}") assert_equal(len(json_obj), 1) # ensure that there is one header in the json response assert_equal(json_obj[0]['hash'], bb_hash) # request/response hash should be the same @@ -277,7 +276,7 @@ class RESTTest (BitcoinTestFramework): # See if we can get 5 headers in one response self.nodes[1].generate(5) self.sync_all() - json_obj = self.test_rest_request("/headers/5/{}".format(bb_hash)) + json_obj = self.test_rest_request(f"/headers/5/{bb_hash}") assert_equal(len(json_obj), 5) # now we should have 5 header objects self.log.info("Test tx inclusion in the /mempool and /block URIs") @@ -307,13 +306,13 @@ class RESTTest (BitcoinTestFramework): self.sync_all() # Check if the 3 tx show up in the new block - json_obj = self.test_rest_request("/block/{}".format(newblockhash[0])) + json_obj = self.test_rest_request(f"/block/{newblockhash[0]}") non_coinbase_txs = {tx['txid'] for tx in json_obj['tx'] if 'coinbase' not in tx['vin'][0]} assert_equal(non_coinbase_txs, set(txs)) # Check the same but without tx details - json_obj = self.test_rest_request("/block/notxdetails/{}".format(newblockhash[0])) + json_obj = self.test_rest_request(f"/block/notxdetails/{newblockhash[0]}") for tx in txs: assert tx in json_obj['tx'] diff --git a/test/functional/interface_rpc.py b/test/functional/interface_rpc.py index 4d5666f414..89a7d29b24 100755 --- a/test/functional/interface_rpc.py +++ b/test/functional/interface_rpc.py @@ -16,7 +16,7 @@ def expect_http_status(expected_http_status, expected_rpc_code, fcn, *args): try: fcn(*args) - raise AssertionError("Expected RPC error %d, got none" % expected_rpc_code) + raise AssertionError(f"Expected RPC error {expected_rpc_code}, got none") except JSONRPCException as exc: assert_equal(exc.error["code"], expected_rpc_code) assert_equal(exc.http_status, expected_http_status) diff --git a/test/functional/interface_zmq.py b/test/functional/interface_zmq.py index 15f352d68c..61b96a8250 100755 --- a/test/functional/interface_zmq.py +++ b/test/functional/interface_zmq.py @@ -132,7 +132,7 @@ class ZMQTest (BitcoinTestFramework): socket = self.ctx.socket(zmq.SUB) subscribers.append(ZMQSubscriber(socket, topic.encode())) - self.restart_node(0, ["-zmqpub%s=%s" % (topic, address) for topic, address in services] + + self.restart_node(0, [f"-zmqpub{topic}={address}" for topic, address in services] + self.extra_args[0]) for i, sub in enumerate(subscribers): @@ -184,7 +184,7 @@ class ZMQTest (BitcoinTestFramework): rawtx = subs[3] num_blocks = 5 - self.log.info("Generate %(n)d blocks (and %(n)d coinbase txes)" % {"n": num_blocks}) + self.log.info(f"Generate {num_blocks} blocks (and {num_blocks} coinbase txes)") genhashes = self.nodes[0].generatetoaddress(num_blocks, ADDRESS_BCRT1_UNSPENDABLE) self.sync_all() @@ -504,7 +504,7 @@ class ZMQTest (BitcoinTestFramework): if mempool_sequence is not None: zmq_mem_seq = mempool_sequence if zmq_mem_seq > get_raw_seq: - raise Exception("We somehow jumped mempool sequence numbers! zmq_mem_seq: {} > get_raw_seq: {}".format(zmq_mem_seq, get_raw_seq)) + raise Exception(f"We somehow jumped mempool sequence numbers! zmq_mem_seq: {zmq_mem_seq} > get_raw_seq: {get_raw_seq}") # 4) Moving forward, we apply the delta to our local view # remaining txs(5) + 1 rbf(A+R) + 1 block connect + 1 final tx @@ -520,7 +520,7 @@ class ZMQTest (BitcoinTestFramework): assert mempool_sequence > expected_sequence r_gap += mempool_sequence - expected_sequence else: - raise Exception("WARNING: txhash has unexpected mempool sequence value: {} vs expected {}".format(mempool_sequence, expected_sequence)) + raise Exception(f"WARNING: txhash has unexpected mempool sequence value: {mempool_sequence} vs expected {expected_sequence}") if label == "A": assert hash_str not in mempool_view mempool_view.add(hash_str) diff --git a/test/functional/mempool_accept_wtxid.py b/test/functional/mempool_accept_wtxid.py index ffafe7428f..82752f2d70 100755 --- a/test/functional/mempool_accept_wtxid.py +++ b/test/functional/mempool_accept_wtxid.py @@ -90,7 +90,7 @@ class MempoolWtxidTest(BitcoinTestFramework): self.log.info("Submit child_one to the mempool") txid_submitted = node.sendrawtransaction(child_one.serialize().hex()) - assert_equal(node.getrawmempool(True)[txid_submitted]['wtxid'], child_one_wtxid) + assert_equal(node.getmempoolentry(txid_submitted)['wtxid'], child_one_wtxid) peer_wtxid_relay.wait_for_broadcast([child_one_wtxid]) assert_equal(node.getmempoolinfo()["unbroadcastcount"], 0) diff --git a/test/functional/mempool_compatibility.py b/test/functional/mempool_compatibility.py index 87f40b7f2b..9a50647a01 100755 --- a/test/functional/mempool_compatibility.py +++ b/test/functional/mempool_compatibility.py @@ -65,8 +65,7 @@ class MempoolCompatibilityTest(BitcoinTestFramework): self.log.info("Add unbroadcasted tx to mempool on new node and shutdown") unbroadcasted_tx_hash = new_wallet.send_self_transfer(from_node=new_node)['txid'] assert unbroadcasted_tx_hash in new_node.getrawmempool() - mempool = new_node.getrawmempool(True) - assert mempool[unbroadcasted_tx_hash]['unbroadcast'] + assert new_node.getmempoolentry(unbroadcasted_tx_hash)['unbroadcast'] self.stop_node(1) self.log.info("Move mempool.dat from new to old node") diff --git a/test/functional/mempool_package_limits.py b/test/functional/mempool_package_limits.py new file mode 100755 index 0000000000..a54c1d0457 --- /dev/null +++ b/test/functional/mempool_package_limits.py @@ -0,0 +1,475 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test logic for limiting mempool and package ancestors/descendants.""" + +from decimal import Decimal + +from test_framework.address import ADDRESS_BCRT1_P2WSH_OP_TRUE +from test_framework.test_framework import BitcoinTestFramework +from test_framework.messages import ( + COIN, + CTransaction, + CTxInWitness, + tx_from_hex, + WITNESS_SCALE_FACTOR, +) +from test_framework.script import ( + CScript, + OP_TRUE, +) +from test_framework.util import ( + assert_equal, +) +from test_framework.wallet import ( + bulk_transaction, + create_child_with_parents, + make_chain, +) + +class MempoolPackageLimitsTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + + def run_test(self): + self.log.info("Generate blocks to create UTXOs") + node = self.nodes[0] + self.privkeys = [node.get_deterministic_priv_key().key] + self.address = node.get_deterministic_priv_key().address + self.coins = [] + # The last 100 coinbase transactions are premature + for b in node.generatetoaddress(200, self.address)[:100]: + coinbase = node.getblock(blockhash=b, verbosity=2)["tx"][0] + self.coins.append({ + "txid": coinbase["txid"], + "amount": coinbase["vout"][0]["value"], + "scriptPubKey": coinbase["vout"][0]["scriptPubKey"], + }) + + self.test_chain_limits() + self.test_desc_count_limits() + self.test_anc_count_limits() + self.test_anc_count_limits_2() + self.test_anc_count_limits_bushy() + + # The node will accept our (nonstandard) extra large OP_RETURN outputs + self.restart_node(0, extra_args=["-acceptnonstdtxn=1"]) + self.test_anc_size_limits() + self.test_desc_size_limits() + + def test_chain_limits_helper(self, mempool_count, package_count): + node = self.nodes[0] + assert_equal(0, node.getmempoolinfo()["size"]) + first_coin = self.coins.pop() + spk = None + txid = first_coin["txid"] + chain_hex = [] + chain_txns = [] + value = first_coin["amount"] + + for i in range(mempool_count + package_count): + (tx, txhex, value, spk) = make_chain(node, self.address, self.privkeys, txid, value, 0, spk) + txid = tx.rehash() + if i < mempool_count: + node.sendrawtransaction(txhex) + assert_equal(node.getmempoolentry(txid)["ancestorcount"], i + 1) + else: + chain_hex.append(txhex) + chain_txns.append(tx) + testres_too_long = node.testmempoolaccept(rawtxs=chain_hex) + for txres in testres_too_long: + assert_equal(txres["package-error"], "package-mempool-limits") + + # Clear mempool and check that the package passes now + node.generate(1) + assert all([res["allowed"] for res in node.testmempoolaccept(rawtxs=chain_hex)]) + + def test_chain_limits(self): + """Create chains from mempool and package transactions that are longer than 25, + but only if both in-mempool and in-package transactions are considered together. + This checks that both mempool and in-package transactions are taken into account when + calculating ancestors/descendant limits. + """ + self.log.info("Check that in-package ancestors count for mempool ancestor limits") + + # 24 transactions in the mempool and 2 in the package. The parent in the package has + # 24 in-mempool ancestors and 1 in-package descendant. The child has 0 direct parents + # in the mempool, but 25 in-mempool and in-package ancestors in total. + self.test_chain_limits_helper(24, 2) + # 2 transactions in the mempool and 24 in the package. + self.test_chain_limits_helper(2, 24) + # 13 transactions in the mempool and 13 in the package. + self.test_chain_limits_helper(13, 13) + + def test_desc_count_limits(self): + """Create an 'A' shaped package with 24 transactions in the mempool and 2 in the package: + M1 + ^ ^ + M2a M2b + . . + . . + . . + M12a ^ + ^ M13b + ^ ^ + Pa Pb + The top ancestor in the package exceeds descendant limits but only if the in-mempool and in-package + descendants are all considered together (24 including in-mempool descendants and 26 including both + package transactions). + """ + node = self.nodes[0] + assert_equal(0, node.getmempoolinfo()["size"]) + self.log.info("Check that in-mempool and in-package descendants are calculated properly in packages") + # Top parent in mempool, M1 + first_coin = self.coins.pop() + parent_value = (first_coin["amount"] - Decimal("0.0002")) / 2 # Deduct reasonable fee and make 2 outputs + inputs = [{"txid": first_coin["txid"], "vout": 0}] + outputs = [{self.address : parent_value}, {ADDRESS_BCRT1_P2WSH_OP_TRUE : parent_value}] + rawtx = node.createrawtransaction(inputs, outputs) + + parent_signed = node.signrawtransactionwithkey(hexstring=rawtx, privkeys=self.privkeys) + assert parent_signed["complete"] + parent_tx = tx_from_hex(parent_signed["hex"]) + parent_txid = parent_tx.rehash() + node.sendrawtransaction(parent_signed["hex"]) + + package_hex = [] + + # Chain A + spk = parent_tx.vout[0].scriptPubKey.hex() + value = parent_value + txid = parent_txid + for i in range(12): + (tx, txhex, value, spk) = make_chain(node, self.address, self.privkeys, txid, value, 0, spk) + txid = tx.rehash() + if i < 11: # M2a... M12a + node.sendrawtransaction(txhex) + else: # Pa + package_hex.append(txhex) + + # Chain B + value = parent_value - Decimal("0.0001") + rawtx_b = node.createrawtransaction([{"txid": parent_txid, "vout": 1}], {self.address : value}) + tx_child_b = tx_from_hex(rawtx_b) # M2b + tx_child_b.wit.vtxinwit = [CTxInWitness()] + tx_child_b.wit.vtxinwit[0].scriptWitness.stack = [CScript([OP_TRUE])] + tx_child_b_hex = tx_child_b.serialize().hex() + node.sendrawtransaction(tx_child_b_hex) + spk = tx_child_b.vout[0].scriptPubKey.hex() + txid = tx_child_b.rehash() + for i in range(12): + (tx, txhex, value, spk) = make_chain(node, self.address, self.privkeys, txid, value, 0, spk) + txid = tx.rehash() + if i < 11: # M3b... M13b + node.sendrawtransaction(txhex) + else: # Pb + package_hex.append(txhex) + + assert_equal(24, node.getmempoolinfo()["size"]) + assert_equal(2, len(package_hex)) + testres_too_long = node.testmempoolaccept(rawtxs=package_hex) + for txres in testres_too_long: + assert_equal(txres["package-error"], "package-mempool-limits") + + # Clear mempool and check that the package passes now + node.generate(1) + assert all([res["allowed"] for res in node.testmempoolaccept(rawtxs=package_hex)]) + + def test_anc_count_limits(self): + """Create a 'V' shaped chain with 24 transactions in the mempool and 3 in the package: + M1a M1b + ^ ^ + M2a M2b + . . + . . + . . + M12a M12b + ^ ^ + Pa Pb + ^ ^ + Pc + The lowest descendant, Pc, exceeds ancestor limits, but only if the in-mempool + and in-package ancestors are all considered together. + """ + node = self.nodes[0] + assert_equal(0, node.getmempoolinfo()["size"]) + package_hex = [] + parents_tx = [] + values = [] + scripts = [] + + self.log.info("Check that in-mempool and in-package ancestors are calculated properly in packages") + + # Two chains of 13 transactions each + for _ in range(2): + spk = None + top_coin = self.coins.pop() + txid = top_coin["txid"] + value = top_coin["amount"] + for i in range(13): + (tx, txhex, value, spk) = make_chain(node, self.address, self.privkeys, txid, value, 0, spk) + txid = tx.rehash() + if i < 12: + node.sendrawtransaction(txhex) + else: # Save the 13th transaction for the package + package_hex.append(txhex) + parents_tx.append(tx) + scripts.append(spk) + values.append(value) + + # Child Pc + child_hex = create_child_with_parents(node, self.address, self.privkeys, parents_tx, values, scripts) + package_hex.append(child_hex) + + assert_equal(24, node.getmempoolinfo()["size"]) + assert_equal(3, len(package_hex)) + testres_too_long = node.testmempoolaccept(rawtxs=package_hex) + for txres in testres_too_long: + assert_equal(txres["package-error"], "package-mempool-limits") + + # Clear mempool and check that the package passes now + node.generate(1) + assert all([res["allowed"] for res in node.testmempoolaccept(rawtxs=package_hex)]) + + def test_anc_count_limits_2(self): + """Create a 'Y' shaped chain with 24 transactions in the mempool and 2 in the package: + M1a M1b + ^ ^ + M2a M2b + . . + . . + . . + M12a M12b + ^ ^ + Pc + ^ + Pd + The lowest descendant, Pd, exceeds ancestor limits, but only if the in-mempool + and in-package ancestors are all considered together. + """ + node = self.nodes[0] + assert_equal(0, node.getmempoolinfo()["size"]) + parents_tx = [] + values = [] + scripts = [] + + self.log.info("Check that in-mempool and in-package ancestors are calculated properly in packages") + # Two chains of 12 transactions each + for _ in range(2): + spk = None + top_coin = self.coins.pop() + txid = top_coin["txid"] + value = top_coin["amount"] + for i in range(12): + (tx, txhex, value, spk) = make_chain(node, self.address, self.privkeys, txid, value, 0, spk) + txid = tx.rehash() + value -= Decimal("0.0001") + node.sendrawtransaction(txhex) + if i == 11: + # last 2 transactions will be the parents of Pc + parents_tx.append(tx) + values.append(value) + scripts.append(spk) + + # Child Pc + pc_hex = create_child_with_parents(node, self.address, self.privkeys, parents_tx, values, scripts) + pc_tx = tx_from_hex(pc_hex) + pc_value = sum(values) - Decimal("0.0002") + pc_spk = pc_tx.vout[0].scriptPubKey.hex() + + # Child Pd + (_, pd_hex, _, _) = make_chain(node, self.address, self.privkeys, pc_tx.rehash(), pc_value, 0, pc_spk) + + assert_equal(24, node.getmempoolinfo()["size"]) + testres_too_long = node.testmempoolaccept(rawtxs=[pc_hex, pd_hex]) + for txres in testres_too_long: + assert_equal(txres["package-error"], "package-mempool-limits") + + # Clear mempool and check that the package passes now + node.generate(1) + assert all([res["allowed"] for res in node.testmempoolaccept(rawtxs=[pc_hex, pd_hex])]) + + def test_anc_count_limits_bushy(self): + """Create a tree with 20 transactions in the mempool and 6 in the package: + M1...M4 M5...M8 M9...M12 M13...M16 M17...M20 + ^ ^ ^ ^ ^ (each with 4 parents) + P0 P1 P2 P3 P4 + ^ ^ ^ ^ ^ (5 parents) + PC + Where M(4i+1)...M+(4i+4) are the parents of Pi and P0, P1, P2, P3, and P4 are the parents of PC. + P0... P4 individually only have 4 parents each, and PC has no in-mempool parents. But + combined, PC has 25 in-mempool and in-package parents. + """ + node = self.nodes[0] + assert_equal(0, node.getmempoolinfo()["size"]) + package_hex = [] + parent_txns = [] + parent_values = [] + scripts = [] + for _ in range(5): # Make package transactions P0 ... P4 + gp_tx = [] + gp_values = [] + gp_scripts = [] + for _ in range(4): # Make mempool transactions M(4i+1)...M(4i+4) + parent_coin = self.coins.pop() + value = parent_coin["amount"] + txid = parent_coin["txid"] + (tx, txhex, value, spk) = make_chain(node, self.address, self.privkeys, txid, value) + gp_tx.append(tx) + gp_values.append(value) + gp_scripts.append(spk) + node.sendrawtransaction(txhex) + # Package transaction Pi + pi_hex = create_child_with_parents(node, self.address, self.privkeys, gp_tx, gp_values, gp_scripts) + package_hex.append(pi_hex) + pi_tx = tx_from_hex(pi_hex) + parent_txns.append(pi_tx) + parent_values.append(Decimal(pi_tx.vout[0].nValue) / COIN) + scripts.append(pi_tx.vout[0].scriptPubKey.hex()) + # Package transaction PC + package_hex.append(create_child_with_parents(node, self.address, self.privkeys, parent_txns, parent_values, scripts)) + + assert_equal(20, node.getmempoolinfo()["size"]) + assert_equal(6, len(package_hex)) + testres = node.testmempoolaccept(rawtxs=package_hex) + for txres in testres: + assert_equal(txres["package-error"], "package-mempool-limits") + + # Clear mempool and check that the package passes now + node.generate(1) + assert all([res["allowed"] for res in node.testmempoolaccept(rawtxs=package_hex)]) + + def test_anc_size_limits(self): + """Test Case with 2 independent transactions in the mempool and a parent + child in the + package, where the package parent is the child of both mempool transactions (30KvB each): + A B + ^ ^ + C + ^ + D + The lowest descendant, D, exceeds ancestor size limits, but only if the in-mempool + and in-package ancestors are all considered together. + """ + node = self.nodes[0] + assert_equal(0, node.getmempoolinfo()["size"]) + parents_tx = [] + values = [] + scripts = [] + target_weight = WITNESS_SCALE_FACTOR * 1000 * 30 # 30KvB + high_fee = Decimal("0.003") # 10 sats/vB + self.log.info("Check that in-mempool and in-package ancestor size limits are calculated properly in packages") + # Mempool transactions A and B + for _ in range(2): + spk = None + top_coin = self.coins.pop() + txid = top_coin["txid"] + value = top_coin["amount"] + (tx, _, _, _) = make_chain(node, self.address, self.privkeys, txid, value, 0, spk, high_fee) + bulked_tx = bulk_transaction(tx, node, target_weight, self.privkeys) + node.sendrawtransaction(bulked_tx.serialize().hex()) + parents_tx.append(bulked_tx) + values.append(Decimal(bulked_tx.vout[0].nValue) / COIN) + scripts.append(bulked_tx.vout[0].scriptPubKey.hex()) + + # Package transaction C + small_pc_hex = create_child_with_parents(node, self.address, self.privkeys, parents_tx, values, scripts, high_fee) + pc_tx = bulk_transaction(tx_from_hex(small_pc_hex), node, target_weight, self.privkeys) + pc_value = Decimal(pc_tx.vout[0].nValue) / COIN + pc_spk = pc_tx.vout[0].scriptPubKey.hex() + pc_hex = pc_tx.serialize().hex() + + # Package transaction D + (small_pd, _, val, spk) = make_chain(node, self.address, self.privkeys, pc_tx.rehash(), pc_value, 0, pc_spk, high_fee) + prevtxs = [{ + "txid": pc_tx.rehash(), + "vout": 0, + "scriptPubKey": spk, + "amount": val, + }] + pd_tx = bulk_transaction(small_pd, node, target_weight, self.privkeys, prevtxs) + pd_hex = pd_tx.serialize().hex() + + assert_equal(2, node.getmempoolinfo()["size"]) + testres_too_heavy = node.testmempoolaccept(rawtxs=[pc_hex, pd_hex]) + for txres in testres_too_heavy: + assert_equal(txres["package-error"], "package-mempool-limits") + + # Clear mempool and check that the package passes now + node.generate(1) + assert all([res["allowed"] for res in node.testmempoolaccept(rawtxs=[pc_hex, pd_hex])]) + + def test_desc_size_limits(self): + """Create 3 mempool transactions and 2 package transactions (25KvB each): + Ma + ^ ^ + Mb Mc + ^ ^ + Pd Pe + The top ancestor in the package exceeds descendant size limits but only if the in-mempool + and in-package descendants are all considered together. + """ + node = self.nodes[0] + assert_equal(0, node.getmempoolinfo()["size"]) + target_weight = 21 * 1000 * WITNESS_SCALE_FACTOR + high_fee = Decimal("0.0021") # 10 sats/vB + self.log.info("Check that in-mempool and in-package descendant sizes are calculated properly in packages") + # Top parent in mempool, Ma + first_coin = self.coins.pop() + parent_value = (first_coin["amount"] - high_fee) / 2 # Deduct fee and make 2 outputs + inputs = [{"txid": first_coin["txid"], "vout": 0}] + outputs = [{self.address : parent_value}, {ADDRESS_BCRT1_P2WSH_OP_TRUE: parent_value}] + rawtx = node.createrawtransaction(inputs, outputs) + parent_tx = bulk_transaction(tx_from_hex(rawtx), node, target_weight, self.privkeys) + node.sendrawtransaction(parent_tx.serialize().hex()) + + package_hex = [] + for j in range(2): # Two legs (left and right) + # Mempool transaction (Mb and Mc) + mempool_tx = CTransaction() + spk = parent_tx.vout[j].scriptPubKey.hex() + value = Decimal(parent_tx.vout[j].nValue) / COIN + txid = parent_tx.rehash() + prevtxs = [{ + "txid": txid, + "vout": j, + "scriptPubKey": spk, + "amount": value, + }] + if j == 0: # normal key + (tx_small, _, _, _) = make_chain(node, self.address, self.privkeys, txid, value, j, spk, high_fee) + mempool_tx = bulk_transaction(tx_small, node, target_weight, self.privkeys, prevtxs) + else: # OP_TRUE + inputs = [{"txid": txid, "vout": 1}] + outputs = {self.address: value - high_fee} + small_tx = tx_from_hex(node.createrawtransaction(inputs, outputs)) + mempool_tx = bulk_transaction(small_tx, node, target_weight, None, prevtxs) + node.sendrawtransaction(mempool_tx.serialize().hex()) + + # Package transaction (Pd and Pe) + spk = mempool_tx.vout[0].scriptPubKey.hex() + value = Decimal(mempool_tx.vout[0].nValue) / COIN + txid = mempool_tx.rehash() + (tx_small, _, _, _) = make_chain(node, self.address, self.privkeys, txid, value, 0, spk, high_fee) + prevtxs = [{ + "txid": txid, + "vout": 0, + "scriptPubKey": spk, + "amount": value, + }] + package_tx = bulk_transaction(tx_small, node, target_weight, self.privkeys, prevtxs) + package_hex.append(package_tx.serialize().hex()) + + assert_equal(3, node.getmempoolinfo()["size"]) + assert_equal(2, len(package_hex)) + testres_too_heavy = node.testmempoolaccept(rawtxs=package_hex) + for txres in testres_too_heavy: + assert_equal(txres["package-error"], "package-mempool-limits") + + # Clear mempool and check that the package passes now + node.generate(1) + assert all([res["allowed"] for res in node.testmempoolaccept(rawtxs=package_hex)]) + +if __name__ == "__main__": + MempoolPackageLimitsTest().main() diff --git a/test/functional/mempool_package_onemore.py b/test/functional/mempool_package_onemore.py index fcd8b061fa..3ee50f5a8a 100755 --- a/test/functional/mempool_package_onemore.py +++ b/test/functional/mempool_package_onemore.py @@ -51,7 +51,7 @@ class MempoolPackagesTest(BitcoinTestFramework): (second_chain, second_chain_value) = chain_transaction(self.nodes[0], [utxo[1]['txid']], [utxo[1]['vout']], utxo[1]['amount'], fee, 1) # Check mempool has MAX_ANCESTORS + 1 transactions in it - assert_equal(len(self.nodes[0].getrawmempool(True)), MAX_ANCESTORS + 1) + assert_equal(len(self.nodes[0].getrawmempool()), MAX_ANCESTORS + 1) # Adding one more transaction on to the chain should fail. assert_raises_rpc_error(-26, "too-long-mempool-chain, too many unconfirmed ancestors [limit: 25]", chain_transaction, self.nodes[0], [txid], [0], value, fee, 1) @@ -74,7 +74,7 @@ class MempoolPackagesTest(BitcoinTestFramework): self.nodes[0].sendrawtransaction(signed_second_tx['hex']) # Finally, check that we added two transactions - assert_equal(len(self.nodes[0].getrawmempool(True)), MAX_ANCESTORS + 3) + assert_equal(len(self.nodes[0].getrawmempool()), MAX_ANCESTORS + 3) if __name__ == '__main__': MempoolPackagesTest().main() diff --git a/test/functional/mempool_packages.py b/test/functional/mempool_packages.py index 5fc3ec23ae..173d1d7493 100755 --- a/test/functional/mempool_packages.py +++ b/test/functional/mempool_packages.py @@ -89,28 +89,28 @@ class MempoolPackagesTest(BitcoinTestFramework): assert_equal(entry, mempool[x]) # Check that the descendant calculations are correct - assert_equal(mempool[x]['descendantcount'], descendant_count) - descendant_fees += mempool[x]['fee'] - assert_equal(mempool[x]['modifiedfee'], mempool[x]['fee']) - assert_equal(mempool[x]['fees']['base'], mempool[x]['fee']) - assert_equal(mempool[x]['fees']['modified'], mempool[x]['modifiedfee']) - assert_equal(mempool[x]['descendantfees'], descendant_fees * COIN) - assert_equal(mempool[x]['fees']['descendant'], descendant_fees) - descendant_vsize += mempool[x]['vsize'] - assert_equal(mempool[x]['descendantsize'], descendant_vsize) + assert_equal(entry['descendantcount'], descendant_count) + descendant_fees += entry['fee'] + assert_equal(entry['modifiedfee'], entry['fee']) + assert_equal(entry['fees']['base'], entry['fee']) + assert_equal(entry['fees']['modified'], entry['modifiedfee']) + assert_equal(entry['descendantfees'], descendant_fees * COIN) + assert_equal(entry['fees']['descendant'], descendant_fees) + descendant_vsize += entry['vsize'] + assert_equal(entry['descendantsize'], descendant_vsize) descendant_count += 1 # Check that ancestor calculations are correct - assert_equal(mempool[x]['ancestorcount'], ancestor_count) - assert_equal(mempool[x]['ancestorfees'], ancestor_fees * COIN) - assert_equal(mempool[x]['ancestorsize'], ancestor_vsize) - ancestor_vsize -= mempool[x]['vsize'] - ancestor_fees -= mempool[x]['fee'] + assert_equal(entry['ancestorcount'], ancestor_count) + assert_equal(entry['ancestorfees'], ancestor_fees * COIN) + assert_equal(entry['ancestorsize'], ancestor_vsize) + ancestor_vsize -= entry['vsize'] + ancestor_fees -= entry['fee'] ancestor_count -= 1 # Check that parent/child list is correct - assert_equal(mempool[x]['spentby'], descendants[-1:]) - assert_equal(mempool[x]['depends'], ancestors[-2:-1]) + assert_equal(entry['spentby'], descendants[-1:]) + assert_equal(entry['depends'], ancestors[-2:-1]) # Check that getmempooldescendants is correct assert_equal(sorted(descendants), sorted(self.nodes[0].getmempooldescendants(x))) @@ -153,12 +153,12 @@ class MempoolPackagesTest(BitcoinTestFramework): # Check that ancestor modified fees includes fee deltas from # prioritisetransaction self.nodes[0].prioritisetransaction(txid=chain[0], fee_delta=1000) - mempool = self.nodes[0].getrawmempool(True) ancestor_fees = 0 for x in chain: - ancestor_fees += mempool[x]['fee'] - assert_equal(mempool[x]['fees']['ancestor'], ancestor_fees + Decimal('0.00001')) - assert_equal(mempool[x]['ancestorfees'], ancestor_fees * COIN + 1000) + entry = self.nodes[0].getmempoolentry(x) + ancestor_fees += entry['fee'] + assert_equal(entry['fees']['ancestor'], ancestor_fees + Decimal('0.00001')) + assert_equal(entry['ancestorfees'], ancestor_fees * COIN + 1000) # Undo the prioritisetransaction for later tests self.nodes[0].prioritisetransaction(txid=chain[0], fee_delta=-1000) @@ -166,13 +166,13 @@ class MempoolPackagesTest(BitcoinTestFramework): # Check that descendant modified fees includes fee deltas from # prioritisetransaction self.nodes[0].prioritisetransaction(txid=chain[-1], fee_delta=1000) - mempool = self.nodes[0].getrawmempool(True) descendant_fees = 0 for x in reversed(chain): - descendant_fees += mempool[x]['fee'] - assert_equal(mempool[x]['fees']['descendant'], descendant_fees + Decimal('0.00001')) - assert_equal(mempool[x]['descendantfees'], descendant_fees * COIN + 1000) + entry = self.nodes[0].getmempoolentry(x) + descendant_fees += entry['fee'] + assert_equal(entry['fees']['descendant'], descendant_fees + Decimal('0.00001')) + assert_equal(entry['descendantfees'], descendant_fees * COIN + 1000) # Adding one more transaction on to the chain should fail. assert_raises_rpc_error(-26, "too-long-mempool-chain", chain_transaction, self.nodes[0], [txid], [vout], value, fee, 1) @@ -190,16 +190,15 @@ class MempoolPackagesTest(BitcoinTestFramework): self.nodes[1].invalidateblock(self.nodes[1].getbestblockhash()) # Now check that the transaction is in the mempool, with the right modified fee - mempool = self.nodes[0].getrawmempool(True) - descendant_fees = 0 for x in reversed(chain): - descendant_fees += mempool[x]['fee'] + entry = self.nodes[0].getmempoolentry(x) + descendant_fees += entry['fee'] if (x == chain[-1]): - assert_equal(mempool[x]['modifiedfee'], mempool[x]['fee']+satoshi_round(0.00002)) - assert_equal(mempool[x]['fees']['modified'], mempool[x]['fee']+satoshi_round(0.00002)) - assert_equal(mempool[x]['descendantfees'], descendant_fees * COIN + 2000) - assert_equal(mempool[x]['fees']['descendant'], descendant_fees+satoshi_round(0.00002)) + assert_equal(entry['modifiedfee'], entry['fee']+satoshi_round(0.00002)) + assert_equal(entry['fees']['modified'], entry['fee']+satoshi_round(0.00002)) + assert_equal(entry['descendantfees'], descendant_fees * COIN + 2000) + assert_equal(entry['fees']['descendant'], descendant_fees+satoshi_round(0.00002)) # Check that node1's mempool is as expected (-> custom ancestor limit) mempool0 = self.nodes[0].getrawmempool(False) @@ -255,7 +254,7 @@ class MempoolPackagesTest(BitcoinTestFramework): # - txs from previous ancestor test (-> custom ancestor limit) # - parent tx for descendant test # - txs chained off parent tx (-> custom descendant limit) - self.wait_until(lambda: len(self.nodes[1].getrawmempool(False)) == + self.wait_until(lambda: len(self.nodes[1].getrawmempool()) == MAX_ANCESTORS_CUSTOM + 1 + MAX_DESCENDANTS_CUSTOM, timeout=10) mempool0 = self.nodes[0].getrawmempool(False) mempool1 = self.nodes[1].getrawmempool(False) diff --git a/test/functional/mempool_unbroadcast.py b/test/functional/mempool_unbroadcast.py index 7d9e6c306d..5fe20ea9e1 100755 --- a/test/functional/mempool_unbroadcast.py +++ b/test/functional/mempool_unbroadcast.py @@ -94,9 +94,7 @@ class MempoolUnbroadcastTest(BitcoinTestFramework): self.log.info("Rebroadcast transaction and ensure it is not added to unbroadcast set when already in mempool") rpc_tx_hsh = node.sendrawtransaction(txFS["hex"]) - mempool = node.getrawmempool(True) - assert rpc_tx_hsh in mempool - assert not mempool[rpc_tx_hsh]['unbroadcast'] + assert not node.getmempoolentry(rpc_tx_hsh)['unbroadcast'] def test_txn_removal(self): self.log.info("Test that transactions removed from mempool are removed from unbroadcast set") diff --git a/test/functional/mempool_updatefromblock.py b/test/functional/mempool_updatefromblock.py index 8baf974a0a..4cd11e9d11 100755 --- a/test/functional/mempool_updatefromblock.py +++ b/test/functional/mempool_updatefromblock.py @@ -86,7 +86,7 @@ class MempoolUpdateFromBlockTest(BitcoinTestFramework): unsigned_raw_tx = self.nodes[0].createrawtransaction(inputs, outputs) signed_raw_tx = self.nodes[0].signrawtransactionwithwallet(unsigned_raw_tx) tx_id.append(self.nodes[0].sendrawtransaction(signed_raw_tx['hex'])) - tx_size.append(self.nodes[0].getrawmempool(True)[tx_id[-1]]['vsize']) + tx_size.append(self.nodes[0].getmempoolentry(tx_id[-1])['vsize']) if tx_count in n_tx_to_mine: # The created transactions are mined into blocks by batches. @@ -109,10 +109,11 @@ class MempoolUpdateFromBlockTest(BitcoinTestFramework): self.log.info('Checking descendants/ancestors properties of all of the in-mempool transactions...') for k, tx in enumerate(tx_id): self.log.debug('Check transaction #{}.'.format(k)) - assert_equal(self.nodes[0].getrawmempool(True)[tx]['descendantcount'], size - k) - assert_equal(self.nodes[0].getrawmempool(True)[tx]['descendantsize'], sum(tx_size[k:size])) - assert_equal(self.nodes[0].getrawmempool(True)[tx]['ancestorcount'], k + 1) - assert_equal(self.nodes[0].getrawmempool(True)[tx]['ancestorsize'], sum(tx_size[0:(k + 1)])) + entry = self.nodes[0].getmempoolentry(tx) + assert_equal(entry['descendantcount'], size - k) + assert_equal(entry['descendantsize'], sum(tx_size[k:size])) + assert_equal(entry['ancestorcount'], k + 1) + assert_equal(entry['ancestorsize'], sum(tx_size[0:(k + 1)])) def run_test(self): # Use batch size limited by DEFAULT_ANCESTOR_LIMIT = 25 to not fire "too many unconfirmed parents" error. diff --git a/test/functional/mining_basic.py b/test/functional/mining_basic.py index 01fc02f27e..23711646a2 100755 --- a/test/functional/mining_basic.py +++ b/test/functional/mining_basic.py @@ -65,10 +65,10 @@ class MiningTest(BitcoinTestFramework): assert_equal(mining_info['currentblockweight'], 4000) self.log.info('test blockversion') - self.restart_node(0, extra_args=['-mocktime={}'.format(t), '-blockversion=1337']) + self.restart_node(0, extra_args=[f'-mocktime={t}', '-blockversion=1337']) self.connect_nodes(0, 1) assert_equal(1337, self.nodes[0].getblocktemplate(NORMAL_GBT_REQUEST_PARAMS)['version']) - self.restart_node(0, extra_args=['-mocktime={}'.format(t)]) + self.restart_node(0, extra_args=[f'-mocktime={t}']) self.connect_nodes(0, 1) assert_equal(VERSIONBITS_TOP_BITS + (1 << VERSIONBITS_DEPLOYMENT_TESTDUMMY_BIT), self.nodes[0].getblocktemplate(NORMAL_GBT_REQUEST_PARAMS)['version']) self.restart_node(0) diff --git a/test/functional/p2p_addr_relay.py b/test/functional/p2p_addr_relay.py index 95743a1bf5..e93698b08e 100755 --- a/test/functional/p2p_addr_relay.py +++ b/test/functional/p2p_addr_relay.py @@ -179,7 +179,7 @@ class AddrTest(BitcoinTestFramework): # of the outbound peer which is often sent before the GETADDR response. assert_equal(inbound_peer.num_ipv4_received, 0) - # Send an empty ADDR message to intialize address relay on this connection. + # Send an empty ADDR message to initialize address relay on this connection. inbound_peer.send_and_ping(msg_addr()) self.log.info('Check that subsequent addr messages sent from an outbound peer are relayed') @@ -311,7 +311,7 @@ class AddrTest(BitcoinTestFramework): self.nodes[0].disconnect_p2ps() - def send_addrs_and_test_rate_limiting(self, peer, no_relay, new_addrs, total_addrs): + def send_addrs_and_test_rate_limiting(self, peer, no_relay, *, new_addrs, total_addrs): """Send an addr message and check that the number of addresses processed and rate-limited is as expected""" peer.send_and_ping(self.setup_rand_addr_msg(new_addrs)) @@ -329,27 +329,26 @@ class AddrTest(BitcoinTestFramework): assert_equal(addrs_rate_limited, max(0, total_addrs - peer.tokens)) def rate_limit_tests(self): - self.mocktime = int(time.time()) self.restart_node(0, []) self.nodes[0].setmocktime(self.mocktime) - for contype, no_relay in [("outbound-full-relay", False), ("block-relay-only", True), ("inbound", False)]: - self.log.info(f'Test rate limiting of addr processing for {contype} peers') - if contype == "inbound": + for conn_type, no_relay in [("outbound-full-relay", False), ("block-relay-only", True), ("inbound", False)]: + self.log.info(f'Test rate limiting of addr processing for {conn_type} peers') + if conn_type == "inbound": peer = self.nodes[0].add_p2p_connection(AddrReceiver()) else: - peer = self.nodes[0].add_outbound_p2p_connection(AddrReceiver(), p2p_idx=0, connection_type=contype) + peer = self.nodes[0].add_outbound_p2p_connection(AddrReceiver(), p2p_idx=0, connection_type=conn_type) # Send 600 addresses. For all but the block-relay-only peer this should result in addresses being processed. - self.send_addrs_and_test_rate_limiting(peer, no_relay, 600, 600) + self.send_addrs_and_test_rate_limiting(peer, no_relay, new_addrs=600, total_addrs=600) # Send 600 more addresses. For the outbound-full-relay peer (which we send a GETADDR, and thus will # process up to 1001 incoming addresses), this means more addresses will be processed. - self.send_addrs_and_test_rate_limiting(peer, no_relay, 600, 1200) + self.send_addrs_and_test_rate_limiting(peer, no_relay, new_addrs=600, total_addrs=1200) # Send 10 more. As we reached the processing limit for all nodes, no more addresses should be procesesd. - self.send_addrs_and_test_rate_limiting(peer, no_relay, 10, 1210) + self.send_addrs_and_test_rate_limiting(peer, no_relay, new_addrs=10, total_addrs=1210) # Advance the time by 100 seconds, permitting the processing of 10 more addresses. # Send 200 and verify that 10 are processed. @@ -357,7 +356,7 @@ class AddrTest(BitcoinTestFramework): self.nodes[0].setmocktime(self.mocktime) peer.increment_tokens(10) - self.send_addrs_and_test_rate_limiting(peer, no_relay, 200, 1410) + self.send_addrs_and_test_rate_limiting(peer, no_relay, new_addrs=200, total_addrs=1410) # Advance the time by 1000 seconds, permitting the processing of 100 more addresses. # Send 200 and verify that 100 are processed. @@ -365,9 +364,10 @@ class AddrTest(BitcoinTestFramework): self.nodes[0].setmocktime(self.mocktime) peer.increment_tokens(100) - self.send_addrs_and_test_rate_limiting(peer, no_relay, 200, 1610) + self.send_addrs_and_test_rate_limiting(peer, no_relay, new_addrs=200, total_addrs=1610) self.nodes[0].disconnect_p2ps() + if __name__ == '__main__': AddrTest().main() diff --git a/test/functional/p2p_addrfetch.py b/test/functional/p2p_addrfetch.py index 66ee1544a9..2a0f6432a9 100755 --- a/test/functional/p2p_addrfetch.py +++ b/test/functional/p2p_addrfetch.py @@ -21,18 +21,24 @@ ADDR.port = 18444 class P2PAddrFetch(BitcoinTestFramework): - def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 + def assert_getpeerinfo(self, *, peer_ids): + num_peers = len(peer_ids) + info = self.nodes[0].getpeerinfo() + assert_equal(len(info), num_peers) + for n in range(0, num_peers): + assert_equal(info[n]['id'], peer_ids[n]) + assert_equal(info[n]['connection_type'], 'addr-fetch') + def run_test(self): node = self.nodes[0] self.log.info("Connect to an addr-fetch peer") - peer = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=0, connection_type="addr-fetch") - info = node.getpeerinfo() - assert_equal(len(info), 1) - assert_equal(info[0]['connection_type'], 'addr-fetch') + peer_id = 0 + peer = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=peer_id, connection_type="addr-fetch") + self.assert_getpeerinfo(peer_ids=[peer_id]) self.log.info("Check that we send getaddr but don't try to sync headers with the addr-fetch peer") peer.sync_send_with_ping() @@ -45,7 +51,7 @@ class P2PAddrFetch(BitcoinTestFramework): msg = msg_addr() msg.addrs = [ADDR] peer.send_and_ping(msg) - assert_equal(len(node.getpeerinfo()), 1) + self.assert_getpeerinfo(peer_ids=[peer_id]) self.log.info("Check that answering with larger addr messages leads to disconnect") msg.addrs = [ADDR] * 2 @@ -53,9 +59,20 @@ class P2PAddrFetch(BitcoinTestFramework): peer.wait_for_disconnect(timeout=5) self.log.info("Check timeout for addr-fetch peer that does not send addrs") - peer = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=1, connection_type="addr-fetch") - node.setmocktime(int(time.time()) + 301) # Timeout: 5 minutes + peer_id = 1 + peer = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=peer_id, connection_type="addr-fetch") + + time_now = int(time.time()) + self.assert_getpeerinfo(peer_ids=[peer_id]) + + # Expect addr-fetch peer connection to be maintained up to 5 minutes. + node.setmocktime(time_now + 295) + self.assert_getpeerinfo(peer_ids=[peer_id]) + + # Expect addr-fetch peer connection to be disconnected after 5 minutes. + node.setmocktime(time_now + 301) peer.wait_for_disconnect(timeout=5) + self.assert_getpeerinfo(peer_ids=[]) if __name__ == '__main__': diff --git a/test/functional/rpc_blockchain.py b/test/functional/rpc_blockchain.py index 721e3f93a3..1e73dcf5cd 100755 --- a/test/functional/rpc_blockchain.py +++ b/test/functional/rpc_blockchain.py @@ -27,6 +27,7 @@ import subprocess from test_framework.address import ADDRESS_BCRT1_P2WSH_OP_TRUE from test_framework.blocktools import ( + DERSIG_HEIGHT, create_block, create_coinbase, TIME_GENESIS_BLOCK, @@ -141,7 +142,7 @@ class BlockchainTest(BitcoinTestFramework): assert_equal(res['softforks'], { 'bip34': {'type': 'buried', 'active': True, 'height': 2}, - 'bip66': {'type': 'buried', 'active': False, 'height': 1251}, + 'bip66': {'type': 'buried', 'active': True, 'height': DERSIG_HEIGHT}, 'bip65': {'type': 'buried', 'active': False, 'height': 1351}, 'csv': {'type': 'buried', 'active': False, 'height': 432}, 'segwit': {'type': 'buried', 'active': True, 'height': 0}, diff --git a/test/functional/rpc_createmultisig.py b/test/functional/rpc_createmultisig.py index 816ec67492..bb2e51c2f7 100755 --- a/test/functional/rpc_createmultisig.py +++ b/test/functional/rpc_createmultisig.py @@ -3,7 +3,6 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test multisig RPCs""" -import binascii import decimal import itertools import json @@ -66,9 +65,9 @@ class RpcCreateMultiSigTest(BitcoinTestFramework): # decompress pk2 pk_obj = ECPubKey() - pk_obj.set(binascii.unhexlify(pk2)) + pk_obj.set(bytes.fromhex(pk2)) pk_obj.compressed = False - pk2 = binascii.hexlify(pk_obj.get_bytes()).decode() + pk2 = pk_obj.get_bytes().hex() node0.createwallet(wallet_name='wmulti0', disable_private_keys=True) wmulti0 = node0.get_wallet_rpc('wmulti0') diff --git a/test/functional/rpc_fundrawtransaction.py b/test/functional/rpc_fundrawtransaction.py index fa98c44152..2369ad25d5 100755 --- a/test/functional/rpc_fundrawtransaction.py +++ b/test/functional/rpc_fundrawtransaction.py @@ -99,6 +99,7 @@ class RawTransactionsTest(BitcoinTestFramework): self.test_subtract_fee_with_presets() self.test_transaction_too_large() self.test_include_unsafe() + self.test_22670() def test_change_position(self): """Ensure setting changePosition in fundraw with an exact match is handled properly.""" @@ -379,7 +380,7 @@ class RawTransactionsTest(BitcoinTestFramework): # Create same transaction over sendtoaddress. txId = self.nodes[0].sendtoaddress(self.nodes[1].getnewaddress(), 1.1) - signedFee = self.nodes[0].getrawmempool(True)[txId]['fee'] + signedFee = self.nodes[0].getmempoolentry(txId)['fee'] # Compare fee. feeDelta = Decimal(fundedTx['fee']) - Decimal(signedFee) @@ -402,7 +403,7 @@ class RawTransactionsTest(BitcoinTestFramework): # Create same transaction over sendtoaddress. txId = self.nodes[0].sendmany("", outputs) - signedFee = self.nodes[0].getrawmempool(True)[txId]['fee'] + signedFee = self.nodes[0].getmempoolentry(txId)['fee'] # Compare fee. feeDelta = Decimal(fundedTx['fee']) - Decimal(signedFee) @@ -426,7 +427,7 @@ class RawTransactionsTest(BitcoinTestFramework): # Create same transaction over sendtoaddress. txId = self.nodes[0].sendtoaddress(mSigObj, 1.1) - signedFee = self.nodes[0].getrawmempool(True)[txId]['fee'] + signedFee = self.nodes[0].getmempoolentry(txId)['fee'] # Compare fee. feeDelta = Decimal(fundedTx['fee']) - Decimal(signedFee) @@ -467,7 +468,7 @@ class RawTransactionsTest(BitcoinTestFramework): # Create same transaction over sendtoaddress. txId = self.nodes[0].sendtoaddress(mSigObj, 1.1) - signedFee = self.nodes[0].getrawmempool(True)[txId]['fee'] + signedFee = self.nodes[0].getmempoolentry(txId)['fee'] # Compare fee. feeDelta = Decimal(fundedTx['fee']) - Decimal(signedFee) @@ -599,7 +600,7 @@ class RawTransactionsTest(BitcoinTestFramework): # Create same transaction over sendtoaddress. txId = self.nodes[1].sendmany("", outputs) - signedFee = self.nodes[1].getrawmempool(True)[txId]['fee'] + signedFee = self.nodes[1].getmempoolentry(txId)['fee'] # Compare fee. feeDelta = Decimal(fundedTx['fee']) - Decimal(signedFee) @@ -969,6 +970,62 @@ class RawTransactionsTest(BitcoinTestFramework): signedtx = wallet.signrawtransactionwithwallet(fundedtx['hex']) wallet.sendrawtransaction(signedtx['hex']) + def test_22670(self): + # In issue #22670, it was observed that ApproximateBestSubset may + # choose enough value to cover the target amount but not enough to cover the transaction fees. + # This leads to a transaction whose actual transaction feerate is lower than expected. + # However at normal feerates, the difference between the effective value and the real value + # that this bug is not detected because the transaction fee must be at least 0.01 BTC (the minimum change value). + # Otherwise the targeted minimum change value will be enough to cover the transaction fees that were not + # being accounted for. So the minimum relay fee is set to 0.1 BTC/kvB in this test. + self.log.info("Test issue 22670 ApproximateBestSubset bug") + # Make sure the default wallet will not be loaded when restarted with a high minrelaytxfee + self.nodes[0].unloadwallet(self.default_wallet_name, False) + feerate = Decimal("0.1") + self.restart_node(0, [f"-minrelaytxfee={feerate}", "-discardfee=0"]) # Set high minrelayfee, set discardfee to 0 for easier calculation + + self.nodes[0].loadwallet(self.default_wallet_name, True) + funds = self.nodes[0].get_wallet_rpc(self.default_wallet_name) + self.nodes[0].createwallet(wallet_name="tester") + tester = self.nodes[0].get_wallet_rpc("tester") + + # Because this test is specifically for ApproximateBestSubset, the target value must be greater + # than any single input available, and require more than 1 input. So we make 3 outputs + for i in range(0, 3): + funds.sendtoaddress(tester.getnewaddress(address_type="bech32"), 1) + self.nodes[0].generate(1) + + # Create transactions in order to calculate fees for the target bounds that can trigger this bug + change_tx = tester.fundrawtransaction(tester.createrawtransaction([], [{funds.getnewaddress(): 1.5}])) + tx = tester.createrawtransaction([], [{funds.getnewaddress(): 2}]) + no_change_tx = tester.fundrawtransaction(tx, {"subtractFeeFromOutputs": [0]}) + + overhead_fees = feerate * len(tx) / 2 / 1000 + cost_of_change = change_tx["fee"] - no_change_tx["fee"] + fees = no_change_tx["fee"] + assert_greater_than(fees, 0.01) + + def do_fund_send(target): + create_tx = tester.createrawtransaction([], [{funds.getnewaddress(): target}]) + funded_tx = tester.fundrawtransaction(create_tx) + signed_tx = tester.signrawtransactionwithwallet(funded_tx["hex"]) + assert signed_tx["complete"] + decoded_tx = tester.decoderawtransaction(signed_tx["hex"]) + assert_equal(len(decoded_tx["vin"]), 3) + assert tester.testmempoolaccept([signed_tx["hex"]])[0]["allowed"] + + # We want to choose more value than is available in 2 inputs when considering the fee, + # but not enough to need 3 inputs when not considering the fee. + # So the target value must be at least 2.00000001 - fee. + lower_bound = Decimal("2.00000001") - fees + # The target value must be at most 2 - cost_of_change - not_input_fees - min_change (these are all + # included in the target before ApproximateBestSubset). + upper_bound = Decimal("2.0") - cost_of_change - overhead_fees - Decimal("0.01") + assert_greater_than_or_equal(upper_bound, lower_bound) + do_fund_send(lower_bound) + do_fund_send(upper_bound) + + self.restart_node(0) if __name__ == '__main__': RawTransactionsTest().main() diff --git a/test/functional/rpc_packages.py b/test/functional/rpc_packages.py index 4b2ed20958..3cb4154601 100755 --- a/test/functional/rpc_packages.py +++ b/test/functional/rpc_packages.py @@ -22,6 +22,11 @@ from test_framework.script import ( from test_framework.util import ( assert_equal, ) +from test_framework.wallet import ( + create_child_with_parents, + create_raw_chain, + make_chain, +) class RPCPackagesTest(BitcoinTestFramework): def set_test_params(self): @@ -78,26 +83,6 @@ class RPCPackagesTest(BitcoinTestFramework): self.test_conflicting() self.test_rbf() - def chain_transaction(self, parent_txid, parent_value, n=0, parent_locking_script=None): - """Build a transaction that spends parent_txid.vout[n] and produces one output with - amount = parent_value with a fee deducted. - Return tuple (CTransaction object, raw hex, nValue, scriptPubKey of the output created). - """ - node = self.nodes[0] - inputs = [{"txid": parent_txid, "vout": n}] - my_value = parent_value - Decimal("0.0001") - outputs = {self.address : my_value} - rawtx = node.createrawtransaction(inputs, outputs) - prevtxs = [{ - "txid": parent_txid, - "vout": n, - "scriptPubKey": parent_locking_script, - "amount": parent_value, - }] if parent_locking_script else None - signedtx = node.signrawtransactionwithkey(hexstring=rawtx, privkeys=self.privkeys, prevtxs=prevtxs) - assert signedtx["complete"] - tx = tx_from_hex(signedtx["hex"]) - return (tx, signedtx["hex"], my_value, tx.vout[0].scriptPubKey.hex()) def test_independent(self): self.log.info("Test multiple independent transactions in a package") @@ -148,20 +133,7 @@ class RPCPackagesTest(BitcoinTestFramework): def test_chain(self): node = self.nodes[0] first_coin = self.coins.pop() - - # Chain of 25 transactions - parent_locking_script = None - txid = first_coin["txid"] - chain_hex = [] - chain_txns = [] - value = first_coin["amount"] - - for _ in range(25): - (tx, txhex, value, parent_locking_script) = self.chain_transaction(txid, value, 0, parent_locking_script) - txid = tx.rehash() - chain_hex.append(txhex) - chain_txns.append(tx) - + (chain_hex, chain_txns) = create_raw_chain(node, first_coin, self.address, self.privkeys) self.log.info("Check that testmempoolaccept requires packages to be sorted by dependency") assert_equal(node.testmempoolaccept(rawtxs=chain_hex[::-1]), [{"txid": tx.rehash(), "wtxid": tx.getwtxid(), "package-error": "package-not-sorted"} for tx in chain_txns[::-1]]) @@ -201,7 +173,7 @@ class RPCPackagesTest(BitcoinTestFramework): child_value = value - Decimal("0.0001") # Child A - (_, tx_child_a_hex, _, _) = self.chain_transaction(parent_txid, child_value, 0, parent_locking_script_a) + (_, tx_child_a_hex, _, _) = make_chain(node, self.address, self.privkeys, parent_txid, child_value, 0, parent_locking_script_a) assert not node.testmempoolaccept([tx_child_a_hex])[0]["allowed"] # Child B @@ -226,19 +198,6 @@ class RPCPackagesTest(BitcoinTestFramework): node.sendrawtransaction(rawtx) assert_equal(testres_single, testres_multiple_ab) - def create_child_with_parents(self, parents_tx, values, locking_scripts): - """Creates a transaction that spends the first output of each parent in parents_tx.""" - num_parents = len(parents_tx) - total_value = sum(values) - inputs = [{"txid": tx.rehash(), "vout": 0} for tx in parents_tx] - outputs = {self.address : total_value - num_parents * Decimal("0.0001")} - rawtx_child = self.nodes[0].createrawtransaction(inputs, outputs) - prevtxs = [] - for i in range(num_parents): - prevtxs.append({"txid": parents_tx[i].rehash(), "vout": 0, "scriptPubKey": locking_scripts[i], "amount": values[i]}) - signedtx_child = self.nodes[0].signrawtransactionwithkey(hexstring=rawtx_child, privkeys=self.privkeys, prevtxs=prevtxs) - assert signedtx_child["complete"] - return signedtx_child["hex"] def test_multiple_parents(self): node = self.nodes[0] @@ -253,12 +212,12 @@ class RPCPackagesTest(BitcoinTestFramework): for _ in range(num_parents): parent_coin = self.coins.pop() value = parent_coin["amount"] - (tx, txhex, value, parent_locking_script) = self.chain_transaction(parent_coin["txid"], value) + (tx, txhex, value, parent_locking_script) = make_chain(node, self.address, self.privkeys, parent_coin["txid"], value) package_hex.append(txhex) parents_tx.append(tx) values.append(value) parent_locking_scripts.append(parent_locking_script) - child_hex = self.create_child_with_parents(parents_tx, values, parent_locking_scripts) + child_hex = create_child_with_parents(node, self.address, self.privkeys, parents_tx, values, parent_locking_scripts) # Package accept should work with the parents in any order (as long as parents come before child) for _ in range(10): random.shuffle(package_hex) diff --git a/test/functional/rpc_signmessage.py b/test/functional/rpc_signmessagewithprivkey.py index 1c71732a61..95beba8730 100755 --- a/test/functional/rpc_signmessage.py +++ b/test/functional/rpc_signmessagewithprivkey.py @@ -2,7 +2,7 @@ # Copyright (c) 2016-2019 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -"""Test RPC commands for signing and verifying messages.""" +"""Test RPC commands for signing messages with private key.""" from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( @@ -10,14 +10,10 @@ from test_framework.util import ( assert_raises_rpc_error, ) -class SignMessagesTest(BitcoinTestFramework): +class SignMessagesWithPrivTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 - self.extra_args = [["-addresstype=legacy"]] - - def skip_test_if_missing_module(self): - self.skip_if_no_wallet() def run_test(self): message = 'This is just a test message' @@ -30,33 +26,20 @@ class SignMessagesTest(BitcoinTestFramework): assert_equal(expected_signature, signature) assert self.nodes[0].verifymessage(address, signature, message) - self.log.info('test signing with an address with wallet') - address = self.nodes[0].getnewaddress() - signature = self.nodes[0].signmessage(address, message) - assert self.nodes[0].verifymessage(address, signature, message) - - self.log.info('test verifying with another address should not work') - other_address = self.nodes[0].getnewaddress() - other_signature = self.nodes[0].signmessage(other_address, message) - assert not self.nodes[0].verifymessage(other_address, signature, message) - assert not self.nodes[0].verifymessage(address, other_signature, message) - self.log.info('test parameter validity and error codes') - # signmessage(withprivkey) have two required parameters + # signmessagewithprivkey has two required parameters for num_params in [0, 1, 3, 4, 5]: param_list = ["dummy"]*num_params assert_raises_rpc_error(-1, "signmessagewithprivkey", self.nodes[0].signmessagewithprivkey, *param_list) - assert_raises_rpc_error(-1, "signmessage", self.nodes[0].signmessage, *param_list) # verifymessage has three required parameters for num_params in [0, 1, 2, 4, 5]: param_list = ["dummy"]*num_params assert_raises_rpc_error(-1, "verifymessage", self.nodes[0].verifymessage, *param_list) # invalid key or address provided assert_raises_rpc_error(-5, "Invalid private key", self.nodes[0].signmessagewithprivkey, "invalid_key", message) - assert_raises_rpc_error(-5, "Invalid address", self.nodes[0].signmessage, "invalid_addr", message) assert_raises_rpc_error(-5, "Invalid address", self.nodes[0].verifymessage, "invalid_addr", signature, message) # malformed signature provided - assert_raises_rpc_error(-3, "Malformed base64 encoding", self.nodes[0].verifymessage, self.nodes[0].getnewaddress(), "invalid_sig", message) + assert_raises_rpc_error(-3, "Malformed base64 encoding", self.nodes[0].verifymessage, 'mpLQjfK79b7CCV4VMJWEWAj5Mpx8Up5zxB', "invalid_sig", message) if __name__ == '__main__': - SignMessagesTest().main() + SignMessagesWithPrivTest().main() diff --git a/test/functional/test_framework/bdb.py b/test/functional/test_framework/bdb.py index 97b9c1d6d0..d623bcdf6e 100644 --- a/test/functional/test_framework/bdb.py +++ b/test/functional/test_framework/bdb.py @@ -24,7 +24,6 @@ transactions. `db_dump -da wallet.dat` is useful to see the data in a wallet.dat BDB file """ -import binascii import struct # Important constants @@ -96,7 +95,7 @@ def dump_meta_page(page): metadata['key_count'] = key_count metadata['record_count'] = record_count metadata['flags'] = flags - metadata['uid'] = binascii.hexlify(uid) + metadata['uid'] = uid.hex().encode() assert magic == BTREE_MAGIC, 'bdb magic does not match bdb btree magic' assert pg_type == BTREE_META, 'Metadata page is not a btree metadata page' @@ -110,8 +109,9 @@ def dump_meta_page(page): metadata['re_pad'] = re_pad metadata['root'] = root metadata['crypto_magic'] = crypto_magic - metadata['iv'] = binascii.hexlify(iv) - metadata['chksum'] = binascii.hexlify(chksum) + metadata['iv'] = iv.hex().encode() + metadata['chksum'] = chksum.hex().encode() + return metadata # Given the dict from dump_leaf_page, get the key-value pairs and put them into a dict diff --git a/test/functional/test_framework/blocktools.py b/test/functional/test_framework/blocktools.py index a88a119e6f..11d0ab40d5 100644 --- a/test/functional/test_framework/blocktools.py +++ b/test/functional/test_framework/blocktools.py @@ -4,7 +4,6 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Utilities for manipulating blocks and transactions.""" -from binascii import a2b_hex import struct import time import unittest @@ -55,6 +54,7 @@ TIME_GENESIS_BLOCK = 1296688602 COINBASE_MATURITY = 100 # Soft-fork activation heights +DERSIG_HEIGHT = 102 # BIP 66 CLTV_HEIGHT = 1351 CSV_ACTIVATION_HEIGHT = 432 @@ -74,7 +74,7 @@ def create_block(hashprev=None, coinbase=None, ntime=None, *, version=None, tmpl block.nTime = ntime or tmpl.get('curtime') or int(time.time() + 600) block.hashPrevBlock = hashprev or int(tmpl['previousblockhash'], 0x10) if tmpl and not tmpl.get('bits') is None: - block.nBits = struct.unpack('>I', a2b_hex(tmpl['bits']))[0] + block.nBits = struct.unpack('>I', bytes.fromhex(tmpl['bits']))[0] else: block.nBits = 0x207fffff # difficulty retargeting is disabled in REGTEST chainparams if coinbase is None: diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py index 6e57107f86..65d90f8448 100755 --- a/test/functional/test_framework/messages.py +++ b/test/functional/test_framework/messages.py @@ -19,7 +19,6 @@ Classes use __slots__ to ensure extraneous attributes aren't accidentally added by tests, compromising their intended effect. """ from base64 import b32decode, b32encode -from codecs import encode import copy import hashlib from io import BytesIO @@ -681,7 +680,7 @@ class CBlockHeader: r += struct.pack("<I", self.nBits) r += struct.pack("<I", self.nNonce) self.sha256 = uint256_from_str(hash256(r)) - self.hash = encode(hash256(r)[::-1], 'hex_codec').decode('ascii') + self.hash = hash256(r)[::-1].hex() def rehash(self): self.sha256 = None diff --git a/test/functional/test_framework/netutil.py b/test/functional/test_framework/netutil.py index 5dc723c1d5..b5f78e0cf3 100644 --- a/test/functional/test_framework/netutil.py +++ b/test/functional/test_framework/netutil.py @@ -12,7 +12,6 @@ import socket import struct import array import os -from binascii import unhexlify # STATE_ESTABLISHED = '01' # STATE_SYN_SENT = '02' @@ -44,7 +43,7 @@ def _remove_empty(array): def _convert_ip_port(array): host,port = array.split(':') # convert host from mangled-per-four-bytes form as used by kernel - host = unhexlify(host) + host = bytes.fromhex(host) host_out = '' for x in range(0, len(host) // 4): (val,) = struct.unpack('=I', host[x*4:(x+1)*4]) diff --git a/test/functional/test_framework/wallet.py b/test/functional/test_framework/wallet.py index 609553c6d0..ba5b95f930 100644 --- a/test/functional/test_framework/wallet.py +++ b/test/functional/test_framework/wallet.py @@ -4,8 +4,10 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. """A limited-functionality wallet, which may replace a real wallet in tests""" +from copy import deepcopy from decimal import Decimal from enum import Enum +from random import choice from typing import Optional from test_framework.address import ADDRESS_BCRT1_P2WSH_OP_TRUE from test_framework.key import ECKey @@ -16,6 +18,7 @@ from test_framework.messages import ( CTxIn, CTxInWitness, CTxOut, + tx_from_hex, ) from test_framework.script import ( CScript, @@ -27,9 +30,11 @@ from test_framework.script import ( ) from test_framework.util import ( assert_equal, + assert_greater_than_or_equal, satoshi_round, ) +DEFAULT_FEE = Decimal("0.0001") class MiniWalletMode(Enum): """Determines the transaction type the MiniWallet is creating and spending. @@ -176,3 +181,75 @@ class MiniWallet: def sendrawtransaction(self, *, from_node, tx_hex): from_node.sendrawtransaction(tx_hex) self.scan_tx(from_node.decoderawtransaction(tx_hex)) + +def make_chain(node, address, privkeys, parent_txid, parent_value, n=0, parent_locking_script=None, fee=DEFAULT_FEE): + """Build a transaction that spends parent_txid.vout[n] and produces one output with + amount = parent_value with a fee deducted. + Return tuple (CTransaction object, raw hex, nValue, scriptPubKey of the output created). + """ + inputs = [{"txid": parent_txid, "vout": n}] + my_value = parent_value - fee + outputs = {address : my_value} + rawtx = node.createrawtransaction(inputs, outputs) + prevtxs = [{ + "txid": parent_txid, + "vout": n, + "scriptPubKey": parent_locking_script, + "amount": parent_value, + }] if parent_locking_script else None + signedtx = node.signrawtransactionwithkey(hexstring=rawtx, privkeys=privkeys, prevtxs=prevtxs) + assert signedtx["complete"] + tx = tx_from_hex(signedtx["hex"]) + return (tx, signedtx["hex"], my_value, tx.vout[0].scriptPubKey.hex()) + +def create_child_with_parents(node, address, privkeys, parents_tx, values, locking_scripts, fee=DEFAULT_FEE): + """Creates a transaction that spends the first output of each parent in parents_tx.""" + num_parents = len(parents_tx) + total_value = sum(values) + inputs = [{"txid": tx.rehash(), "vout": 0} for tx in parents_tx] + outputs = {address : total_value - fee} + rawtx_child = node.createrawtransaction(inputs, outputs) + prevtxs = [] + for i in range(num_parents): + prevtxs.append({"txid": parents_tx[i].rehash(), "vout": 0, "scriptPubKey": locking_scripts[i], "amount": values[i]}) + signedtx_child = node.signrawtransactionwithkey(hexstring=rawtx_child, privkeys=privkeys, prevtxs=prevtxs) + assert signedtx_child["complete"] + return signedtx_child["hex"] + +def create_raw_chain(node, first_coin, address, privkeys, chain_length=25): + """Helper function: create a "chain" of chain_length transactions. The nth transaction in the + chain is a child of the n-1th transaction and parent of the n+1th transaction. + """ + parent_locking_script = None + txid = first_coin["txid"] + chain_hex = [] + chain_txns = [] + value = first_coin["amount"] + + for _ in range(chain_length): + (tx, txhex, value, parent_locking_script) = make_chain(node, address, privkeys, txid, value, 0, parent_locking_script) + txid = tx.rehash() + chain_hex.append(txhex) + chain_txns.append(tx) + + return (chain_hex, chain_txns) + +def bulk_transaction(tx, node, target_weight, privkeys, prevtxs=None): + """Pad a transaction with extra outputs until it reaches a target weight (or higher). + returns CTransaction object + """ + tx_heavy = deepcopy(tx) + assert_greater_than_or_equal(target_weight, tx_heavy.get_weight()) + while tx_heavy.get_weight() < target_weight: + random_spk = "6a4d0200" # OP_RETURN OP_PUSH2 512 bytes + for _ in range(512*2): + random_spk += choice("0123456789ABCDEF") + tx_heavy.vout.append(CTxOut(0, bytes.fromhex(random_spk))) + # Re-sign the transaction + if privkeys: + signed = node.signrawtransactionwithkey(tx_heavy.serialize().hex(), privkeys, prevtxs) + return tx_from_hex(signed["hex"]) + # OP_TRUE + tx_heavy.wit.vtxinwit = [CTxInWitness()] + tx_heavy.wit.vtxinwit[0].scriptWitness.stack = [CScript([OP_TRUE])] + return tx_heavy diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index fecf52d53a..d6d676da1f 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -218,13 +218,15 @@ BASE_SCRIPTS = [ 'rpc_createmultisig.py --legacy-wallet', 'rpc_createmultisig.py --descriptors', 'rpc_packages.py', + 'mempool_package_limits.py', 'feature_versionbits_warning.py', 'rpc_preciousblock.py', 'wallet_importprunedfunds.py --legacy-wallet', 'wallet_importprunedfunds.py --descriptors', 'p2p_leak_tx.py', 'p2p_eviction.py', - 'rpc_signmessage.py', + 'wallet_signmessagewithaddress.py', + 'rpc_signmessagewithprivkey.py', 'rpc_generateblock.py', 'rpc_generate.py', 'wallet_balance.py --legacy-wallet', diff --git a/test/functional/wallet_backup.py b/test/functional/wallet_backup.py index 05a0ef0ea1..c7a983556d 100755 --- a/test/functional/wallet_backup.py +++ b/test/functional/wallet_backup.py @@ -111,6 +111,18 @@ class WalletBackupTest(BitcoinTestFramework): os.remove(os.path.join(self.nodes[1].datadir, self.chain, 'wallets', self.default_wallet_name, self.wallet_data_filename)) os.remove(os.path.join(self.nodes[2].datadir, self.chain, 'wallets', self.default_wallet_name, self.wallet_data_filename)) + def restore_nonexistent_wallet(self): + node = self.nodes[3] + nonexistent_wallet_file = os.path.join(self.nodes[0].datadir, 'nonexistent_wallet.bak') + wallet_name = "res0" + assert_raises_rpc_error(-8, "Backup file does not exist", node.restorewallet, wallet_name, nonexistent_wallet_file) + + def restore_wallet_existent_name(self): + node = self.nodes[3] + wallet_file = os.path.join(self.nodes[0].datadir, 'wallet.bak') + wallet_name = "res0" + assert_raises_rpc_error(-8, "Wallet name already exists.", node.restorewallet, wallet_name, wallet_file) + def init_three(self): self.init_wallet(0) self.init_wallet(1) @@ -169,26 +181,27 @@ class WalletBackupTest(BitcoinTestFramework): ## # Test restoring spender wallets from backups ## - self.log.info("Restoring using wallet.dat") - self.stop_three() - self.erase_three() + self.log.info("Restoring wallets on node 3 using backup files") - # Start node2 with no chain - shutil.rmtree(os.path.join(self.nodes[2].datadir, self.chain, 'blocks')) - shutil.rmtree(os.path.join(self.nodes[2].datadir, self.chain, 'chainstate')) + self.restore_nonexistent_wallet() - # Restore wallets from backup - shutil.copyfile(os.path.join(self.nodes[0].datadir, 'wallet.bak'), os.path.join(self.nodes[0].datadir, self.chain, 'wallets', self.default_wallet_name, self.wallet_data_filename)) - shutil.copyfile(os.path.join(self.nodes[1].datadir, 'wallet.bak'), os.path.join(self.nodes[1].datadir, self.chain, 'wallets', self.default_wallet_name, self.wallet_data_filename)) - shutil.copyfile(os.path.join(self.nodes[2].datadir, 'wallet.bak'), os.path.join(self.nodes[2].datadir, self.chain, 'wallets', self.default_wallet_name, self.wallet_data_filename)) + backup_file_0 = os.path.join(self.nodes[0].datadir, 'wallet.bak') + backup_file_1 = os.path.join(self.nodes[1].datadir, 'wallet.bak') + backup_file_2 = os.path.join(self.nodes[2].datadir, 'wallet.bak') - self.log.info("Re-starting nodes") - self.start_three() - self.sync_blocks() + self.nodes[3].restorewallet("res0", backup_file_0) + self.nodes[3].restorewallet("res1", backup_file_1) + self.nodes[3].restorewallet("res2", backup_file_2) + + res0_rpc = self.nodes[3].get_wallet_rpc("res0") + res1_rpc = self.nodes[3].get_wallet_rpc("res1") + res2_rpc = self.nodes[3].get_wallet_rpc("res2") + + assert_equal(res0_rpc.getbalance(), balance0) + assert_equal(res1_rpc.getbalance(), balance1) + assert_equal(res2_rpc.getbalance(), balance2) - assert_equal(self.nodes[0].getbalance(), balance0) - assert_equal(self.nodes[1].getbalance(), balance1) - assert_equal(self.nodes[2].getbalance(), balance2) + self.restore_wallet_existent_name() if not self.options.descriptors: self.log.info("Restoring using dumped wallet") diff --git a/test/functional/wallet_listdescriptors.py b/test/functional/wallet_listdescriptors.py index c2565d84f6..221f5262d9 100755 --- a/test/functional/wallet_listdescriptors.py +++ b/test/functional/wallet_listdescriptors.py @@ -72,11 +72,39 @@ class ListDescriptorsTest(BitcoinTestFramework): ], } assert_equal(expected, wallet.listdescriptors()) + assert_equal(expected, wallet.listdescriptors(False)) + + self.log.info('Test list private descriptors') + expected_private = { + 'wallet_name': 'w2', + 'descriptors': [ + {'desc': descsum_create('wpkh(' + xprv + hardened_path + '/0/*)'), + 'timestamp': 1296688602, + 'active': False, + 'range': [0, 0], + 'next': 0}, + ], + } + assert_equal(expected_private, wallet.listdescriptors(True)) self.log.info("Test listdescriptors with encrypted wallet") wallet.encryptwallet("pass") assert_equal(expected, wallet.listdescriptors()) + self.log.info('Test list private descriptors with encrypted wallet') + assert_raises_rpc_error(-13, 'Please enter the wallet passphrase with walletpassphrase first.', wallet.listdescriptors, True) + wallet.walletpassphrase(passphrase="pass", timeout=1000000) + assert_equal(expected_private, wallet.listdescriptors(True)) + + self.log.info('Test list private descriptors with watch-only wallet') + node.createwallet(wallet_name='watch-only', descriptors=True, disable_private_keys=True) + watch_only_wallet = node.get_wallet_rpc('watch-only') + watch_only_wallet.importdescriptors([{ + 'desc': descsum_create('wpkh(' + xpub_acc + ')'), + 'timestamp': 1296688602, + }]) + assert_raises_rpc_error(-4, 'Can\'t get descriptor string', watch_only_wallet.listdescriptors, True) + self.log.info('Test non-active non-range combo descriptor') node.createwallet(wallet_name='w4', blank=True, descriptors=True) wallet = node.get_wallet_rpc('w4') diff --git a/test/functional/wallet_signmessagewithaddress.py b/test/functional/wallet_signmessagewithaddress.py new file mode 100755 index 0000000000..bf6f95e3f1 --- /dev/null +++ b/test/functional/wallet_signmessagewithaddress.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +# Copyright (c) 2016-2019 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test Wallet commands for signing and verifying messages.""" + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_raises_rpc_error, +) + +class SignMessagesWithAddressTest(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 1 + self.extra_args = [["-addresstype=legacy"]] + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + + def run_test(self): + message = 'This is just a test message' + + self.log.info('test signing with an address with wallet') + address = self.nodes[0].getnewaddress() + signature = self.nodes[0].signmessage(address, message) + assert self.nodes[0].verifymessage(address, signature, message) + + self.log.info('test verifying with another address should not work') + other_address = self.nodes[0].getnewaddress() + other_signature = self.nodes[0].signmessage(other_address, message) + assert not self.nodes[0].verifymessage(other_address, signature, message) + assert not self.nodes[0].verifymessage(address, other_signature, message) + + self.log.info('test parameter validity and error codes') + # signmessage has two required parameters + for num_params in [0, 1, 3, 4, 5]: + param_list = ["dummy"]*num_params + assert_raises_rpc_error(-1, "signmessage", self.nodes[0].signmessage, *param_list) + # invalid key or address provided + assert_raises_rpc_error(-5, "Invalid address", self.nodes[0].signmessage, "invalid_addr", message) + + +if __name__ == '__main__': + SignMessagesWithAddressTest().main() diff --git a/test/util/bitcoin-util-test.py b/test/util/bitcoin-util-test.py index 7b1cc2b031..aa8fd6eee5 100755 --- a/test/util/bitcoin-util-test.py +++ b/test/util/bitcoin-util-test.py @@ -10,7 +10,6 @@ Runs automatically during `make check`. Can also be run manually.""" import argparse -import binascii import configparser import difflib import json @@ -167,7 +166,7 @@ def parse_output(a, fmt): if fmt == 'json': # json: compare parsed data return json.loads(a) elif fmt == 'hex': # hex: parse and compare binary data - return binascii.a2b_hex(a.strip()) + return bytes.fromhex(a.strip()) else: raise NotImplementedError("Don't know how to compare %s" % fmt) |