diff --git a/Makefile.am b/Makefile.am index ea2cba9fd2fa..6f3dfaa7500a 100644 --- a/Makefile.am +++ b/Makefile.am @@ -187,7 +187,7 @@ $(COV_TOOL_WRAPPER): @chmod +x $(COV_TOOL_WRAPPER) baseline.info: $(COV_TOOL_WRAPPER) - $(LCOV) -c -i -d $(abs_builddir)/src -o $@ + $(LCOV) $(LCOV_OPTS) -c -i -d $(abs_builddir)/src -o $@ baseline_filtered.info: baseline.info $(abs_builddir)/contrib/filter-lcov.py $(LCOV_FILTER_PATTERN) $< $@ @@ -221,13 +221,13 @@ functional_test_filtered.info: functional_test.info $(LCOV) -a $@ $(LCOV_OPTS) -o $@ fuzz_coverage.info: fuzz_filtered.info - $(LCOV) -a $(LCOV_OPTS) baseline_filtered.info -a fuzz_filtered.info -o $@ | $(GREP) "\%" | $(AWK) '{ print substr($$3,2,50) "/" $$5 }' > coverage_percent.txt + $(LCOV) $(LCOV_OPTS) -a baseline_filtered.info -a fuzz_filtered.info -o $@ | $(GREP) "\%" | $(AWK) '{ print substr($$3,2,50) "/" $$5 }' > coverage_percent.txt test_dash_coverage.info: baseline_filtered.info test_dash_filtered.info - $(LCOV) -a $(LCOV_OPTS) baseline_filtered.info -a test_dash_filtered.info -o $@ + $(LCOV) $(LCOV_OPTS) -a baseline_filtered.info -a test_dash_filtered.info -o $@ total_coverage.info: test_dash_filtered.info functional_test_filtered.info - $(LCOV) -a $(LCOV_OPTS) baseline_filtered.info -a test_dash_filtered.info -a functional_test_filtered.info -o $@ | $(GREP) "\%" | $(AWK) '{ print substr($$3,2,50) "/" $$5 }' > coverage_percent.txt + $(LCOV) $(LCOV_OPTS) -a baseline_filtered.info -a test_dash_filtered.info -a functional_test_filtered.info -o $@ | $(GREP) "\%" | $(AWK) '{ print substr($$3,2,50) "/" $$5 }' > coverage_percent.txt fuzz.coverage/.dirstamp: fuzz_coverage.info $(GENHTML) -s $(LCOV_OPTS) $< -o $(@D) diff --git a/ci/lint/04_install.sh b/ci/lint/04_install.sh index 9ca5ebe37216..eb0827a61bf2 100755 --- a/ci/lint/04_install.sh +++ b/ci/lint/04_install.sh @@ -33,6 +33,17 @@ if [ -z "${SKIP_PYTHON_INSTALL}" ]; then python3 --version fi +export LINT_RUNNER_PATH="/lint_test_runner" +if [ ! -d "${LINT_RUNNER_PATH}" ]; then + ${CI_RETRY_EXE} apt-get install -y cargo + ( + cd ./test/lint/test_runner || exit 1 + cargo build + mkdir -p "${LINT_RUNNER_PATH}" + mv target/debug/test_runner "${LINT_RUNNER_PATH}" + ) +fi + # NOTE: BUMP ALSO contrib/containers/ci/ci-slim.Dockerfile ${CI_RETRY_EXE} pip3 install codespell==2.2.1 ${CI_RETRY_EXE} pip3 install flake8==5.0.4 diff --git a/ci/lint/06_script.sh b/ci/lint/06_script.sh index 93e6aeaeac93..59f70e98fdfb 100755 --- a/ci/lint/06_script.sh +++ b/ci/lint/06_script.sh @@ -27,6 +27,7 @@ test/lint/git-subtree-check.sh src/crypto/ctaes test/lint/git-subtree-check.sh src/secp256k1 test/lint/git-subtree-check.sh src/minisketch test/lint/git-subtree-check.sh src/leveldb +RUST_BACKTRACE=1 "${LINT_RUNNER_PATH}/test_runner" test/lint/check-doc.py test/lint/all-lint.py diff --git a/ci/lint/docker-entrypoint.sh b/ci/lint/docker-entrypoint.sh index 3fdbbb0761c0..4ef246697d0d 100755 --- a/ci/lint/docker-entrypoint.sh +++ b/ci/lint/docker-entrypoint.sh @@ -5,6 +5,8 @@ export LC_ALL=C # of the mounted bitcoin src dir. git config --global --add safe.directory /bitcoin +export LINT_RUNNER_PATH="/lint_test_runner" + if [ -z "$1" ]; then LOCAL_BRANCH=1 bash -ic "./ci/lint/06_script.sh" else diff --git a/ci/test/00_setup_env_native_fuzz.sh b/ci/test/00_setup_env_native_fuzz.sh index 6d405af70d47..f04d825705cd 100755 --- a/ci/test/00_setup_env_native_fuzz.sh +++ b/ci/test/00_setup_env_native_fuzz.sh @@ -16,3 +16,4 @@ export RUN_FUNCTIONAL_TESTS=false export RUN_FUZZ_TESTS=true export GOAL="install" export BITCOIN_CONFIG="--enable-zmq --enable-fuzz --with-sanitizers=fuzzer,address,undefined,integer CC='clang-19 -ftrivial-auto-var-init=pattern' CXX='clang++-19 -ftrivial-auto-var-init=pattern'" +export LLVM_SYMBOLIZER_PATH="/usr/bin/llvm-symbolizer-17" diff --git a/contrib/devtools/circular-dependencies.py b/contrib/devtools/circular-dependencies.py index e939ec6d45ba..373d5fd69b18 100755 --- a/contrib/devtools/circular-dependencies.py +++ b/contrib/devtools/circular-dependencies.py @@ -6,7 +6,6 @@ import sys import re from multiprocess import Pool # type: ignore[import] -from typing import Dict, List, Set MAPPING = { 'core_read.cpp': 'core_io.cpp', @@ -35,7 +34,7 @@ def module_name(path): if __name__=="__main__": files = dict() - deps: Dict[str, Set[str]] = dict() + deps: dict[str, set[str]] = dict() RE = re.compile("^#include <(.*)>") @@ -49,7 +48,7 @@ def handle_module(arg): def handle_module2(module): # Build the transitive closure of dependencies of module - closure: Dict[str, List[str]] = dict() + closure: dict[str, list[str]] = dict() for dep in deps[module]: closure[dep] = [] while True: diff --git a/contrib/devtools/utils.py b/contrib/devtools/utils.py index 68ad1c3aba19..8b4c67c6c0ce 100755 --- a/contrib/devtools/utils.py +++ b/contrib/devtools/utils.py @@ -8,10 +8,9 @@ import shutil import sys import os -from typing import List -def determine_wellknown_cmd(envvar, progname) -> List[str]: +def determine_wellknown_cmd(envvar, progname) -> list[str]: maybe_env = os.getenv(envvar) maybe_which = shutil.which(progname) if maybe_env: diff --git a/contrib/guix/security-check.py b/contrib/guix/security-check.py index 0a5d1bbfa6f5..3c02b2ab4f01 100755 --- a/contrib/guix/security-check.py +++ b/contrib/guix/security-check.py @@ -13,7 +13,6 @@ ''' import re import sys -from typing import List import lief @@ -277,7 +276,7 @@ def check_MACHO_BRANCH_PROTECTION(binary) -> bool: arch = binary.abstract.header.architecture binary.concrete - failed: List[str] = [] + failed: list[str] = [] for (name, func) in CHECKS[etype][arch]: if not func(binary): failed.append(name) diff --git a/contrib/guix/symbol-check.py b/contrib/guix/symbol-check.py index bca9029b24d8..8272efa305af 100755 --- a/contrib/guix/symbol-check.py +++ b/contrib/guix/symbol-check.py @@ -11,7 +11,6 @@ find ../path/to/guix/binaries -type f -executable | xargs python3 contrib/guix/symbol-check.py ''' import sys -from typing import Dict, List import lief @@ -50,7 +49,7 @@ # Expected linker-loader names can be found here: # https://sourceware.org/glibc/wiki/ABIList?action=recall&rev=16 -ELF_INTERPRETER_NAMES: Dict[lief.ELF.ARCH, Dict[lief.ENDIANNESS, str]] = { +ELF_INTERPRETER_NAMES: dict[lief.ELF.ARCH, dict[lief.ENDIANNESS, str]] = { lief.ELF.ARCH.x86_64: { lief.ENDIANNESS.LITTLE: "/lib64/ld-linux-x86-64.so.2", }, @@ -69,7 +68,7 @@ }, } -ELF_ABIS: Dict[lief.ELF.ARCH, Dict[lief.ENDIANNESS, List[int]]] = { +ELF_ABIS: dict[lief.ELF.ARCH, dict[lief.ENDIANNESS, list[int]]] = { lief.ELF.ARCH.x86_64: { lief.ENDIANNESS.LITTLE: [3,2,0], }, @@ -303,7 +302,7 @@ def check_ELF_ABI(binary) -> bool: binary = lief.parse(filename) etype = binary.format - failed: List[str] = [] + failed: list[str] = [] for (name, func) in CHECKS[etype]: if not func(binary): failed.append(name) diff --git a/contrib/macdeploy/macdeployqtplus b/contrib/macdeploy/macdeployqtplus index f86b6f760f66..5183ce57255c 100755 --- a/contrib/macdeploy/macdeployqtplus +++ b/contrib/macdeploy/macdeployqtplus @@ -20,7 +20,7 @@ import sys, re, os, platform, shutil, stat, subprocess, os.path from argparse import ArgumentParser from pathlib import Path from subprocess import PIPE, run -from typing import List, Optional +from typing import Optional # This is ported from the original macdeployqt with modifications @@ -181,7 +181,7 @@ class DeploymentInfo(object): return True return False -def getFrameworks(binaryPath: str, verbose: int) -> List[FrameworkInfo]: +def getFrameworks(binaryPath: str, verbose: int) -> list[FrameworkInfo]: objdump = os.getenv("OBJDUMP", "objdump") if verbose: print(f"Inspecting with {objdump}: {binaryPath}") @@ -285,7 +285,7 @@ def copyFramework(framework: FrameworkInfo, path: str, verbose: int) -> Optional return toPath -def deployFrameworks(frameworks: List[FrameworkInfo], bundlePath: str, binaryPath: str, strip: bool, verbose: int, deploymentInfo: Optional[DeploymentInfo] = None) -> DeploymentInfo: +def deployFrameworks(frameworks: list[FrameworkInfo], bundlePath: str, binaryPath: str, strip: bool, verbose: int, deploymentInfo: Optional[DeploymentInfo] = None) -> DeploymentInfo: if deploymentInfo is None: deploymentInfo = DeploymentInfo() diff --git a/contrib/message-capture/message-capture-parser.py b/contrib/message-capture/message-capture-parser.py index b416c11302bb..0f0dc65a12f9 100755 --- a/contrib/message-capture/message-capture-parser.py +++ b/contrib/message-capture/message-capture-parser.py @@ -11,7 +11,7 @@ from io import BytesIO import json from pathlib import Path -from typing import Any, List, Optional +from typing import Any, Optional sys.path.append(os.path.join(os.path.dirname(__file__), '../../test/functional')) @@ -92,7 +92,7 @@ def to_jsonable(obj: Any) -> Any: return obj -def process_file(path: str, messages: List[Any], recv: bool, progress_bar: Optional[ProgressBar]) -> None: +def process_file(path: str, messages: list[Any], recv: bool, progress_bar: Optional[ProgressBar]) -> None: with open(path, 'rb') as f_in: if progress_bar: bytes_read = 0 @@ -189,7 +189,7 @@ def main(): output = Path.cwd() / Path(args.output) if args.output else False use_progress_bar = (not args.no_progress_bar) and sys.stdout.isatty() - messages = [] # type: List[Any] + messages = [] # type: list[Any] if use_progress_bar: total_size = sum(capture.stat().st_size for capture in capturepaths) progress_bar = ProgressBar(total_size) diff --git a/contrib/seeds/makeseeds.py b/contrib/seeds/makeseeds.py index 17e06125ef4d..b5e1f10e6955 100755 --- a/contrib/seeds/makeseeds.py +++ b/contrib/seeds/makeseeds.py @@ -13,7 +13,7 @@ import collections import json import multiprocessing -from typing import List, Dict, Union +from typing import Union NSEEDS=512 @@ -76,28 +76,28 @@ def parseip(ip_in: str) -> Union[dict, None]: "sortkey": sortkey } -def filtermulticollateralhash(mns : List[Dict]) -> List[Dict]: +def filtermulticollateralhash(mns : list[dict]) -> list[dict]: '''Filter out MNs sharing the same collateral hash''' hist = collections.defaultdict(list) for mn in mns: hist[mn['collateralHash']].append(mn) return [mn for mn in mns if len(hist[mn['collateralHash']]) == 1] -def filtermulticollateraladdress(mns : List[Dict]) -> List[Dict]: +def filtermulticollateraladdress(mns : list[dict]) -> list[dict]: '''Filter out MNs sharing the same collateral address''' hist = collections.defaultdict(list) for mn in mns: hist[mn['collateralAddress']].append(mn) return [mn for mn in mns if len(hist[mn['collateralAddress']]) == 1] -def filtermultipayoutaddress(mns : List[Dict]) -> List[Dict]: +def filtermultipayoutaddress(mns : list[dict]) -> list[dict]: '''Filter out MNs sharing the same payout address''' hist = collections.defaultdict(list) for mn in mns: hist[mn['state']['payoutAddress']].append(mn) return [mn for mn in mns if len(hist[mn['state']['payoutAddress']]) == 1] -def resolveasn(resolver, ip : Dict) -> Union[int, None]: +def resolveasn(resolver, ip : dict) -> Union[int, None]: """ Look up the asn for an `ip` address by querying cymru.com on network `net` (e.g. ipv4 or ipv6). @@ -124,7 +124,7 @@ def resolveasn(resolver, ip : Dict) -> Union[int, None]: return None # Based on Greg Maxwell's seed_filter.py -def filterbyasn(ips: List[Dict], max_per_asn: Dict, max_per_net: int) -> List[Dict]: +def filterbyasn(ips: list[dict], max_per_asn: dict, max_per_net: int) -> list[dict]: """ Prunes `ips` by (a) trimming ips to have at most `max_per_net` ips from each net (e.g. ipv4, ipv6); and (b) trimming ips to have at most `max_per_asn` ips from each asn in each net. @@ -144,8 +144,8 @@ def filterbyasn(ips: List[Dict], max_per_asn: Dict, max_per_net: int) -> List[Di # Filter IPv46 by ASN, and limit to max_per_net per network result = [] - net_count: Dict[str, int] = collections.defaultdict(int) - asn_count: Dict[int, int] = collections.defaultdict(int) + net_count: dict[str, int] = collections.defaultdict(int) + asn_count: dict[int, int] = collections.defaultdict(int) for i, ip in enumerate(ips_ipv46): if i % 10 == 0: @@ -169,9 +169,9 @@ def filterbyasn(ips: List[Dict], max_per_asn: Dict, max_per_net: int) -> List[Di result.extend(ips_onion[0:max_per_net]) return result -def ip_stats(ips: List[Dict]) -> str: +def ip_stats(ips: list[dict]) -> str: """ Format and return pretty string from `ips`. """ - hist: Dict[str, int] = collections.defaultdict(int) + hist: dict[str, int] = collections.defaultdict(int) for ip in ips: if ip is not None: hist[ip['net']] += 1 diff --git a/src/.clang-tidy b/src/.clang-tidy index bb3f4194760c..21592f7a97d3 100644 --- a/src/.clang-tidy +++ b/src/.clang-tidy @@ -1,6 +1,7 @@ Checks: ' -*, bugprone-argument-comment, +bugprone-string-constructor, bugprone-use-after-move, misc-unused-using-decls, modernize-use-default-member-init, diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 04652688dbef..06de1e2237dc 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -489,11 +489,14 @@ SECONDARY: $(QT_QM) $(srcdir)/qt/dashstrings.cpp: FORCE @test -n $(XGETTEXT) || echo "xgettext is required for updating translations" - $(AM_V_GEN) cd $(srcdir); XGETTEXT=$(XGETTEXT) COPYRIGHT_HOLDERS="$(COPYRIGHT_HOLDERS)" $(PYTHON) ../share/qt/extract_strings_qt.py $(libbitcoin_node_a_SOURCES) $(libbitcoin_wallet_a_SOURCES) $(libbitcoin_common_a_SOURCES) $(libbitcoin_zmq_a_SOURCES) $(libbitcoin_consensus_a_SOURCES) $(libbitcoin_util_a_SOURCES) + $(AM_V_GEN) cd $(srcdir); XGETTEXT=$(XGETTEXT) COPYRIGHT_HOLDERS="$(COPYRIGHT_HOLDERS)" $(PYTHON) ../share/qt/extract_strings_qt.py \ + $(libbitcoin_node_a_SOURCES) $(libbitcoin_wallet_a_SOURCES) $(libbitcoin_common_a_SOURCES) \ + $(libbitcoin_zmq_a_SOURCES) $(libbitcoin_consensus_a_SOURCES) $(libbitcoin_util_a_SOURCES) \ + $(BITCOIN_QT_BASE_CPP) $(BITCOIN_QT_WINDOWS_CPP) $(BITCOIN_QT_WALLET_CPP) $(BITCOIN_QT_H) $(BITCOIN_MM) # The resulted dash_en.xlf source file should follow Transifex requirements. # See: https://docs.transifex.com/formats/xliff#how-to-distinguish-between-a-source-file-and-a-translation-file -translate: $(srcdir)/qt/dashstrings.cpp $(QT_FORMS_UI) $(QT_FORMS_UI) $(BITCOIN_QT_BASE_CPP) qt/bitcoin.cpp $(BITCOIN_QT_WINDOWS_CPP) $(BITCOIN_QT_WALLET_CPP) $(BITCOIN_QT_H) $(BITCOIN_MM) +translate: $(srcdir)/qt/dashstrings.cpp $(QT_FORMS_UI) $(QT_FORMS_UI) $(BITCOIN_QT_BASE_CPP) $(BITCOIN_QT_WINDOWS_CPP) $(BITCOIN_QT_WALLET_CPP) $(BITCOIN_QT_H) $(BITCOIN_MM) @test -n $(LUPDATE) || echo "lupdate is required for updating translations" $(AM_V_GEN) QT_SELECT=$(QT_SELECT) $(LUPDATE) -no-obsolete -I $(srcdir) -locations relative $^ -ts $(srcdir)/qt/locale/dash_en.ts @test -n $(LCONVERT) || echo "lconvert is required for updating translations" diff --git a/src/fs.h b/src/fs.h index 270ce6fd1f00..2c344f1a271a 100644 --- a/src/fs.h +++ b/src/fs.h @@ -184,6 +184,7 @@ static inline path PathFromString(const std::string& string) * already exists or is a symlink to an existing directory. * This is a temporary workaround for an issue in libstdc++ that has been fixed * upstream [PR101510]. + * https://gcc.gnu.org/bugzilla/show_bug.cgi?id=101510 */ static inline bool create_directories(const std::filesystem::path& p) { diff --git a/src/test/util/script.h b/src/test/util/script.h index 81f94afec0ea..1a2b1eb806c8 100644 --- a/src/test/util/script.h +++ b/src/test/util/script.h @@ -16,6 +16,18 @@ static const CScript P2SH_OP_TRUE{ << ToByteVector(CScriptID{CScript{} << OP_TRUE}) << OP_EQUAL}; +static const std::vector EMPTY{}; +static const CScript P2WSH_EMPTY{ + CScript{} + << OP_0 + << ToByteVector([] { + uint256 hash; + CSHA256().Write(EMPTY.data(), EMPTY.size()).Finalize(hash.begin()); + return hash; + }())}; +static const std::vector> P2WSH_EMPTY_TRUE_STACK{{static_cast(OP_TRUE)}, {}}; +static const std::vector> P2WSH_EMPTY_TWO_STACK{{static_cast(OP_2)}, {}}; + /** Flags that are not forbidden by an assert in script validation */ bool IsValidFlagCombination(unsigned flags); diff --git a/src/wallet/db.cpp b/src/wallet/db.cpp index 12b0c92dda1f..00778a129015 100644 --- a/src/wallet/db.cpp +++ b/src/wallet/db.cpp @@ -129,9 +129,9 @@ bool IsSQLiteFile(const fs::path& path) file.close(); - // Check the magic, see https://sqlite.org/fileformat2.html + // Check the magic, see https://sqlite.org/fileformat.html std::string magic_str(magic, 16); - if (magic_str != std::string("SQLite format 3", 16)) { + if (magic_str != std::string{"SQLite format 3\000", 16}) { return false; } diff --git a/src/wallet/transaction.cpp b/src/wallet/transaction.cpp index a46846c1d4a3..eb02ca20d0be 100644 --- a/src/wallet/transaction.cpp +++ b/src/wallet/transaction.cpp @@ -4,6 +4,10 @@ #include +#include + +using interfaces::FoundBlock; + namespace wallet { bool CWalletTx::IsEquivalentTo(const CWalletTx& _tx) const { @@ -24,4 +28,25 @@ int64_t CWalletTx::GetTxTime() const int64_t n = nTimeSmart; return n ? n : nTimeReceived; } + +void CWalletTx::updateState(interfaces::Chain& chain) +{ + bool active; + auto lookup_block = [&](const uint256& hash, int& height, TxState& state) { + // If tx block (or conflicting block) was reorged out of chain + // while the wallet was shutdown, change tx status to UNCONFIRMED + // and reset block height, hash, and index. ABANDONED tx don't have + // associated blocks and don't need to be updated. The case where a + // transaction was reorged out while online and then reconfirmed + // while offline is covered by the rescan logic. + if (!chain.findBlock(hash, FoundBlock().inActiveChain(active).height(height)) || !active) { + state = TxStateInactive{}; + } + }; + if (auto* conf = state()) { + lookup_block(conf->confirmed_block_hash, conf->confirmed_block_height, m_state); + } else if (auto* conf = state()) { + lookup_block(conf->conflicting_block_hash, conf->conflicting_block_height, m_state); + } +} } // namespace wallet diff --git a/src/wallet/transaction.h b/src/wallet/transaction.h index bac89c2f633f..bdd336c6ce7a 100644 --- a/src/wallet/transaction.h +++ b/src/wallet/transaction.h @@ -20,6 +20,10 @@ #include #include +namespace interfaces { +class Chain; +} // namespace interfaces + namespace wallet { //! State of transaction confirmed in a block. struct TxStateConfirmed { @@ -309,6 +313,10 @@ class CWalletTx template const T* state() const { return std::get_if(&m_state); } template T* state() { return std::get_if(&m_state); } + //! Update transaction state when attaching to a chain, filling in heights + //! of conflicted and confirmed blocks + void updateState(interfaces::Chain& chain); + bool isAbandoned() const { return state() && state()->abandoned; } bool isConflicted() const { return state(); } bool isUnconfirmed() const { return !isAbandoned() && !isConflicted() && !isConfirmed(); } diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index d7be196ed58b..a9d42dc3c3fb 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1127,23 +1127,7 @@ bool CWallet::LoadToWallet(const uint256& hash, const UpdateWalletTxFn& fill_wtx // If wallet doesn't have a chain (e.g when using dash-wallet tool), // don't bother to update txn. if (HaveChain()) { - bool active; - auto lookup_block = [&](const uint256& hash, int& height, TxState& state) { - // If tx block (or conflicting block) was reorged out of chain - // while the wallet was shutdown, change tx status to UNCONFIRMED - // and reset block height, hash, and index. ABANDONED tx don't have - // associated blocks and don't need to be updated. The case where a - // transaction was reorged out while online and then reconfirmed - // while offline is covered by the rescan logic. - if (!chain().findBlock(hash, FoundBlock().inActiveChain(active).height(height)) || !active) { - state = TxStateInactive{}; - } - }; - if (auto* conf = wtx.state()) { - lookup_block(conf->confirmed_block_hash, conf->confirmed_block_height, wtx.m_state); - } else if (auto* conf = wtx.state()) { - lookup_block(conf->conflicting_block_hash, conf->conflicting_block_height, wtx.m_state); - } + wtx.updateState(chain()); } if (/* insertion took place */ ins.second) { wtx.m_it_wtxOrdered = wtxOrdered.insert(std::make_pair(wtx.nOrderPos, &wtx)); @@ -3721,8 +3705,10 @@ int CWallet::GetTxDepthInMainChain(const CWalletTx& wtx) const { AssertLockHeld(cs_wallet); if (auto* conf = wtx.state()) { + assert(conf->confirmed_block_height >= 0); return GetLastBlockHeight() - conf->confirmed_block_height + 1; } else if (auto* conf = wtx.state()) { + assert(conf->conflicting_block_height >= 0); return -1 * (GetLastBlockHeight() - conf->conflicting_block_height + 1); } else { return 0; diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 97c79ba5bf1e..16f3f5c82d3f 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -543,6 +543,13 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati * <0 : conflicts with a transaction this deep in the blockchain * 0 : in memory pool, waiting to be included in a block * >=1 : this many blocks deep in the main chain + * + * Preconditions: it is only valid to call this function when the wallet is + * online and the block index is loaded. So this cannot be called by + * bitcoin-wallet tool code or by wallet migration code. If this is called + * without the wallet being online, it won't be able able to determine the + * the height of the last block processed, or the heights of blocks + * referenced in transaction, and might cause assert failures. */ int GetTxDepthInMainChain(const CWalletTx& wtx) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); bool IsTxInMainChain(const CWalletTx& wtx) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) diff --git a/test/functional/interface_rest.py b/test/functional/interface_rest.py index a2186bf57ea1..802a583b5eb3 100755 --- a/test/functional/interface_rest.py +++ b/test/functional/interface_rest.py @@ -64,7 +64,7 @@ def test_rest_request( body: str = '', status: int = 200, ret_type: RetType = RetType.JSON, - query_params: typing.Dict[str, typing.Any] = None, + query_params: dict[str, typing.Any] = None, ) -> typing.Union[http.client.HTTPResponse, bytes, str, None]: rest_uri = '/rest' + uri if req_type in ReqType: diff --git a/test/functional/mempool_datacarrier.py b/test/functional/mempool_datacarrier.py index 871346467ffa..57896d4097a3 100755 --- a/test/functional/mempool_datacarrier.py +++ b/test/functional/mempool_datacarrier.py @@ -13,12 +13,11 @@ ) from test_framework.test_framework import BitcoinTestFramework from test_framework.test_node import TestNode -from test_framework.util import ( - assert_raises_rpc_error, - random_bytes, -) +from test_framework.util import assert_raises_rpc_error from test_framework.wallet import MiniWallet +from random import randbytes + class DataCarrierTest(BitcoinTestFramework): def set_test_params(self): @@ -49,11 +48,11 @@ def run_test(self): self.wallet.rescan_utxos() # By default, only 80 bytes are used for data (+1 for OP_RETURN, +2 for the pushdata opcodes). - default_size_data = random_bytes(MAX_OP_RETURN_RELAY - 3) - too_long_data = random_bytes(MAX_OP_RETURN_RELAY - 2) - small_data = random_bytes(MAX_OP_RETURN_RELAY - 4) - one_byte = random_bytes(1) - zero_bytes = random_bytes(0) + default_size_data = randbytes(MAX_OP_RETURN_RELAY - 3) + too_long_data = randbytes(MAX_OP_RETURN_RELAY - 2) + small_data = randbytes(MAX_OP_RETURN_RELAY - 4) + one_byte = randbytes(1) + zero_bytes = randbytes(0) self.log.info("Testing null data transaction with default -datacarrier and -datacarriersize values.") self.test_null_data_transaction(node=self.nodes[0], data=default_size_data, success=True) diff --git a/test/functional/p2p_net_deadlock.py b/test/functional/p2p_net_deadlock.py index b1b9e67c6d47..dd052605198f 100755 --- a/test/functional/p2p_net_deadlock.py +++ b/test/functional/p2p_net_deadlock.py @@ -6,7 +6,7 @@ import threading from test_framework.messages import MAX_PROTOCOL_MESSAGE_LENGTH from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import random_bytes +from random import randbytes class NetDeadlockTest(BitcoinTestFramework): def set_test_params(self): @@ -18,7 +18,7 @@ def run_test(self): node1 = self.nodes[1] self.log.info("Simultaneously send a large message on both sides") - rand_msg = random_bytes(MAX_PROTOCOL_MESSAGE_LENGTH).hex() + rand_msg = randbytes(MAX_PROTOCOL_MESSAGE_LENGTH).hex() thread0 = threading.Thread(target=node0.sendmsgtopeer, args=(0, "unknown", rand_msg)) thread1 = threading.Thread(target=node1.sendmsgtopeer, args=(0, "unknown", rand_msg)) diff --git a/test/functional/p2p_v2_misbehaving.py b/test/functional/p2p_v2_misbehaving.py index 60c2876cf5d2..472520d227c3 100755 --- a/test/functional/p2p_v2_misbehaving.py +++ b/test/functional/p2p_v2_misbehaving.py @@ -7,7 +7,6 @@ from enum import Enum from test_framework.messages import MAGIC_BYTES -from test_framework.util import random_bytes from test_framework.p2p import P2PInterface from test_framework.test_framework import BitcoinTestFramework from test_framework.util import random_bitflip @@ -91,7 +90,7 @@ def complete_handshake(self, response): class NonEmptyVersionPacketState(EncryptedP2PState): """"Add option for sending non-empty transport version packet.""" def complete_handshake(self, response): - self.transport_version = random_bytes(5) + self.transport_version = random.randbytes(5) return super().complete_handshake(response) diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py index 5788ff158336..10173efc645a 100755 --- a/test/functional/rpc_psbt.py +++ b/test/functional/rpc_psbt.py @@ -7,6 +7,7 @@ from decimal import Decimal from itertools import product +from random import randbytes from test_framework.descriptors import descsum_create from test_framework.key import ECKey @@ -32,7 +33,6 @@ assert_greater_than, assert_raises_rpc_error, find_output, - random_bytes, ) from test_framework.wallet_util import bytes_to_wif @@ -598,10 +598,10 @@ def test_psbt_input_keys(psbt_input, keys): self.log.info("Test decoding PSBT with per-input preimage types") # note that the decodepsbt RPC doesn't check whether preimages and hashes match - hash_ripemd160, preimage_ripemd160 = random_bytes(20), random_bytes(50) - hash_sha256, preimage_sha256 = random_bytes(32), random_bytes(50) - hash_hash160, preimage_hash160 = random_bytes(20), random_bytes(50) - hash_hash256, preimage_hash256 = random_bytes(32), random_bytes(50) + hash_ripemd160, preimage_ripemd160 = randbytes(20), randbytes(50) + hash_sha256, preimage_sha256 = randbytes(32), randbytes(50) + hash_hash160, preimage_hash160 = randbytes(20), randbytes(50) + hash_hash256, preimage_hash256 = randbytes(32), randbytes(50) tx = CTransaction() tx.vin = [CTxIn(outpoint=COutPoint(hash=int('aa' * 32, 16), n=0), scriptSig=b""), diff --git a/test/functional/test_framework/script.py b/test/functional/test_framework/script.py index bb078591b9f4..9e4a96507258 100644 --- a/test/functional/test_framework/script.py +++ b/test/functional/test_framework/script.py @@ -8,7 +8,6 @@ """ import struct import unittest -from typing import List, Dict from .messages import ( CTransaction, @@ -97,8 +96,8 @@ def __new__(cls, n): _opcode_instances.append(super().__new__(cls, n)) return _opcode_instances[n] -OPCODE_NAMES: Dict[CScriptOp, str] = {} -_opcode_instances: List[CScriptOp] = [] +OPCODE_NAMES: dict[CScriptOp, str] = {} +_opcode_instances: list[CScriptOp] = [] # Populate opcode instance table for n in range(0xff + 1): diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index de0e4ca70501..87e23c40f823 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -23,7 +23,7 @@ import time from concurrent.futures import ThreadPoolExecutor -from typing import List, Optional, Union +from typing import Optional, Union from .address import ADDRESS_BCRT1_P2SH_OP_TRUE from .authproxy import JSONRPCException from test_framework.masternodes import check_banned, check_punished @@ -122,7 +122,7 @@ def __init__(self): self.chain: str = 'regtest' self.setup_clean_chain: bool = False self.disable_mocktime: bool = False - self.nodes: List[TestNode] = [] + self.nodes: list[TestNode] = [] self.extra_args = None self.network_thread = None self.mocktime = 0 @@ -1244,8 +1244,8 @@ def get_node(self, test: BitcoinTestFramework) -> TestNode: raise AssertionError(f"Node at pos {self.nodeIdx} not present, did you start the node?") return test.nodes[self.nodeIdx] - def validate_inputs(self, platform_node_id: Optional[str] = None, addrs_platform_p2p: Union[int, str, List[str], None] = None, - addrs_platform_https: Union[int, str, List[str], None] = None, expected_assert_code: Optional[int] = None, + def validate_inputs(self, platform_node_id: Optional[str] = None, addrs_platform_p2p: Union[int, str, list[str], None] = None, + addrs_platform_https: Union[int, str, list[str], None] = None, expected_assert_code: Optional[int] = None, expected_assert_msg: Optional[str] = None): if (expected_assert_code and not expected_assert_msg) or (not expected_assert_code and expected_assert_msg): raise AssertionError("Intending to use assert_raises_rpc_error() but didn't specify code and message") @@ -1258,10 +1258,10 @@ def validate_inputs(self, platform_node_id: Optional[str] = None, addrs_platform raise AssertionError("EvoNode but addrs_platform_https is missing, must be specified!") def register(self, node: TestNode, submit: bool, collateral_txid: Optional[str] = None, collateral_vout: Optional[int] = None, - addrs_core_p2p: Union[str, List[str], None] = None, ownerAddr: Optional[str] = None, pubKeyOperator: Optional[str] = None, + addrs_core_p2p: Union[str, list[str], None] = None, ownerAddr: Optional[str] = None, pubKeyOperator: Optional[str] = None, votingAddr: Optional[str] = None, operator_reward: Optional[int] = None, rewards_address: Optional[str] = None, - fundsAddr: Optional[str] = None, platform_node_id: Optional[str] = None, addrs_platform_p2p: Union[int, str, List[str], None] = None, - addrs_platform_https: Union[int, str, List[str], None] = None, expected_assert_code: Optional[int] = None, + fundsAddr: Optional[str] = None, platform_node_id: Optional[str] = None, addrs_platform_p2p: Union[int, str, list[str], None] = None, + addrs_platform_https: Union[int, str, list[str], None] = None, expected_assert_code: Optional[int] = None, expected_assert_msg: Optional[str] = None) -> Optional[str]: self.validate_inputs(platform_node_id, addrs_platform_p2p, addrs_platform_https, expected_assert_code, expected_assert_msg) @@ -1304,11 +1304,11 @@ def register(self, node: TestNode, submit: bool, collateral_txid: Optional[str] return ret - def register_fund(self, node: TestNode, submit: bool, collateral_address: Optional[str] = None, addrs_core_p2p: Union[str, List[str], None] = None, + def register_fund(self, node: TestNode, submit: bool, collateral_address: Optional[str] = None, addrs_core_p2p: Union[str, list[str], None] = None, ownerAddr: Optional[str] = None, pubKeyOperator: Optional[str] = None, votingAddr: Optional[str] = None, operator_reward: Optional[int] = None, rewards_address: Optional[str] = None, fundsAddr: Optional[str] = None, - platform_node_id: Optional[str] = None, addrs_platform_p2p: Union[int, str, List[str], None] = None, - addrs_platform_https: Union[int, str, List[str], None] = None, expected_assert_code: Optional[int] = None, + platform_node_id: Optional[str] = None, addrs_platform_p2p: Union[int, str, list[str], None] = None, + addrs_platform_https: Union[int, str, list[str], None] = None, expected_assert_code: Optional[int] = None, expected_assert_msg: Optional[str] = None) -> Optional[str]: self.validate_inputs(platform_node_id, addrs_platform_p2p, addrs_platform_https, expected_assert_code, expected_assert_msg) @@ -1434,8 +1434,8 @@ def update_registrar(self, node: TestNode, submit: bool, pubKeyOperator: Optiona return ret - def update_service(self, node: TestNode, submit: bool, addrs_core_p2p: Union[str, List[str], None] = None, platform_node_id: Optional[str] = None, - addrs_platform_p2p: Union[int, str, List[str], None] = None, addrs_platform_https: Union[int, str, List[str], None] = None, + def update_service(self, node: TestNode, submit: bool, addrs_core_p2p: Union[str, list[str], None] = None, platform_node_id: Optional[str] = None, + addrs_platform_p2p: Union[int, str, list[str], None] = None, addrs_platform_https: Union[int, str, list[str], None] = None, address_operator: Optional[str] = None, fundsAddr: Optional[str] = None, expected_assert_code: Optional[int] = None, expected_assert_msg: Optional[str] = None) -> Optional[str]: self.validate_inputs(platform_node_id, addrs_platform_p2p, addrs_platform_https, expected_assert_code, expected_assert_msg) @@ -2375,7 +2375,7 @@ def get_recovered_sig(self, rec_sig_id, rec_sig_msg_hash, llmq_type=100, use_pla self.log.info(f"getrecsig failed with '{e}'") assert False - def get_quorum_masternodes(self, q, llmq_type=100) -> List[Optional[MasternodeInfo]]: + def get_quorum_masternodes(self, q, llmq_type=100) -> list[Optional[MasternodeInfo]]: qi = self.nodes[0].quorum('info', llmq_type, q) result = [] for m in qi['members']: diff --git a/test/functional/test_framework/util.py b/test/functional/test_framework/util.py index 7cdcbc8983d8..b2830b1612f7 100644 --- a/test/functional/test_framework/util.py +++ b/test/functional/test_framework/util.py @@ -21,7 +21,8 @@ from . import coverage from .authproxy import AuthServiceProxy, JSONRPCException -from typing import Callable, Optional +from collections.abc import Callable +from typing import Optional logger = logging.getLogger("TestFramework.utils") @@ -310,10 +311,6 @@ def sha256sum_file(filename): d = f.read(4096) return h.digest() -# TODO: Remove and use random.randbytes(n) directly -def random_bytes(n): - """Return a random bytes object of length n.""" - return random.randbytes(n) # RPC/P2P connection constants and functions ############################################ diff --git a/test/functional/test_framework/v2_p2p.py b/test/functional/test_framework/v2_p2p.py index c1c6e7afe258..dcc90c38d537 100644 --- a/test/functional/test_framework/v2_p2p.py +++ b/test/functional/test_framework/v2_p2p.py @@ -12,7 +12,6 @@ from .crypto.hkdf import hkdf_sha256 from .key import TaggedHash from .messages import MAGIC_BYTES -from .util import random_bytes CHACHA20POLY1305_EXPANSION = 16 @@ -156,7 +155,7 @@ def generate_keypair_and_garbage(self, garbage_len=None): self.privkey_ours, self.ellswift_ours = ellswift_create() if garbage_len is None: garbage_len = random.randrange(MAX_GARBAGE_LEN + 1) - self.sent_garbage = random_bytes(garbage_len) + self.sent_garbage = random.randbytes(garbage_len) return self.ellswift_ours + self.sent_garbage def initiate_v2_handshake(self): diff --git a/test/functional/test_framework/wallet.py b/test/functional/test_framework/wallet.py index 8333558dba5c..a7ced05bba36 100644 --- a/test/functional/test_framework/wallet.py +++ b/test/functional/test_framework/wallet.py @@ -9,7 +9,6 @@ from enum import Enum from typing import ( Any, - List, Optional, ) from test_framework.address import ( @@ -257,7 +256,7 @@ def send_self_transfer_multi(self, *, from_node, **kwargs): def create_self_transfer_multi( self, *, - utxos_to_spend: Optional[List[dict]] = None, + utxos_to_spend: Optional[list[dict]] = None, num_outputs=1, amount_per_output=0, locktime=0, diff --git a/test/functional/wallet_fast_rescan.py b/test/functional/wallet_fast_rescan.py index 50f4602e8c42..8cec87cd0b50 100755 --- a/test/functional/wallet_fast_rescan.py +++ b/test/functional/wallet_fast_rescan.py @@ -5,7 +5,6 @@ """Test that fast rescan using block filters for descriptor wallets detects top-ups correctly and finds the same transactions than the slow variant.""" import os -from typing import List from test_framework.descriptors import descsum_create from test_framework.test_framework import BitcoinTestFramework @@ -32,7 +31,7 @@ def skip_test_if_missing_module(self): self.skip_if_no_wallet() self.skip_if_no_sqlite() - def get_wallet_txids(self, node: TestNode, wallet_name: str) -> List[str]: + def get_wallet_txids(self, node: TestNode, wallet_name: str) -> list[str]: w = node.get_wallet_rpc(wallet_name) txs = w.listtransactions('*', 1000000) return [tx['txid'] for tx in txs] diff --git a/test/fuzz/test_runner.py b/test/fuzz/test_runner.py index 399d9209e280..e6ab467f2efb 100755 --- a/test/fuzz/test_runner.py +++ b/test/fuzz/test_runner.py @@ -17,11 +17,14 @@ def get_fuzz_env(*, target, source_dir): + symbolizer = os.environ.get('LLVM_SYMBOLIZER_PATH', "/usr/bin/llvm-symbolizer") return { 'FUZZ': target, 'UBSAN_OPTIONS': f'suppressions={source_dir}/test/sanitizer_suppressions/ubsan:print_stacktrace=1:halt_on_error=1:report_error_type=1', + 'UBSAN_SYMBOLIZER_PATH':symbolizer, "ASAN_OPTIONS": "detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1", + 'ASAN_SYMBOLIZER_PATH':symbolizer, } diff --git a/test/lint/README.md b/test/lint/README.md index 704922d7abe3..891262bccf70 100644 --- a/test/lint/README.md +++ b/test/lint/README.md @@ -17,6 +17,14 @@ docker run --rm -v $(pwd):/bitcoin -it bitcoin-linter After building the container once, you can simply run the last command any time you want to lint. +test runner +=========== + +To run the checks in the test runner outside the docker, use: + +```sh +( cd ./test/lint/test_runner/ && cargo fmt && cargo clippy && cargo run ) +``` check-doc.py ============ diff --git a/test/lint/lint-files.py b/test/lint/lint-files.py index ee09cea93235..a23fc4416a7c 100755 --- a/test/lint/lint-files.py +++ b/test/lint/lint-files.py @@ -11,7 +11,7 @@ import re import sys from subprocess import check_output -from typing import Dict, Optional, NoReturn +from typing import Optional, NoReturn CMD_TOP_LEVEL = ["git", "rev-parse", "--show-toplevel"] CMD_ALL_FILES = ["git", "ls-files", "-z", "--full-name", "--stage"] @@ -69,7 +69,7 @@ def full_extension(self) -> Optional[str]: return None -def get_git_file_metadata() -> Dict[str, FileMeta]: +def get_git_file_metadata() -> dict[str, FileMeta]: ''' Return a dictionary mapping the name of all files in the repository to git tree metadata. ''' diff --git a/test/lint/lint-include-guards.py b/test/lint/lint-include-guards.py index 6e59c83f7dbc..9686c1aad853 100755 --- a/test/lint/lint-include-guards.py +++ b/test/lint/lint-include-guards.py @@ -11,7 +11,6 @@ import re import sys from subprocess import check_output -from typing import List HEADER_ID_PREFIX = 'BITCOIN_' @@ -33,7 +32,7 @@ 'src/immer'] -def _get_header_file_lst() -> List[str]: +def _get_header_file_lst() -> list[str]: """ Helper function to get a list of header filepaths to be checked for include guards. """ diff --git a/test/lint/test_runner/Cargo.lock b/test/lint/test_runner/Cargo.lock new file mode 100644 index 000000000000..ca83aa93310c --- /dev/null +++ b/test/lint/test_runner/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "test_runner" +version = "0.1.0" diff --git a/test/lint/test_runner/Cargo.toml b/test/lint/test_runner/Cargo.toml new file mode 100644 index 000000000000..053ce43d6ce3 --- /dev/null +++ b/test/lint/test_runner/Cargo.toml @@ -0,0 +1,12 @@ +# Copyright (c) The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://opensource.org/license/mit/. + +[package] +name = "test_runner" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/test/lint/test_runner/src/main.rs b/test/lint/test_runner/src/main.rs new file mode 100644 index 000000000000..b7ec9ee3b213 --- /dev/null +++ b/test/lint/test_runner/src/main.rs @@ -0,0 +1,77 @@ +// Copyright (c) The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or https://opensource.org/license/mit/. + +use std::env; +use std::path::PathBuf; +use std::process::Command; +use std::process::ExitCode; + +use String as LintError; + +/// Return the git command +fn git() -> Command { + Command::new("git") +} + +/// Return stdout +fn check_output(cmd: &mut std::process::Command) -> Result { + let out = cmd.output().expect("command error"); + if !out.status.success() { + return Err(String::from_utf8_lossy(&out.stderr).to_string()); + } + Ok(String::from_utf8(out.stdout) + .map_err(|e| format!("{e}"))? + .trim() + .to_string()) +} + +/// Return the git root as utf8, or panic +fn get_git_root() -> String { + check_output(git().args(["rev-parse", "--show-toplevel"])).unwrap() +} + +fn lint_std_filesystem() -> Result<(), LintError> { + let found = git() + .args([ + "grep", + "std::filesystem", + "--", + "./src/", + ":(exclude)src/util/fs.h", + ]) + .status() + .expect("command error") + .success(); + if found { + Err(r#" +^^^ +Direct use of std::filesystem may be dangerous and buggy. Please include and use the +fs:: namespace, which has unsafe filesystem functions marked as deleted. + "# + .to_string()) + } else { + Ok(()) + } +} + +fn main() -> ExitCode { + let test_list = [("std::filesystem check", lint_std_filesystem)]; + + let git_root = PathBuf::from(get_git_root()); + + let mut test_failed = false; + for (lint_name, lint_fn) in test_list { + // chdir to root before each lint test + env::set_current_dir(&git_root).unwrap(); + if let Err(err) = lint_fn() { + println!("{err}\n^---- Failure generated from {lint_name}!"); + test_failed = true; + } + } + if test_failed { + ExitCode::FAILURE + } else { + ExitCode::SUCCESS + } +}