diff --git a/.github/workflows/build-src.yml b/.github/workflows/build-src.yml
index 9af8b19b2e00..6f9d3279b845 100644
--- a/.github/workflows/build-src.yml
+++ b/.github/workflows/build-src.yml
@@ -76,7 +76,7 @@ jobs:
 
       - name: Build source
         run: |
-          CCACHE_SIZE="400M"
+          CCACHE_MAXSIZE="400M"
           CACHE_DIR="/cache"
           mkdir /output
           BASE_OUTDIR="/output"
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 224d7988ecc3..4e9b6c9efa8b 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -18,8 +18,29 @@ concurrency:
   cancel-in-progress: ${{ github.ref_type != 'tag' }}
 
 jobs:
+  check-skip:
+    name: Check skip conditions
+    runs-on: ubuntu-latest
+    outputs:
+      skip: ${{ steps.skip-check.outputs.skip }}
+    steps:
+      - name: Check skip environment variables
+        id: skip-check
+        run: |
+          if [[ "${{ github.event_name }}" == "push" && "${{ vars.SKIP_ON_PUSH }}" != "" ]]; then
+            echo "Skipping build on push due to SKIP_ON_PUSH environment variable"
+            echo "skip=true" >> $GITHUB_OUTPUT
+          elif [[ "${{ github.event_name }}" == "pull_request_target" && "${{ vars.SKIP_ON_PR }}" != "" ]]; then
+            echo "Skipping build on pull request due to SKIP_ON_PR environment variable"
+            echo "skip=true" >> $GITHUB_OUTPUT
+          else
+            echo "skip=false" >> $GITHUB_OUTPUT
+          fi
+
   container:
     name: Build container
+    needs: [check-skip]
+    if: ${{ needs.check-skip.outputs.skip == 'false' }}
     uses: ./.github/workflows/build-container.yml
     with:
       context: ./contrib/containers/ci
@@ -28,6 +49,8 @@ jobs:
 
   container-slim:
     name: Build slim container
+    needs: [check-skip]
+    if: ${{ needs.check-skip.outputs.skip == 'false' }}
     uses: ./.github/workflows/build-container.yml
     with:
       context: ./contrib/containers/ci
diff --git a/.tx/config b/.tx/config
index 7699aee5fffe..a9939bf4f9cd 100644
--- a/.tx/config
+++ b/.tx/config
@@ -1,7 +1,7 @@
 [main]
 host = https://www.transifex.com
 
-[dash.dash_ents]
+[o:dash:p:dash:r:dash_ents]
 file_filter = src/qt/locale/dash_<lang>.ts
 source_file = src/qt/locale/dash_en.xlf
 source_lang = en
diff --git a/ci/dash/build_src.sh b/ci/dash/build_src.sh
index 3ebdc910dea2..ad1201e4599d 100755
--- a/ci/dash/build_src.sh
+++ b/ci/dash/build_src.sh
@@ -27,7 +27,7 @@ if [ "$CHECK_DOC" = 1 ]; then
     test/lint/all-lint.py
 fi
 
-ccache --zero-stats --max-size="$CCACHE_SIZE"
+ccache --zero-stats
 
 if [ -n "$CONFIG_SHELL" ]; then
   export CONFIG_SHELL="$CONFIG_SHELL"
diff --git a/ci/lint/04_install.sh b/ci/lint/04_install.sh
index bcd636641f7f..431672ba1fb3 100755
--- a/ci/lint/04_install.sh
+++ b/ci/lint/04_install.sh
@@ -35,6 +35,7 @@ fi
 
 ${CI_RETRY_EXE} pip3 install codespell==2.0.0
 ${CI_RETRY_EXE} pip3 install flake8==3.8.3
+${CI_RETRY_EXE} pip3 install lief==0.13.1
 ${CI_RETRY_EXE} pip3 install mypy==0.910
 ${CI_RETRY_EXE} pip3 install pyzmq==22.3.0
 ${CI_RETRY_EXE} pip3 install vulture==2.3
diff --git a/ci/test/00_setup_env.sh b/ci/test/00_setup_env.sh
index d4840e9ebfd8..1d9acfb52f53 100755
--- a/ci/test/00_setup_env.sh
+++ b/ci/test/00_setup_env.sh
@@ -55,7 +55,7 @@ export BOOST_TEST_RANDOM=${BOOST_TEST_RANDOM:-1}
 export DEBIAN_FRONTEND=noninteractive
 export HOST_CACHE_DIR=${HOST_CACHE_DIR:-$BASE_ROOT_DIR/ci-cache-$BUILD_TARGET}
 export CACHE_DIR=${CACHE_DIR:-$HOST_CACHE_DIR}
-export CCACHE_SIZE=${CCACHE_SIZE:-100M}
+export CCACHE_MAXSIZE=${CCACHE_MAXSIZE:-100M}
 export CCACHE_TEMPDIR=${CCACHE_TEMPDIR:-/tmp/.ccache-temp}
 export CCACHE_COMPRESS=${CCACHE_COMPRESS:-1}
 # The cache dir.
@@ -70,7 +70,7 @@ export BASE_OUTDIR=${BASE_OUTDIR:-$BASE_SCRATCH_DIR/out/$HOST}
 export BASE_BUILD_DIR=${BASE_BUILD_DIR:-$BASE_SCRATCH_DIR/build-ci}
 export PREVIOUS_RELEASES_DIR=${PREVIOUS_RELEASES_DIR:-$BASE_ROOT_DIR/releases/$HOST}
 export SDK_URL=${SDK_URL:-https://bitcoincore.org/depends-sources/sdks}
-export DOCKER_PACKAGES=${DOCKER_PACKAGES:-build-essential libtool autotools-dev automake pkg-config bsdmainutils curl ca-certificates ccache python3 rsync git procps}
+export DOCKER_PACKAGES=${DOCKER_PACKAGES:-build-essential libtool autotools-dev automake pkg-config bsdmainutils curl ca-certificates ccache python3 rsync git procps bison e2fsprogs}
 export GOAL=${GOAL:-install}
 export DIR_QA_ASSETS=${DIR_QA_ASSETS:-${BASE_SCRATCH_DIR}/qa-assets}
 export PATH=${BASE_ROOT_DIR}/ci/retry:$PATH
diff --git a/ci/test/00_setup_env_i686_centos.sh b/ci/test/00_setup_env_i686_centos.sh
new file mode 100644
index 000000000000..d509c7214101
--- /dev/null
+++ b/ci/test/00_setup_env_i686_centos.sh
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+#
+# Copyright (c) 2020-2022 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+export LC_ALL=C.UTF-8
+
+export HOST=i686-pc-linux-gnu
+export CONTAINER_NAME=ci_i686_centos
+export CI_IMAGE_NAME_TAG="quay.io/centos/amd64:stream9"
+export CI_BASE_PACKAGES="gcc-c++ glibc-devel.x86_64 libstdc++-devel.x86_64 glibc-devel.i686 libstdc++-devel.i686 ccache libtool make git python3 python3-pip which patch lbzip2 xz procps-ng dash rsync coreutils bison util-linux e2fsprogs"
+export PIP_PACKAGES="pyzmq"
+export GOAL="install"
+export NO_WERROR=1  # Suppress error: #warning _FORTIFY_SOURCE > 2 is treated like 2 on this platform [-Werror=cpp]
+export BITCOIN_CONFIG="--enable-zmq --with-gui=qt5 --enable-reduce-exports"
+export CONFIG_SHELL="/bin/dash"
diff --git a/ci/test/00_setup_env_mac_native_x86_64.sh b/ci/test/00_setup_env_mac_native_x86_64.sh
index 0ac2d62eb6c5..252b95c2178f 100755
--- a/ci/test/00_setup_env_mac_native_x86_64.sh
+++ b/ci/test/00_setup_env_mac_native_x86_64.sh
@@ -14,6 +14,6 @@ export BITCOIN_CONFIG="--with-gui --enable-reduce-exports --disable-miner --with
 export CI_OS_NAME="macos"
 export NO_DEPENDS=1
 export OSX_SDK=""
-export CCACHE_SIZE=300M
+export CCACHE_MAXSIZE=300M
 
 export RUN_SECURITY_TESTS="true"
diff --git a/ci/test/00_setup_env_native_fuzz_with_valgrind.sh b/ci/test/00_setup_env_native_fuzz_with_valgrind.sh
index 1b5728feb84c..f379d8ee7edc 100755
--- a/ci/test/00_setup_env_native_fuzz_with_valgrind.sh
+++ b/ci/test/00_setup_env_native_fuzz_with_valgrind.sh
@@ -7,7 +7,7 @@
 export LC_ALL=C.UTF-8
 
 export CONTAINER_NAME=ci_native_fuzz_valgrind
-export PACKAGES="clang llvm python3 libevent-dev bsdmainutils libboost-dev valgrind"
+export PACKAGES="clang llvm libclang-rt-dev python3 libevent-dev bsdmainutils libboost-dev valgrind"
 export NO_DEPENDS=1
 export RUN_UNIT_TESTS=false
 export RUN_FUNCTIONAL_TESTS=false
@@ -16,4 +16,4 @@ export FUZZ_TESTS_CONFIG="--valgrind"
 export GOAL="install"
 # Temporarily pin dwarf 4, until valgrind can understand clang's dwarf 5
 export BITCOIN_CONFIG="--enable-fuzz --with-sanitizers=fuzzer CC=clang-18 CXX=clang++-18 CFLAGS='-gdwarf-4' CXXFLAGS='-gdwarf-4'"
-export CCACHE_SIZE=200M
+export CCACHE_MAXSIZE=200M
diff --git a/ci/test/00_setup_env_native_valgrind.sh b/ci/test/00_setup_env_native_valgrind.sh
index d0a217a1f659..d7bf8b2f3550 100755
--- a/ci/test/00_setup_env_native_valgrind.sh
+++ b/ci/test/00_setup_env_native_valgrind.sh
@@ -6,7 +6,7 @@
 
 export LC_ALL=C.UTF-8
 
-export PACKAGES="valgrind clang llvm python3-zmq libevent-dev bsdmainutils libboost-dev libdb5.3++-dev libminiupnpc-dev libzmq3-dev"
+export PACKAGES="valgrind clang llvm libclang-rt-dev python3-zmq libevent-dev bsdmainutils libboost-dev libdb5.3++-dev libminiupnpc-dev libzmq3-dev"
 export USE_VALGRIND=1
 export NO_DEPENDS=1
 export TEST_RUNNER_EXTRA="--exclude rpc_bind,feature_bind_extra --timeout-factor=4"  # Excluded for now, see https://github.com/bitcoin/bitcoin/issues/17765#issuecomment-602068547
diff --git a/ci/test/04_install.sh b/ci/test/04_install.sh
index 3ea6d24e6b86..b2a2c7cab6d8 100755
--- a/ci/test/04_install.sh
+++ b/ci/test/04_install.sh
@@ -42,7 +42,7 @@ if [ -z "$DANGER_RUN_CI_ON_HOST" ]; then
   ${CI_RETRY_EXE} docker pull "$DOCKER_NAME_TAG"
 
   # shellcheck disable=SC2086
-  DOCKER_ID=$(docker run $DOCKER_ADMIN -idt \
+  DOCKER_ID=$(docker run $DOCKER_ADMIN --cap-add LINUX_IMMUTABLE -idt \
                   --mount type=bind,src=$BASE_ROOT_DIR,dst=/ro_base,readonly \
                   --mount type=bind,src=$CCACHE_DIR,dst=$CCACHE_DIR \
                   --mount type=bind,src=$DEPENDS_DIR,dst=$DEPENDS_DIR \
@@ -97,7 +97,7 @@ if [ "$CI_OS_NAME" == "macos" ]; then
   echo "Number of CPUs: $(sysctl -n hw.logicalcpu)"
 else
   CI_EXEC free -m -h
-  CI_EXEC echo "Number of CPUs \(nproc\):" \$\(nproc\)
+  CI_EXEC echo "Number of CPUs (nproc): $(nproc)"
   CI_EXEC echo "$(lscpu | grep Endian)"
 fi
 CI_EXEC echo "Free disk space:"
diff --git a/configure.ac b/configure.ac
index ac4bd086422d..11f4fe2c215b 100644
--- a/configure.ac
+++ b/configure.ac
@@ -450,15 +450,6 @@ if test "$enable_werror" = "yes"; then
   fi
   ERROR_CXXFLAGS=$CXXFLAG_WERROR
 
-  dnl -Warray-bounds cause problems with GCC. Do not treat these warnings as errors.
-  dnl Suppress -Warray-bounds entirely because of noisy output, currently unhappy with immer implementation.
-  AX_CHECK_COMPILE_FLAG([-Warray-bounds], [NOWARN_CXXFLAGS="$NOWARN_CXXFLAGS -Wno-array-bounds"], [], [$CXXFLAG_WERROR], [AC_LANG_SOURCE([
-    #if defined(__clang__) || defined(__INTEL_COMPILER) || !defined(__GNUC__)
-      #error Non-GCC compiler detected, not setting flag
-    #endif
-    int main(void) { return 0; }
-  ])])
-
   dnl -Wattributes cause problems with some versions of GCC. Do not treat these warnings as errors.
   AX_CHECK_COMPILE_FLAG([-Wattributes], [NOWARN_CXXFLAGS="$NOWARN_CXXFLAGS -Wno-error=attributes"], [], [$CXXFLAG_WERROR], [AC_LANG_SOURCE([
     #if defined(__clang__) || defined(__INTEL_COMPILER) || !defined(__GNUC__)
@@ -478,22 +469,31 @@ if test "$enable_werror" = "yes"; then
     #endif
     int main(void) { return 0; }
   ])])
-
-  dnl -Wstringop-overread and -Wstringop-overflow are broken in GCC. Suppress warnings entirely to avoid noisy output.
-  AX_CHECK_COMPILE_FLAG([-Wstringop-overread], [NOWARN_CXXFLAGS="$NOWARN_CXXFLAGS -Wno-stringop-overread"], [], [$CXXFLAG_WERROR], [AC_LANG_SOURCE([
-    #if defined(__clang__) || defined(__INTEL_COMPILER) || !defined(__GNUC__)
-      #error Non-GCC compiler detected, not setting flag
-    #endif
-    int main(void) { return 0; }
-  ])])
-  AX_CHECK_COMPILE_FLAG([-Wstringop-overflow], [NOWARN_CXXFLAGS="$NOWARN_CXXFLAGS -Wno-stringop-overflow"], [], [$CXXFLAG_WERROR], [AC_LANG_SOURCE([
-    #if defined(__clang__) || defined(__INTEL_COMPILER) || !defined(__GNUC__)
-      #error Non-GCC compiler detected, not setting flag
-    #endif
-    int main(void) { return 0; }
-  ])])
 fi
 
+dnl -Warray-bounds cause problems with GCC. Do not treat these warnings as errors.
+dnl Suppress -Warray-bounds entirely because of noisy output, currently unhappy with immer implementation.
+AX_CHECK_COMPILE_FLAG([-Warray-bounds], [NOWARN_CXXFLAGS="$NOWARN_CXXFLAGS -Wno-array-bounds"], [], [$CXXFLAG_WERROR], [AC_LANG_SOURCE([
+  #if defined(__clang__) || defined(__INTEL_COMPILER) || !defined(__GNUC__)
+    #error Non-GCC compiler detected, not setting flag
+  #endif
+  int main(void) { return 0; }
+])])
+
+dnl -Wstringop-overread and -Wstringop-overflow are broken in GCC. Suppress warnings entirely to avoid noisy output.
+AX_CHECK_COMPILE_FLAG([-Wstringop-overread], [NOWARN_CXXFLAGS="$NOWARN_CXXFLAGS -Wno-stringop-overread"], [], [$CXXFLAG_WERROR], [AC_LANG_SOURCE([
+  #if defined(__clang__) || defined(__INTEL_COMPILER) || !defined(__GNUC__)
+    #error Non-GCC compiler detected, not setting flag
+  #endif
+  int main(void) { return 0; }
+])])
+AX_CHECK_COMPILE_FLAG([-Wstringop-overflow], [NOWARN_CXXFLAGS="$NOWARN_CXXFLAGS -Wno-stringop-overflow"], [], [$CXXFLAG_WERROR], [AC_LANG_SOURCE([
+  #if defined(__clang__) || defined(__INTEL_COMPILER) || !defined(__GNUC__)
+    #error Non-GCC compiler detected, not setting flag
+  #endif
+  int main(void) { return 0; }
+])])
+
 AX_CHECK_COMPILE_FLAG([-Wall], [WARN_CXXFLAGS="$WARN_CXXFLAGS -Wall"], [], [$CXXFLAG_WERROR])
 AX_CHECK_COMPILE_FLAG([-Wextra], [WARN_CXXFLAGS="$WARN_CXXFLAGS -Wextra"], [], [$CXXFLAG_WERROR])
 AX_CHECK_COMPILE_FLAG([-Wgnu], [WARN_CXXFLAGS="$WARN_CXXFLAGS -Wgnu"], [], [$CXXFLAG_WERROR])
diff --git a/contrib/dash-cli.bash-completion b/contrib/dash-cli.bash
similarity index 100%
rename from contrib/dash-cli.bash-completion
rename to contrib/dash-cli.bash
diff --git a/contrib/dash-tx.bash-completion b/contrib/dash-tx.bash
similarity index 100%
rename from contrib/dash-tx.bash-completion
rename to contrib/dash-tx.bash
diff --git a/contrib/dashd.bash-completion b/contrib/dashd.bash
similarity index 100%
rename from contrib/dashd.bash-completion
rename to contrib/dashd.bash
diff --git a/contrib/devtools/security-check.py b/contrib/devtools/security-check.py
index 2cbb861d57c3..001e46ceeef7 100755
--- a/contrib/devtools/security-check.py
+++ b/contrib/devtools/security-check.py
@@ -10,7 +10,7 @@
 import sys
 from typing import List
 
-import lief #type:ignore
+import lief
 
 def check_ELF_RELRO(binary) -> bool:
     '''
diff --git a/contrib/devtools/symbol-check.py b/contrib/devtools/symbol-check.py
index d6bdb193eb64..a89cd32e83d2 100755
--- a/contrib/devtools/symbol-check.py
+++ b/contrib/devtools/symbol-check.py
@@ -13,7 +13,7 @@
 import sys
 from typing import Dict, List
 
-import lief #type:ignore
+import lief
 
 # Debian 11 (Bullseye) EOL: 2026. https://wiki.debian.org/LTS
 #
diff --git a/contrib/devtools/test-security-check.py b/contrib/devtools/test-security-check.py
index 5d9dc169045a..035a6e696877 100755
--- a/contrib/devtools/test-security-check.py
+++ b/contrib/devtools/test-security-check.py
@@ -5,7 +5,7 @@
 '''
 Test script for security-check.py
 '''
-import lief #type:ignore
+import lief
 import os
 import subprocess
 from typing import List
diff --git a/contrib/testgen/README.md b/contrib/testgen/README.md
index 4d9054095571..1efe07d483a0 100644
--- a/contrib/testgen/README.md
+++ b/contrib/testgen/README.md
@@ -4,5 +4,5 @@ Utilities to generate test vectors for the data-driven Dash tests.
 
 Usage:
 
-    PYTHONPATH=../../test/functional/test_framework ./gen_key_io_test_vectors.py valid 50 > ../../src/test/data/key_io_valid.json
-    PYTHONPATH=../../test/functional/test_framework ./gen_key_io_test_vectors.py invalid 50 > ../../src/test/data/key_io_invalid.json
+    ./gen_key_io_test_vectors.py valid 50 > ../../src/test/data/key_io_valid.json
+    ./gen_key_io_test_vectors.py invalid 50 > ../../src/test/data/key_io_invalid.json
diff --git a/contrib/testgen/base58.py b/contrib/testgen/base58.py
deleted file mode 100644
index 87341ccf96dc..000000000000
--- a/contrib/testgen/base58.py
+++ /dev/null
@@ -1,115 +0,0 @@
-# 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.
-'''
-Bitcoin base58 encoding and decoding.
-
-Based on https://bitcointalk.org/index.php?topic=1026.0 (public domain)
-'''
-import hashlib
-
-# for compatibility with following code...
-class SHA256:
-    new = hashlib.sha256
-
-if str != bytes:
-    # Python 3.x
-    def ord(c):
-        return c
-    def chr(n):
-        return bytes( (n,) )
-
-__b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
-__b58base = len(__b58chars)
-b58chars = __b58chars
-
-def b58encode(v):
-    """ encode v, which is a string of bytes, to base58.
-    """
-    long_value = 0
-    for (i, c) in enumerate(v[::-1]):
-        if isinstance(c, str):
-            c = ord(c)
-        long_value += (256**i) * c
-
-    result = ''
-    while long_value >= __b58base:
-        div, mod = divmod(long_value, __b58base)
-        result = __b58chars[mod] + result
-        long_value = div
-    result = __b58chars[long_value] + result
-
-    # Bitcoin does a little leading-zero-compression:
-    # leading 0-bytes in the input become leading-1s
-    nPad = 0
-    for c in v:
-        if c == 0:
-            nPad += 1
-        else:
-            break
-
-    return (__b58chars[0]*nPad) + result
-
-def b58decode(v, length = None):
-    """ decode v into a string of len bytes
-    """
-    long_value = 0
-    for i, c in enumerate(v[::-1]):
-        pos = __b58chars.find(c)
-        assert pos != -1
-        long_value += pos * (__b58base**i)
-
-    result = bytes()
-    while long_value >= 256:
-        div, mod = divmod(long_value, 256)
-        result = chr(mod) + result
-        long_value = div
-    result = chr(long_value) + result
-
-    nPad = 0
-    for c in v:
-        if c == __b58chars[0]:
-            nPad += 1
-            continue
-        break
-
-    result = bytes(nPad) + result
-    if length is not None and len(result) != length:
-        return None
-
-    return result
-
-def checksum(v):
-    """Return 32-bit checksum based on SHA256"""
-    return SHA256.new(SHA256.new(v).digest()).digest()[0:4]
-
-def b58encode_chk(v):
-    """b58encode a string, with 32-bit checksum"""
-    return b58encode(v + checksum(v))
-
-def b58decode_chk(v):
-    """decode a base58 string, check and remove checksum"""
-    result = b58decode(v)
-    if result is None:
-        return None
-    if result[-4:] == checksum(result[:-4]):
-        return result[:-4]
-    else:
-        return None
-
-def get_bcaddress_version(strAddress):
-    """ Returns None if strAddress is invalid.  Otherwise returns integer version of address. """
-    addr = b58decode_chk(strAddress)
-    if addr is None or len(addr)!=21:
-        return None
-    version = addr[0]
-    return ord(version)
-
-if __name__ == '__main__':
-    # Test case (from http://gitorious.org/bitcoin/python-base58.git)
-    assert get_bcaddress_version('15VjRaDX9zpbA8LVnbrCAFzrVzN7ixHNsC') == 0
-    _ohai = 'o hai'.encode('ascii')
-    _tmp = b58encode(_ohai)
-    assert _tmp == 'DYB3oMS'
-    assert b58decode(_tmp, 5) == _ohai
-    print("Tests passed")
diff --git a/contrib/testgen/gen_key_io_test_vectors.py b/contrib/testgen/gen_key_io_test_vectors.py
index 94f798081975..ba045ec1c505 100755
--- a/contrib/testgen/gen_key_io_test_vectors.py
+++ b/contrib/testgen/gen_key_io_test_vectors.py
@@ -1,20 +1,24 @@
 #!/usr/bin/env python3
-# Copyright (c) 2012-2021 The Bitcoin Core developers
+# Copyright (c) 2012-2022 The Bitcoin Core developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or http://www.opensource.org/licenses/mit-license.php.
 '''
 Generate valid and invalid base58 address and private key test vectors.
 
 Usage:
-    PYTHONPATH=../../test/functional/test_framework ./gen_key_io_test_vectors.py valid 50 > ../../src/test/data/key_io_valid.json
-    PYTHONPATH=../../test/functional/test_framework ./gen_key_io_test_vectors.py invalid 50 > ../../src/test/data/key_io_invalid.json
+    ./gen_key_io_test_vectors.py valid 50 > ../../src/test/data/key_io_valid.json
+    ./gen_key_io_test_vectors.py invalid 50 > ../../src/test/data/key_io_invalid.json
 '''
-# 2012 Wladimir J. van der Laan
-# Released under MIT License
-import os
+
 from itertools import islice
-from base58 import b58encode_chk, b58decode_chk, b58chars
+import os
 import random
+import sys
+
+sys.path.append(os.path.join(os.path.dirname(__file__), '../../test/functional'))
+
+from test_framework.address import base58_to_byte, byte_to_base58, b58chars  # noqa: E402
+from test_framework.script import OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_CHECKSIG  # noqa: E402
 
 # key types
 PUBKEY_ADDRESS = 76
@@ -28,15 +32,6 @@
 PRIVKEY_REGTEST = 239
 
 # script
-OP_0 = 0x00
-OP_1 = 0x51
-OP_2 = 0x52
-OP_16 = 0x60
-OP_DUP = 0x76
-OP_EQUAL = 0x87
-OP_EQUALVERIFY = 0x88
-OP_HASH160 = 0xa9
-OP_CHECKSIG = 0xac
 pubkey_prefix = (OP_DUP, OP_HASH160, 20)
 pubkey_suffix = (OP_EQUALVERIFY, OP_CHECKSIG)
 script_prefix = (OP_HASH160, 20)
@@ -65,8 +60,10 @@ def is_valid(v):
     '''Check vector v for validity'''
     if len(set(v) - set(b58chars)) > 0:
         return False
-    result = b58decode_chk(v)
-    if result is None:
+    try:
+        payload, version = base58_to_byte(v)
+        result = bytes([version]) + payload
+    except ValueError:  # thrown if checksum doesn't match
         return False
     for template in templates:
         prefix = bytearray(template[0])
@@ -83,7 +80,8 @@ def gen_valid_base58_vector(template):
     suffix = bytearray(template[2])
     dst_prefix = bytearray(template[4])
     dst_suffix = bytearray(template[5])
-    rv = b58encode_chk(prefix + payload + suffix)
+    assert len(prefix) == 1
+    rv = byte_to_base58(payload + suffix, prefix[0])
     return rv, dst_prefix + payload + dst_suffix
 
 def gen_valid_vectors():
@@ -124,7 +122,8 @@ def gen_invalid_base58_vector(template):
     else:
         suffix = bytearray(template[2])
 
-    val = b58encode_chk(prefix + payload + suffix)
+    assert len(prefix) == 1
+    val = byte_to_base58(payload + suffix, prefix[0])
     if random.randint(0,10)<1: # line corruption
         if randbool(): # add random character to end
             val += random.choice(b58chars)
@@ -152,7 +151,6 @@ def gen_invalid_vectors():
                 yield val,
 
 if __name__ == '__main__':
-    import sys
     import json
     iters = {'valid':gen_valid_vectors, 'invalid':gen_invalid_vectors}
     try:
diff --git a/doc/REST-interface.md b/doc/REST-interface.md
index 2aa6f5ee8d45..5b25d76fc30f 100644
--- a/doc/REST-interface.md
+++ b/doc/REST-interface.md
@@ -47,18 +47,24 @@ The HTTP request and response are both handled entirely in-memory.
 With the /notxdetails/ option JSON response will only contain the transaction hash instead of the complete transaction details. The option only affects the JSON response.
 
 #### Blockheaders
-`GET /rest/headers/<COUNT>/<BLOCK-HASH>.<bin|hex|json>`
+`GET /rest/headers/<BLOCK-HASH>.<bin|hex|json>?count=<COUNT=5>`
 
 Given a block hash: returns <COUNT> amount of blockheaders in upward direction.
 Returns empty if the block doesn't exist or it isn't in the active chain.
 
+*Deprecated (but not removed) since 23.0:*
+`GET /rest/headers/<COUNT>/<BLOCK-HASH>.<bin|hex|json>`
+
 #### Blockfilter Headers
-`GET /rest/blockfilterheaders/<FILTERTYPE>/<COUNT>/<BLOCK-HASH>.<bin|hex|json>`
+`GET /rest/blockfilterheaders/<FILTERTYPE>/<BLOCK-HASH>.<bin|hex|json>?count=<COUNT=5>`
 
 Given a block hash: returns <COUNT> amount of blockfilter headers in upward
 direction for the filter type <FILTERTYPE>.
 Returns empty if the block doesn't exist or it isn't in the active chain.
 
+*Deprecated (but not removed) since 23.0:*
+`GET /rest/blockfilterheaders/<FILTERTYPE>/<COUNT>/<BLOCK-HASH>.<bin|hex|json>`
+
 #### Blockfilters
 `GET /rest/blockfilter/<FILTERTYPE>/<BLOCK-HASH>.<bin|hex|json>`
 
diff --git a/doc/descriptors.md b/doc/descriptors.md
index 6a94b867c3f5..fce4f9a323a4 100644
--- a/doc/descriptors.md
+++ b/doc/descriptors.md
@@ -73,7 +73,8 @@ Descriptors consist of several types of expressions. The top level expression is
     - Followed by zero or more `/NUM` unhardened and `/NUM'` hardened BIP32 derivation steps.
     - Optionally followed by a single `/*` or `/*'` final step to denote all (direct) unhardened or hardened children.
     - The usage of hardened derivation steps requires providing the private key.
-- Anywhere a `'` suffix is permitted to denote hardened derivation, the suffix `h` can be used instead.
+
+(Anywhere a `'` suffix is permitted to denote hardened derivation, the suffix `h` can be used instead.)
 
 `ADDR` expressions are any type of supported address:
 - P2PKH addresses (base58, of the form `X...`). Note that P2PKH addresses in descriptors cannot be used for P2PK outputs (use the `pk` function instead).
diff --git a/doc/release-note-26194.md b/doc/release-note-26194.md
new file mode 100644
index 000000000000..b72dbf9a238b
--- /dev/null
+++ b/doc/release-note-26194.md
@@ -0,0 +1,4 @@
+Add `next_index` in `listdescriptors` RPC
+-----------------
+
+- Added a new `next_index` field in the response in `listdescriptors` to have the same format as `importdescriptors`
diff --git a/doc/release-notes-24098.md b/doc/release-notes-24098.md
new file mode 100644
index 000000000000..bc74ef655878
--- /dev/null
+++ b/doc/release-notes-24098.md
@@ -0,0 +1,20 @@
+Notable changes
+===============
+
+Updated REST APIs
+-----------------
+
+- The `/headers/` and `/blockfilterheaders/` endpoints have been updated to use
+  a query parameter instead of path parameter to specify the result count. The
+  count parameter is now optional, and defaults to 5 for both endpoints. The old
+  endpoints are still functional, and have no documented behaviour change.
+
+  For `/headers`, use
+  `GET /rest/headers/<BLOCK-HASH>.<bin|hex|json>?count=<COUNT=5>`
+  instead of
+  `GET /rest/headers/<COUNT>/<BLOCK-HASH>.<bin|hex|json>` (deprecated)
+
+  For `/blockfilterheaders/`, use
+  `GET /rest/blockfilterheaders/<FILTERTYPE>/<BLOCK-HASH>.<bin|hex|json>?count=<COUNT=5>`
+  instead of
+  `GET /rest/blockfilterheaders/<FILTERTYPE>/<COUNT>/<BLOCK-HASH>.<bin|hex|json>` (deprecated)
diff --git a/doc/release-notes-24629.md b/doc/release-notes-24629.md
new file mode 100644
index 000000000000..36ccda440586
--- /dev/null
+++ b/doc/release-notes-24629.md
@@ -0,0 +1,7 @@
+Updated RPCs
+------------
+
+- The return value of the `pruneblockchain` method had an off-by-one bug,
+  returning the height of the block *after* the most recent pruned. This has
+  been corrected, and it now returns the height of the last pruned block as
+  documented.
diff --git a/doc/release-notes-25158.md b/doc/release-notes-25158.md
new file mode 100644
index 000000000000..ce8ab53ddd83
--- /dev/null
+++ b/doc/release-notes-25158.md
@@ -0,0 +1,6 @@
+RPC Wallet
+----------
+
+- The `gettransaction`, `listtransactions`, `listsinceblock` RPCs now return
+  the `abandoned` field for all transactions. Previously, the "abandoned" field
+  was only returned for sent transactions. (#25158)
\ No newline at end of file
diff --git a/doc/release-notes-27068.md b/doc/release-notes-27068.md
new file mode 100644
index 000000000000..3f5c5dba37bd
--- /dev/null
+++ b/doc/release-notes-27068.md
@@ -0,0 +1,6 @@
+Wallet
+------
+
+- Wallet passphrases may now contain null characters.
+  Prior to this change, only characters up to the first
+  null character were recognized and accepted. (#27068)
\ No newline at end of file
diff --git a/doc/release-process.md b/doc/release-process.md
index 2ebcc1582662..aca1c12e52da 100644
--- a/doc/release-process.md
+++ b/doc/release-process.md
@@ -71,7 +71,7 @@ Checkout the Dash Core version you'd like to build:
 pushd ./dash
 export SIGNER='(your builder key, ie udjinm6, pasta, etc)'
 export VERSION='(new version, e.g. 20.0.0)'
-git fetch "v${VERSION}"
+git fetch origin "v${VERSION}"
 git checkout "v${VERSION}"
 popd
 ```
diff --git a/src/Makefile.am b/src/Makefile.am
index 1b26de7005fc..ab0d3bf0f665 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -302,6 +302,7 @@ BITCOIN_CORE_H = \
   psbt.h \
   random.h \
   randomenv.h \
+  rest.h \
   rpc/blockchain.h \
   rpc/client.h \
   rpc/evo_util.h \
@@ -434,7 +435,7 @@ BITCOIN_CORE_H = \
 
 obj/build.h: FORCE
 	@$(MKDIR_P) $(builddir)/obj
-	@$(top_srcdir)/share/genbuild.sh "$(abs_top_builddir)/src/obj/build.h" \
+	$(AM_V_GEN) $(top_srcdir)/share/genbuild.sh "$(abs_top_builddir)/src/obj/build.h" \
 	  "$(abs_top_srcdir)"
 libbitcoin_util_a-clientversion.$(OBJEXT): obj/build.h
 
@@ -1071,7 +1072,7 @@ clean-local:
 	-rm -rf *.dSYM test/*.dSYM bench/*.dSYM qt/*.dSYM qt/test/*.dSYM
 
 .rc.o:
-	@test -f $(WINDRES)
+	@test -f $(WINDRES) || (echo "windres $(WINDRES) not found, but is required to compile windows resource files"; exit 1)
 	## FIXME: How to get the appropriate modulename_CPPFLAGS in here?
 	$(AM_V_GEN) $(WINDRES) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(CPPFLAGS) -DWINDRES_PREPROC -i $< -o $@
 
@@ -1130,12 +1131,11 @@ endif
 
 %.raw.h: %.raw
 	@$(MKDIR_P) $(@D)
-	@{ \
+	$(AM_V_GEN) { \
 	 echo "static unsigned const char $(*F)_raw[] = {" && \
 	 $(HEXDUMP) -v -e '8/1 "0x%02x, "' -e '"\n"' $< | $(SED) -e 's/0x  ,//g' && \
 	 echo "};"; \
 	} > "$@.new" && mv -f "$@.new" "$@"
-	@echo "Generated $@"
 
 include Makefile.minisketch.include
 
diff --git a/src/Makefile.bench.include b/src/Makefile.bench.include
index bc788ac810f8..a6ffeb1c2107 100644
--- a/src/Makefile.bench.include
+++ b/src/Makefile.bench.include
@@ -88,6 +88,7 @@ if ENABLE_WALLET
 bench_bench_dash_SOURCES += bench/coin_selection.cpp
 bench_bench_dash_SOURCES += bench/wallet_balance.cpp
 bench_bench_dash_SOURCES += bench/wallet_create.cpp
+bench_bench_dash_SOURCES += bench/wallet_loading.cpp
 bench_bench_dash_LDADD += $(BDB_LIBS) $(SQLITE_LIBS)
 endif
 
diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include
index c9f918c76467..862b8ba505dc 100644
--- a/src/Makefile.qt.include
+++ b/src/Makefile.qt.include
@@ -437,20 +437,24 @@ $(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)
 
+# 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)
 	@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"
-	$(AM_V_GEN) QT_SELECT=$(QT_SELECT) $(LCONVERT) -o $(srcdir)/qt/locale/dash_en.xlf -i $(srcdir)/qt/locale/dash_en.ts
+	$(AM_V_GEN) QT_SELECT=$(QT_SELECT) $(LCONVERT) -drop-translations -o $(srcdir)/qt/locale/dash_en.xlf -i $(srcdir)/qt/locale/dash_en.ts
+	@$(SED) -i.old -e 's|source-language="en" target-language="en"|source-language="en"|' -e '/<target xml:space="preserve"><\/target>/d' $(srcdir)/qt/locale/dash_en.xlf
+	@rm -f $(srcdir)/qt/locale/dash_en.xlf.old
 
 $(QT_QRC_LOCALE_CPP): $(QT_QRC_LOCALE) $(QT_QM)
-	@test -f $(RCC)
+	@test -f $(RCC) || (echo "rcc $(RCC) not found, but is required for generating qrc cpp files"; exit 1)
 	@cp -f $< $(@D)/temp_$(<F)
 	$(AM_V_GEN) QT_SELECT=$(QT_SELECT) $(RCC) -name dash_locale --format-version 1 $(@D)/temp_$(<F) > $@
 	@rm $(@D)/temp_$(<F)
 
 $(QT_QRC_CPP): $(QT_QRC) $(QT_FORMS_H) $(QT_RES_ICONS) $(QT_RES_IMAGES) $(QT_RES_CSS) $(QT_RES_FONTS) $(QT_RES_ANIMATION)
-	@test -f $(RCC)
+	@test -f $(RCC) || (echo "rcc $(RCC) not found, but is required for generating qrc cpp files"; exit 1)
 	$(AM_V_GEN) QT_SELECT=$(QT_SELECT) $(RCC) -name dash --format-version 1 $< > $@
 
 CLEAN_QT = $(nodist_qt_libbitcoinqt_a_SOURCES) $(QT_QM) $(QT_FORMS_H) qt/*.gcda qt/*.gcno qt/temp_dash_locale.qrc qt/res/css/colors/*
@@ -463,7 +467,7 @@ dash_qt_clean: FORCE
 dash_qt : qt/dash-qt$(EXEEXT)
 
 ui_%.h: %.ui
-	@test -f $(UIC)
+	@test -f $(UIC) || (echo "uic $(UIC) not found, but is required for generating ui headers"; exit 1)
 	@$(MKDIR_P) $(@D)
 	$(AM_V_GEN) QT_SELECT=$(QT_SELECT) $(UIC) -o $@ $< || (echo "Error creating $@"; false)
 
@@ -474,6 +478,6 @@ moc_%.cpp: %.h
 	$(AM_V_GEN) QT_SELECT=$(QT_SELECT) $(MOC) $(DEFAULT_INCLUDES) $(QT_INCLUDES_UNSUPPRESSED) $(MOC_DEFS) $< > $@
 
 %.qm: %.ts
-	@test -f $(LRELEASE)
+	@test -f $(LRELEASE) || (echo "lrelease $(LRELEASE) not found, but is required for generating translations"; exit 1)
 	@$(MKDIR_P) $(@D)
 	$(AM_V_GEN) QT_SELECT=$(QT_SELECT) $(LRELEASE) -silent $< -qm $@
diff --git a/src/Makefile.test.include b/src/Makefile.test.include
index bed7081dd6d4..b479759a8457 100644
--- a/src/Makefile.test.include
+++ b/src/Makefile.test.include
@@ -121,6 +121,7 @@ BITCOIN_TESTS =\
   test/getarg_tests.cpp \
   test/governance_validators_tests.cpp \
   test/hash_tests.cpp \
+  test/httpserver_tests.cpp \
   test/i2p_tests.cpp \
   test/interfaces_tests.cpp \
   test/key_io_tests.cpp \
@@ -156,6 +157,7 @@ BITCOIN_TESTS =\
   test/raii_event_tests.cpp \
   test/random_tests.cpp \
   test/ratecheck_tests.cpp \
+  test/rest_tests.cpp \
   test/reverselock_tests.cpp \
   test/rpc_tests.cpp \
   test/sanity_tests.cpp \
@@ -222,6 +224,7 @@ BITCOIN_TESTS += wallet/test/db_tests.cpp
 endif
 
 FUZZ_WALLET_SRC = \
+ wallet/test/fuzz/coincontrol.cpp \
  wallet/test/fuzz/coinselection.cpp
 
 if USE_SQLITE
@@ -443,10 +446,9 @@ endif
 
 %.json.h: %.json
 	@$(MKDIR_P) $(@D)
-	@{ \
+	$(AM_V_GEN) { \
 	 echo "namespace json_tests{" && \
 	 echo "static unsigned const char $(*F)[] = {" && \
 	 $(HEXDUMP) -v -e '8/1 "0x%02x, "' -e '"\n"' $< | $(SED) -e 's/0x  ,//g' && \
 	 echo "};};"; \
 	} > "$@.new" && mv -f "$@.new" "$@"
-	@echo "Generated $@"
diff --git a/src/bench/util_time.cpp b/src/bench/util_time.cpp
index afc733482e8a..c45584988d9a 100644
--- a/src/bench/util_time.cpp
+++ b/src/bench/util_time.cpp
@@ -32,7 +32,7 @@ static void BenchTimeMillis(benchmark::Bench& bench)
 static void BenchTimeMillisSys(benchmark::Bench& bench)
 {
     bench.run([&] {
-        (void)GetTimeMillis();
+        (void)TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now());
     });
 }
 
diff --git a/src/bench/wallet_loading.cpp b/src/bench/wallet_loading.cpp
new file mode 100644
index 000000000000..7fdacc24529a
--- /dev/null
+++ b/src/bench/wallet_loading.cpp
@@ -0,0 +1,133 @@
+// Copyright (c) 2022 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 <bench/bench.h>
+#include <interfaces/chain.h>
+#include <node/context.h>
+#include <test/util/mining.h>
+#include <test/util/setup_common.h>
+#include <test/util/wallet.h>
+#include <util/translation.h>
+#include <validationinterface.h>
+#include <wallet/context.h>
+#include <wallet/receive.h>
+#include <wallet/wallet.h>
+
+#include <optional>
+
+using wallet::CWallet;
+using wallet::DatabaseFormat;
+using wallet::DatabaseOptions;
+using wallet::ISMINE_SPENDABLE;
+using wallet::MakeWalletDatabase;
+using wallet::TxStateInactive;
+using wallet::WALLET_FLAG_DESCRIPTORS;
+using wallet::WalletContext;
+using wallet::WalletDatabase;
+
+static const std::shared_ptr<CWallet> BenchLoadWallet(std::unique_ptr<WalletDatabase> database, WalletContext& context, DatabaseOptions& options)
+{
+    bilingual_str error;
+    std::vector<bilingual_str> warnings;
+    auto wallet = CWallet::Create(context, "", std::move(database), options.create_flags, error, warnings);
+    NotifyWalletLoaded(context, wallet);
+    if (context.chain) {
+        wallet->postInitProcess();
+    }
+    return wallet;
+}
+
+static void BenchUnloadWallet(std::shared_ptr<CWallet>&& wallet)
+{
+    SyncWithValidationInterfaceQueue();
+    wallet->m_chain_notifications_handler.reset();
+    UnloadWallet(std::move(wallet));
+}
+
+static void AddTx(CWallet& wallet)
+{
+    bilingual_str error;
+    CTxDestination dest;
+    wallet.GetNewDestination("", dest, error);
+
+    CMutableTransaction mtx;
+    mtx.vout.push_back({COIN, GetScriptForDestination(dest)});
+    mtx.vin.push_back(CTxIn());
+
+    wallet.AddToWallet(MakeTransactionRef(mtx), TxStateInactive{});
+}
+
+static std::unique_ptr<WalletDatabase> DuplicateMockDatabase(WalletDatabase& database, DatabaseOptions& options)
+{
+    auto new_database = CreateMockWalletDatabase(options);
+
+    // Get a cursor to the original database
+    auto batch = database.MakeBatch();
+    batch->StartCursor();
+
+    // Get a batch for the new database
+    auto new_batch = new_database->MakeBatch();
+
+    // Read all records from the original database and write them to the new one
+    while (true) {
+        CDataStream key(SER_DISK, CLIENT_VERSION);
+        CDataStream value(SER_DISK, CLIENT_VERSION);
+        bool complete;
+        batch->ReadAtCursor(key, value, complete);
+        if (complete) break;
+        new_batch->Write(key, value);
+    }
+
+    return new_database;
+}
+
+static void WalletLoading(benchmark::Bench& bench, bool legacy_wallet)
+{
+    const auto test_setup = MakeNoLogFileContext<TestingSetup>();
+    test_setup->m_args.ForceSetArg("-unsafesqlitesync", "1");
+
+    WalletContext context;
+    context.args = &test_setup->m_args;
+    context.chain = test_setup->m_node.chain.get();
+
+    // Setup the wallet
+    // Loading the wallet will also create it
+    DatabaseOptions options;
+    if (legacy_wallet) {
+        options.require_format = DatabaseFormat::BERKELEY;
+    } else {
+        options.create_flags = WALLET_FLAG_DESCRIPTORS;
+        options.require_format = DatabaseFormat::SQLITE;
+    }
+    auto database = CreateMockWalletDatabase(options);
+    auto wallet = BenchLoadWallet(std::move(database), context, options);
+
+    // Generate a bunch of transactions and addresses to put into the wallet
+    for (int i = 0; i < 1000; ++i) {
+        AddTx(*wallet);
+    }
+
+    database = DuplicateMockDatabase(wallet->GetDatabase(), options);
+
+    // reload the wallet for the actual benchmark
+    BenchUnloadWallet(std::move(wallet));
+
+    bench.epochs(5).run([&] {
+        wallet = BenchLoadWallet(std::move(database), context, options);
+
+        // Cleanup
+        database = DuplicateMockDatabase(wallet->GetDatabase(), options);
+        BenchUnloadWallet(std::move(wallet));
+    });
+}
+
+#ifdef USE_BDB
+static void WalletLoadingLegacy(benchmark::Bench& bench) { WalletLoading(bench, /*legacy_wallet=*/true); }
+BENCHMARK(WalletLoadingLegacy);
+#endif
+
+#ifdef USE_SQLITE
+static void WalletLoadingDescriptors(benchmark::Bench& bench) { WalletLoading(bench, /*legacy_wallet=*/false); }
+BENCHMARK(WalletLoadingDescriptors);
+#endif
diff --git a/src/chain.cpp b/src/chain.cpp
index 2480047d024f..765c4d55a6ba 100644
--- a/src/chain.cpp
+++ b/src/chain.cpp
@@ -19,11 +19,9 @@ std::string CBlockIndex::ToString() const
                      pprev, nHeight, hashMerkleRoot.ToString(), GetBlockHash().ToString());
 }
 
-void CChain::SetTip(CBlockIndex *pindex) {
-    if (pindex == nullptr) {
-        vChain.clear();
-        return;
-    }
+void CChain::SetTip(CBlockIndex& block)
+{
+    CBlockIndex* pindex = &block;
     vChain.resize(pindex->nHeight + 1);
     while (pindex && vChain[pindex->nHeight] != pindex) {
         vChain[pindex->nHeight] = pindex;
diff --git a/src/chain.h b/src/chain.h
index f7fe7584936b..2fdac98e85c0 100644
--- a/src/chain.h
+++ b/src/chain.h
@@ -470,7 +470,7 @@ class CChain
     }
 
     /** Set/initialize a chain with a given tip. */
-    void SetTip(CBlockIndex* pindex);
+    void SetTip(CBlockIndex& block);
 
     /** Return a CBlockLocator that refers to a block in this chain (by default the tip). */
     CBlockLocator GetLocator(const CBlockIndex* pindex = nullptr) const;
diff --git a/src/crypto/sha256_arm_shani.cpp b/src/crypto/sha256_arm_shani.cpp
index 7ffa56be7060..2ea1d9c2acc4 100644
--- a/src/crypto/sha256_arm_shani.cpp
+++ b/src/crypto/sha256_arm_shani.cpp
@@ -62,7 +62,7 @@ void Transform(uint32_t* s, const unsigned char* chunk, size_t blocks)
         MSG3 = vreinterpretq_u32_u8(vrev32q_u8(vld1q_u8(chunk + 48)));
         chunk += 64;
 
-        // Original implemenation preloaded message and constant addition which was 1-3% slower.
+        // Original implementation preloaded message and constant addition which was 1-3% slower.
         // Now included as first step in quad round code saving one Q Neon register
         // "TMP0 = vaddq_u32(MSG0, vld1q_u32(&K[0]));"
 
diff --git a/src/evo/cbtx.cpp b/src/evo/cbtx.cpp
index 0ea054809917..af8ea39deb0a 100644
--- a/src/evo/cbtx.cpp
+++ b/src/evo/cbtx.cpp
@@ -43,23 +43,6 @@ bool CheckCbTx(const CCbTx& cbTx, const CBlockIndex* pindexPrev, TxValidationSta
     return true;
 }
 
-bool CheckCbTxMerkleRoots(const CBlock& block, const CCbTx& cbTx, const CBlockIndex* pindex,
-                          const llmq::CQuorumBlockProcessor& quorum_block_processor, BlockValidationState& state)
-{
-    if (pindex && cbTx.nVersion >= CCbTx::Version::MERKLE_ROOT_QUORUMS) {
-        uint256 calculatedMerkleRoot;
-        if (!CalcCbTxMerkleRootQuorums(block, pindex->pprev, quorum_block_processor, calculatedMerkleRoot, state)) {
-            // pass the state returned by the function above
-            return false;
-        }
-        if (calculatedMerkleRoot != cbTx.merkleRootQuorums) {
-            return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-cbtx-quorummerkleroot");
-        }
-    }
-
-    return true;
-}
-
 using QcHashMap = std::map<Consensus::LLMQType, std::vector<uint256>>;
 using QcIndexedHashMap = std::map<Consensus::LLMQType, std::map<int16_t, uint256>>;
 
diff --git a/src/evo/cbtx.h b/src/evo/cbtx.h
index c9087f5866b0..482d880dd522 100644
--- a/src/evo/cbtx.h
+++ b/src/evo/cbtx.h
@@ -65,9 +65,6 @@ template<> struct is_serializable_enum<CCbTx::Version> : std::true_type {};
 
 bool CheckCbTx(const CCbTx& cbTx, const CBlockIndex* pindexPrev, TxValidationState& state);
 
-// This can only be done after the block has been fully processed, as otherwise we won't have the finished MN list
-bool CheckCbTxMerkleRoots(const CBlock& block, const CCbTx& cbTx, const CBlockIndex* pindex,
-                          const llmq::CQuorumBlockProcessor& quorum_block_processor, BlockValidationState& state);
 bool CalcCbTxMerkleRootQuorums(const CBlock& block, const CBlockIndex* pindexPrev,
                                const llmq::CQuorumBlockProcessor& quorum_block_processor, uint256& merkleRootRet,
                                BlockValidationState& state);
diff --git a/src/evo/mnhftx.h b/src/evo/mnhftx.h
index 8de6ef6c5d59..2dc38919cf48 100644
--- a/src/evo/mnhftx.h
+++ b/src/evo/mnhftx.h
@@ -50,7 +50,7 @@ class MNHFTx
     [[nodiscard]] UniValue ToJson() const
     {
         UniValue obj(UniValue::VOBJ);
-        obj.pushKV("versionBit", (int)versionBit);
+        obj.pushKV("versionBit", versionBit);
         obj.pushKV("quorumHash", quorumHash.ToString());
         obj.pushKV("sig", sig.ToString());
         return obj;
diff --git a/src/evo/specialtxman.cpp b/src/evo/specialtxman.cpp
index 162446a14d99..f0141922f460 100644
--- a/src/evo/specialtxman.cpp
+++ b/src/evo/specialtxman.cpp
@@ -497,7 +497,7 @@ bool CSpecialTxProcessor::ProcessSpecialTxsInBlock(const CBlock& block, const CB
         static int64_t nTimeQuorum = 0;
         static int64_t nTimeDMN = 0;
         static int64_t nTimeMerkleMNL = 0;
-        static int64_t nTimeMerkle = 0;
+        static int64_t nTimeMerkleQuorums = 0;
         static int64_t nTimeCbTxCL = 0;
         static int64_t nTimeMnehf = 0;
         static int64_t nTimePayload = 0;
@@ -599,62 +599,63 @@ bool CSpecialTxProcessor::ProcessSpecialTxsInBlock(const CBlock& block, const CB
             }
         }
 
-            int64_t nTime5_1 = GetTimeMicros();
-            nTimeDMN += nTime5_1 - nTime5;
-
-            LogPrint(BCLog::BENCHMARK, "      - m_dmnman: %.2fms [%.2fs]\n", 0.001 * (nTime5_1 - nTime5),
-                     nTimeDMN * 0.000001);
+        int64_t nTime6 = GetTimeMicros();
+        nTimeDMN += nTime6 - nTime5;
+        LogPrint(BCLog::BENCHMARK, "      - m_dmnman: %.2fms [%.2fs]\n", 0.001 * (nTime6 - nTime5), nTimeDMN * 0.000001);
 
         if (opt_cbTx.has_value()) {
-            uint256 calculatedMerkleRoot;
-            if (!CalcCbTxMerkleRootMNList(calculatedMerkleRoot, mn_list.to_sml(), state)) {
+            uint256 calculatedMerkleRootMNL;
+            if (!CalcCbTxMerkleRootMNList(calculatedMerkleRootMNL, mn_list.to_sml(), state)) {
                 // pass the state returned by the function above
                 return false;
             }
-            if (calculatedMerkleRoot != opt_cbTx->merkleRootMNList) {
+            if (calculatedMerkleRootMNL != opt_cbTx->merkleRootMNList) {
                 return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-cbtx-mnmerkleroot");
             }
 
-            int64_t nTime5_2 = GetTimeMicros();
-            nTimeMerkleMNL += nTime5_2 - nTime5_1;
+            int64_t nTime6_1 = GetTimeMicros();
+            nTimeMerkleMNL += nTime6_1 - nTime6;
             LogPrint(BCLog::BENCHMARK, "      - CalcCbTxMerkleRootMNList: %.2fms [%.2fs]\n",
-                     0.001 * (nTime5_2 - nTime5_1), nTimeMerkleMNL * 0.000001);
-        }
+                     0.001 * (nTime6_1 - nTime6), nTimeMerkleMNL * 0.000001);
 
-        int64_t nTime6 = GetTimeMicros();
-        if (opt_cbTx.has_value()) {
-            if (!CheckCbTxMerkleRoots(block, *opt_cbTx, pindex, m_qblockman, state)) {
-                // pass the state returned by the function above
-                return false;
+            if (opt_cbTx->nVersion >= CCbTx::Version::MERKLE_ROOT_QUORUMS) {
+                uint256 calculatedMerkleRootQuorums;
+                if (!CalcCbTxMerkleRootQuorums(block, pindex->pprev, m_qblockman, calculatedMerkleRootQuorums, state)) {
+                    // pass the state returned by the function above
+                    return false;
+                }
+                if (calculatedMerkleRootQuorums != opt_cbTx->merkleRootQuorums) {
+                    return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-cbtx-quorummerkleroot");
+                }
             }
-        }
 
-        int64_t nTime7 = GetTimeMicros();
-        nTimeMerkle += nTime7 - nTime6;
+            int64_t nTime6_2 = GetTimeMicros();
+            nTimeMerkleQuorums += nTime6_2 - nTime6_1;
 
-        LogPrint(BCLog::BENCHMARK, "      - CheckCbTxMerkleRoots: %.2fms [%.2fs]\n", 0.001 * (nTime7 - nTime6),
-                 nTimeMerkle * 0.000001);
+            LogPrint(BCLog::BENCHMARK, "      - CalcCbTxMerkleRootQuorums: %.2fms [%.2fs]\n",
+                     0.001 * (nTime6_2 - nTime6_1), nTimeMerkleQuorums * 0.000001);
 
-        if (opt_cbTx.has_value()) {
             if (!CheckCbTxBestChainlock(*opt_cbTx, pindex, m_clhandler, state)) {
                 // pass the state returned by the function above
                 return false;
             }
+
+            int64_t nTime6_3 = GetTimeMicros();
+            nTimeCbTxCL += nTime6_3 - nTime6_2;
+            LogPrint(BCLog::BENCHMARK, "      - CheckCbTxBestChainlock: %.2fms [%.2fs]\n",
+                     0.001 * (nTime6_3 - nTime6_2), nTimeCbTxCL * 0.000001);
         }
 
-        int64_t nTime8 = GetTimeMicros();
-        nTimeCbTxCL += nTime8 - nTime7;
-        LogPrint(BCLog::BENCHMARK, "      - CheckCbTxBestChainlock: %.2fms [%.2fs]\n", 0.001 * (nTime8 - nTime7),
-                 nTimeCbTxCL * 0.000001);
+        int64_t nTime7 = GetTimeMicros();
 
         if (!m_mnhfman.ProcessBlock(block, pindex, fJustCheck, state)) {
             // pass the state returned by the function above
             return false;
         }
 
-        int64_t nTime9 = GetTimeMicros();
-        nTimeMnehf += nTime9 - nTime8;
-        LogPrint(BCLog::BENCHMARK, "      - m_mnhfman: %.2fms [%.2fs]\n", 0.001 * (nTime9 - nTime8), nTimeMnehf * 0.000001);
+        int64_t nTime8 = GetTimeMicros();
+        nTimeMnehf += nTime8 - nTime7;
+        LogPrint(BCLog::BENCHMARK, "      - m_mnhfman: %.2fms [%.2fs]\n", 0.001 * (nTime8 - nTime7), nTimeMnehf * 0.000001);
 
         if (DeploymentActiveAfter(pindex, m_consensus_params, Consensus::DEPLOYMENT_V19) && bls::bls_legacy_scheme.load()) {
             // NOTE: The block next to the activation is the one that is using new rules.
diff --git a/src/httpserver.cpp b/src/httpserver.cpp
index 51d8c3e0d8c2..dc4d6c5dc0d4 100644
--- a/src/httpserver.cpp
+++ b/src/httpserver.cpp
@@ -21,15 +21,17 @@
 
 #include <cstdio>
 #include <deque>
+#include <optional>
 #include <string>
 
 #include <sys/types.h>
 
-#include <event2/thread.h>
 #include <event2/buffer.h>
 #include <event2/bufferevent.h>
-#include <event2/util.h>
+#include <event2/http.h>
 #include <event2/keyvalq_struct.h>
+#include <event2/thread.h>
+#include <event2/util.h>
 
 #include <support/events.h>
 
@@ -670,6 +672,37 @@ HTTPRequest::RequestMethod HTTPRequest::GetRequestMethod() const
     }
 }
 
+std::optional<std::string> HTTPRequest::GetQueryParameter(const std::string& key) const
+{
+    const char* uri{evhttp_request_get_uri(req)};
+
+    return GetQueryParameterFromUri(uri, key);
+}
+
+std::optional<std::string> GetQueryParameterFromUri(const char* uri, const std::string& key)
+{
+    evhttp_uri* uri_parsed{evhttp_uri_parse(uri)};
+    const char* query{evhttp_uri_get_query(uri_parsed)};
+    std::optional<std::string> result;
+
+    if (query) {
+        // Parse the query string into a key-value queue and iterate over it
+        struct evkeyvalq params_q;
+        evhttp_parse_query_str(query, &params_q);
+
+        for (struct evkeyval* param{params_q.tqh_first}; param != nullptr; param = param->next.tqe_next) {
+            if (param->key == key) {
+                result = param->value;
+                break;
+            }
+        }
+        evhttp_clear_headers(&params_q);
+    }
+    evhttp_uri_free(uri_parsed);
+
+    return result;
+}
+
 void RegisterHTTPHandler(const std::string &prefix, bool exactMatch, const HTTPRequestHandler &handler)
 {
     LogPrint(BCLog::HTTP, "Registering HTTP handler for %s (exactmatch %d)\n", prefix, exactMatch);
diff --git a/src/httpserver.h b/src/httpserver.h
index 92c8cf1843a8..5ab3f189273a 100644
--- a/src/httpserver.h
+++ b/src/httpserver.h
@@ -5,8 +5,9 @@
 #ifndef BITCOIN_HTTPSERVER_H
 #define BITCOIN_HTTPSERVER_H
 
-#include <string>
 #include <functional>
+#include <optional>
+#include <string>
 
 static const int DEFAULT_HTTP_THREADS=4;
 static const int DEFAULT_HTTP_WORKQUEUE=16;
@@ -82,6 +83,17 @@ class HTTPRequest
      */
     RequestMethod GetRequestMethod() const;
 
+    /** Get the query parameter value from request uri for a specified key, or std::nullopt if the
+     * key is not found.
+     *
+     * If the query string contains duplicate keys, the first value is returned. Many web frameworks
+     * would instead parse this as an array of values, but this is not (yet) implemented as it is
+     * currently not needed in any of the endpoints.
+     *
+     * @param[in] key represents the query parameter of which the value is returned
+     */
+    std::optional<std::string> GetQueryParameter(const std::string& key) const;
+
     /**
      * Get the request header specified by hdr, or an empty string.
      * Return a pair (isPresent,string).
@@ -114,6 +126,20 @@ class HTTPRequest
     void WriteReply(int nStatus, const std::string& strReply = "");
 };
 
+/** Get the query parameter value from request uri for a specified key, or std::nullopt if the key
+ * is not found.
+ *
+ * If the query string contains duplicate keys, the first value is returned. Many web frameworks
+ * would instead parse this as an array of values, but this is not (yet) implemented as it is
+ * currently not needed in any of the endpoints.
+ *
+ * Helper function for HTTPRequest::GetQueryParameter.
+ *
+ * @param[in] uri is the entire request uri
+ * @param[in] key represents the query parameter of which the value is returned
+ */
+std::optional<std::string> GetQueryParameterFromUri(const char* uri, const std::string& key);
+
 /** Event handler closure.
  */
 class HTTPClosure
diff --git a/src/init.cpp b/src/init.cpp
index f748a4e93416..15929f489831 100644
--- a/src/init.cpp
+++ b/src/init.cpp
@@ -591,7 +591,6 @@ void SetupServerArgs(ArgsManager& argsman)
     argsman.AddArg("-peerbloomfilters", strprintf("Support filtering of blocks and transaction with bloom filters (default: %u)", DEFAULT_PEERBLOOMFILTERS), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
     argsman.AddArg("-txreconciliation", strprintf("Enable transaction reconciliations per BIP 330 (default: %d)", DEFAULT_TXRECONCILIATION_ENABLE), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CONNECTION);
     argsman.AddArg("-peertimeout=<n>", strprintf("Specify a p2p connection timeout delay in seconds. After connecting to a peer, wait this amount of time before considering disconnection based on inactivity (minimum: 1, default: %d)", DEFAULT_PEER_CONNECT_TIMEOUT), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
-    argsman.AddArg("-permitbaremultisig", strprintf("Relay non-P2SH multisig (default: %u)", DEFAULT_PERMIT_BAREMULTISIG), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
     // TODO: remove the sentence "Nodes not using ... incoming connections." once the changes from
     // https://github.com/bitcoin/bitcoin/pull/23542 have become widespread.
     argsman.AddArg("-port=<port>", strprintf("Listen for connections on <port>. Nodes not using the default ports (default: %u, testnet: %u, regtest: %u) are unlikely to get incoming connections. Not relevant for I2P (see doc/i2p.md).", defaultChainParams->GetDefaultPort(), testnetChainParams->GetDefaultPort(), regtestChainParams->GetDefaultPort()), ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::CONNECTION);
@@ -746,6 +745,8 @@ void SetupServerArgs(ArgsManager& argsman)
     argsman.AddArg("-bytespersigop", strprintf("Equivalent bytes per sigop in transactions for relay and mining (default: %u)", DEFAULT_BYTES_PER_SIGOP), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
     argsman.AddArg("-datacarrier", strprintf("Relay and mine data carrier transactions (default: %u)", DEFAULT_ACCEPT_DATACARRIER), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
     argsman.AddArg("-datacarriersize", strprintf("Maximum size of data in data carrier transactions we relay and mine (default: %u)", MAX_OP_RETURN_RELAY), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
+    argsman.AddArg("-permitbaremultisig", strprintf("Relay non-P2SH multisig (default: %u)", DEFAULT_PERMIT_BAREMULTISIG), ArgsManager::ALLOW_ANY,
+                   OptionsCategory::NODE_RELAY);
     argsman.AddArg("-minrelaytxfee=<amt>", strprintf("Fees (in %s/kB) smaller than this are considered zero fee for relaying, mining and transaction creation (default: %s)",
         CURRENCY_UNIT, FormatMoney(DEFAULT_MIN_RELAY_TX_FEE)), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
     argsman.AddArg("-whitelistforcerelay", strprintf("Add 'forcerelay' permission to whitelisted inbound peers with default permissions. This will relay transactions even if the transactions were already in the mempool. (default: %d)", DEFAULT_WHITELISTFORCERELAY), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
@@ -1040,7 +1041,7 @@ namespace { // Variables internal to initialization process only
 int nMaxConnections;
 int nUserMaxConnections;
 int nFD;
-ServiceFlags nLocalServices = ServiceFlags(NODE_NETWORK | NODE_NETWORK_LIMITED | NODE_HEADERS_COMPRESSED);
+ServiceFlags nLocalServices = ServiceFlags(NODE_NETWORK_LIMITED | NODE_HEADERS_COMPRESSED);
 int64_t peer_connect_timeout;
 std::set<BlockFilterType> g_enabled_filter_types;
 
@@ -2220,12 +2221,9 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
     }
     // ********************************************************* Step 10: data directory maintenance
 
-
-    // if pruning, unset the service bit and perform the initial blockstore prune
+    // if pruning, perform the initial blockstore prune
     // after any wallet rescanning has taken place.
     if (fPruneMode) {
-        LogPrintf("Unsetting NODE_NETWORK on prune mode\n");
-        nLocalServices = ServiceFlags(nLocalServices & ~NODE_NETWORK);
         if (!fReindex) {
             LOCK(cs_main);
             for (CChainState* chainstate : chainman.GetAll()) {
@@ -2233,6 +2231,9 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
                 chainstate->PruneAndFlush();
             }
         }
+    } else {
+        LogPrintf("Setting NODE_NETWORK on non-prune mode\n");
+        nLocalServices = ServiceFlags(nLocalServices | NODE_NETWORK);
     }
 
     // As PruneAndFlush can take several minutes, it's possible the user
diff --git a/src/instantsend/instantsend.cpp b/src/instantsend/instantsend.cpp
index 1e92ea074a41..4a228bc18537 100644
--- a/src/instantsend/instantsend.cpp
+++ b/src/instantsend/instantsend.cpp
@@ -155,7 +155,7 @@ PeerMsgRet CInstantSendManager::ProcessMessageInstantSendLock(const CNode& pfrom
             LOCK(cs_timingsTxSeen);
             if (auto it = timingsTxSeen.find(islock->txid); it != timingsTxSeen.end()) {
                 // This is the normal case where we received the TX before the islock
-                auto diff = GetTimeMillis() - it->second;
+                auto diff = TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now()) - it->second;
                 timingsTxSeen.erase(it);
                 return diff;
             }
@@ -551,7 +551,7 @@ void CInstantSendManager::AddNonLockedTx(const CTransactionRef& tx, const CBlock
     if (ShouldReportISLockTiming()) {
         LOCK(cs_timingsTxSeen);
         // Only insert the time the first time we see the tx, as we sometimes try to resign
-        timingsTxSeen.try_emplace(tx->GetHash(), GetTimeMillis());
+        timingsTxSeen.try_emplace(tx->GetHash(), TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now()));
     }
 
     LogPrint(BCLog::INSTANTSEND, "CInstantSendManager::%s -- txid=%s, pindexMined=%s\n", __func__,
diff --git a/src/llmq/chainlocks.cpp b/src/llmq/chainlocks.cpp
index 8e8917180ae8..155e1117260a 100644
--- a/src/llmq/chainlocks.cpp
+++ b/src/llmq/chainlocks.cpp
@@ -111,7 +111,7 @@ MessageProcessingResult CChainLocksHandler::ProcessNewChainLock(const NodeId fro
 
     {
         LOCK(cs);
-        if (!seenChainLocks.emplace(hash, GetTimeMillis()).second) {
+        if (!seenChainLocks.emplace(hash, TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now())).second) {
             return {};
         }
 
@@ -611,15 +611,15 @@ void CChainLocksHandler::Cleanup()
         return;
     }
 
-    if (GetTimeMillis() - lastCleanupTime < CLEANUP_INTERVAL) {
+    if (TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now()) - lastCleanupTime < CLEANUP_INTERVAL) {
         return;
     }
-    lastCleanupTime = GetTimeMillis();
+    lastCleanupTime = TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now());
 
     {
         LOCK(cs);
         for (auto it = seenChainLocks.begin(); it != seenChainLocks.end(); ) {
-            if (GetTimeMillis() - it->second >= CLEANUP_SEEN_TIMEOUT) {
+            if (TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now()) - it->second >= CLEANUP_SEEN_TIMEOUT) {
                 it = seenChainLocks.erase(it);
             } else {
                 ++it;
diff --git a/src/llmq/debug.cpp b/src/llmq/debug.cpp
index 0952c3f52192..953ece2b3d08 100644
--- a/src/llmq/debug.cpp
+++ b/src/llmq/debug.cpp
@@ -34,7 +34,7 @@ UniValue CDKGDebugSessionStatus::ToJson(CDeterministicMNManager& dmnman, CQuorum
 
     ret.pushKV("llmqType", ToUnderlying(llmqType));
     ret.pushKV("quorumHash", quorumHash.ToString());
-    ret.pushKV("quorumHeight", (int)quorumHeight);
+    ret.pushKV("quorumHeight", quorumHeight);
     ret.pushKV("phase", ToUnderlying(phase));
 
     ret.pushKV("sentContributions", statusBits.sentContributions);
@@ -61,10 +61,10 @@ UniValue CDKGDebugSessionStatus::ToJson(CDeterministicMNManager& dmnman, CQuorum
             if (detailLevel == 0) {
                 v.count++;
             } else if (detailLevel == 1) {
-                v.arr.push_back((int)idx);
+                v.arr.push_back(idx);
             } else if (detailLevel == 2) {
                 UniValue a(UniValue::VOBJ);
-                a.pushKV("memberIndex", (int)idx);
+                a.pushKV("memberIndex", idx);
                 if (idx < dmnMembers.size()) {
                     a.pushKV("proTxHash", dmnMembers[idx]->proTxHash.ToString());
                 }
diff --git a/src/llmq/dkgsessionhandler.cpp b/src/llmq/dkgsessionhandler.cpp
index beb111533e9e..7ef53456c8d4 100644
--- a/src/llmq/dkgsessionhandler.cpp
+++ b/src/llmq/dkgsessionhandler.cpp
@@ -301,13 +301,13 @@ void CDKGSessionHandler::SleepBeforePhase(QuorumPhase curPhase,
     double adjustedPhaseSleepTimePerMember = phaseSleepTimePerMember * randomSleepFactor;
 
     int64_t sleepTime = (int64_t)(adjustedPhaseSleepTimePerMember * curSession->GetMyMemberIndex().value_or(0));
-    int64_t endTime = GetTimeMillis() + sleepTime;
+    int64_t endTime = TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now()) + sleepTime;
     int heightTmp{currentHeight.load()};
     int heightStart{heightTmp};
 
     LogPrint(BCLog::LLMQ_DKG, "CDKGSessionManager::%s -- %s qi[%d] - starting sleep for %d ms, curPhase=%d\n", __func__, params.name, quorumIndex, sleepTime, ToUnderlying(curPhase));
 
-    while (GetTimeMillis() < endTime) {
+    while (TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now()) < endTime) {
         if (stopRequested) {
             LogPrint(BCLog::LLMQ_DKG, "CDKGSessionManager::%s -- %s qi[%d] - aborting due to stop/shutdown requested\n", __func__, params.name, quorumIndex);
             throw AbortPhaseException();
diff --git a/src/llmq/dkgsessionmgr.cpp b/src/llmq/dkgsessionmgr.cpp
index 6386d9487ab4..49ca664a597f 100644
--- a/src/llmq/dkgsessionmgr.cpp
+++ b/src/llmq/dkgsessionmgr.cpp
@@ -317,6 +317,9 @@ bool CDKGSessionManager::GetVerifiedContributions(Consensus::LLMQType llmqType,
             if (it == contributionsCache.end()) {
                 CDataStream s(SER_DISK, CLIENT_VERSION);
                 if (!db->ReadDataStream(std::make_tuple(DB_VVEC, llmqType, pQuorumBaseBlockIndex->GetBlockHash(), proTxHash), s)) {
+                    LogPrint(BCLog::LLMQ, "%s -- this node does not have vvec for llmq=%d block=%s protx=%s\n",
+                             __func__, ToUnderlying(llmqType), pQuorumBaseBlockIndex->GetBlockHash().ToString(),
+                             proTxHash.ToString());
                     return false;
                 }
                 size_t vvec_size = ReadCompactSize(s);
@@ -331,7 +334,7 @@ bool CDKGSessionManager::GetVerifiedContributions(Consensus::LLMQType llmqType,
                 CBLSSecretKey skContribution;
                 db->Read(std::make_tuple(DB_SKCONTRIB, llmqType, pQuorumBaseBlockIndex->GetBlockHash(), proTxHash), skContribution);
 
-                it = contributionsCache.emplace(cacheKey, ContributionsCacheEntry{GetTimeMillis(), vvecPtr, skContribution}).first;
+                it = contributionsCache.emplace(cacheKey, ContributionsCacheEntry{TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now()), vvecPtr, skContribution}).first;
             }
 
             memberIndexesRet.emplace_back(i);
@@ -378,7 +381,7 @@ bool CDKGSessionManager::GetEncryptedContributions(Consensus::LLMQType llmqType,
 void CDKGSessionManager::CleanupCache() const
 {
     LOCK(contributionsCacheCs);
-    auto curTime = GetTimeMillis();
+    auto curTime = TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now());
     for (auto it = contributionsCache.begin(); it != contributionsCache.end(); ) {
         if (curTime - it->second.entryTime > MAX_CONTRIBUTION_CACHE_TIME) {
             it = contributionsCache.erase(it);
diff --git a/src/llmq/quorums.cpp b/src/llmq/quorums.cpp
index 6c1dc379bd4e..86e1f29c6240 100644
--- a/src/llmq/quorums.cpp
+++ b/src/llmq/quorums.cpp
@@ -923,11 +923,11 @@ void CQuorumManager::StartQuorumDataRecoveryThread(CConnman& connman, const CQuo
 {
     assert(m_mn_activeman);
 
-    if (pQuorum->fQuorumDataRecoveryThreadRunning) {
+    bool expected = false;
+    if (!pQuorum->fQuorumDataRecoveryThreadRunning.compare_exchange_strong(expected, true)) {
         LogPrint(BCLog::LLMQ, "CQuorumManager::%s -- Already running\n", __func__);
         return;
     }
-    pQuorum->fQuorumDataRecoveryThreadRunning = true;
 
     workerPool.push([&connman, pQuorum, pIndex, nDataMaskIn, this](int threadId) {
         size_t nTries{0};
diff --git a/src/llmq/signing.cpp b/src/llmq/signing.cpp
index f543cd399087..b05375c63d62 100644
--- a/src/llmq/signing.cpp
+++ b/src/llmq/signing.cpp
@@ -656,7 +656,7 @@ void CSigningManager::TruncateRecoveredSig(Consensus::LLMQType llmqType, const u
 
 void CSigningManager::Cleanup()
 {
-    int64_t now = GetTimeMillis();
+    int64_t now = TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now());
     if (now - lastCleanupTime < 5000) {
         return;
     }
@@ -666,7 +666,7 @@ void CSigningManager::Cleanup()
     db.CleanupOldRecoveredSigs(maxAge);
     db.CleanupOldVotes(maxAge);
 
-    lastCleanupTime = GetTimeMillis();
+    lastCleanupTime = TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now());
 }
 
 void CSigningManager::RegisterRecoveredSigsListener(CRecoveredSigsListener* l)
diff --git a/src/llmq/signing_shares.cpp b/src/llmq/signing_shares.cpp
index 35aaddbb7fc6..d0b5d3ed8d22 100644
--- a/src/llmq/signing_shares.cpp
+++ b/src/llmq/signing_shares.cpp
@@ -1488,9 +1488,9 @@ void CSigSharesManager::WorkThreadMain(CConnman& connman, PeerManager& peerman)
         bool fMoreWork = ProcessPendingSigShares(peerman, connman);
         SignPendingSigShares(connman, peerman);
 
-        if (GetTimeMillis() - lastSendTime > 100) {
+        if (TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now()) - lastSendTime > 100) {
             SendMessages(connman);
-            lastSendTime = GetTimeMillis();
+            lastSendTime = TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now());
         }
 
         Cleanup(connman);
diff --git a/src/masternode/sync.cpp b/src/masternode/sync.cpp
index c653778fd401..fca0f0cde5cf 100644
--- a/src/masternode/sync.cpp
+++ b/src/masternode/sync.cpp
@@ -119,7 +119,7 @@ void CMasternodeSync::ProcessTick(const PeerManager& peerman, const CGovernanceM
     static int nTick = 0;
     nTick++;
 
-    const static int64_t nSyncStart = GetTimeMillis();
+    const static int64_t nSyncStart = TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now());
     const static std::string strAllow = strprintf("allow-sync-%lld", nSyncStart);
 
     // reset the sync process if the last call to this function was more than 60 minutes ago (client was in sleep mode)
diff --git a/src/net.cpp b/src/net.cpp
index 659841ee5dbd..5c44be1d8307 100644
--- a/src/net.cpp
+++ b/src/net.cpp
@@ -2117,9 +2117,9 @@ void CConnman::DisconnectNodes()
                         //   1. vSendMsg must be empty and all messages sent via send(). This is ensured by SocketHandler()
                         //      being called before DisconnectNodes and also by the linger time
                         //   2. Internal socket send buffers must be flushed. This is ensured solely by the linger time
-                        pnode->nDisconnectLingerTime = GetTimeMillis() + 100;
+                        pnode->nDisconnectLingerTime = TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now()) + 100;
                     }
-                    if (GetTimeMillis() < pnode->nDisconnectLingerTime) {
+                    if (TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now()) < pnode->nDisconnectLingerTime) {
                         // everything flushed to the kernel?
                         const auto& [to_send, more, _msg_type] = pnode->m_transport->GetBytesToSend(pnode->nSendMsgSize != 0);
                         const bool queue_is_empty{to_send.empty() && !more};
@@ -2638,11 +2638,11 @@ void CConnman::ThreadSocketHandler(CMasternodeSync& mn_sync)
         // Handle sockets before we do the next round of disconnects. This allows us to flush send buffers one last time
         // before actually closing sockets. Receiving is however skipped in case a peer is pending to be disconnected
         SocketHandler(mn_sync);
-        if (GetTimeMillis() - nLastCleanupNodes > 1000) {
+        if (TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now()) - nLastCleanupNodes > 1000) {
             ForEachNode(AllNodes, [&](CNode* pnode) {
                 if (InactivityCheck(*pnode)) pnode->fDisconnect = true;
             });
-            nLastCleanupNodes = GetTimeMillis();
+            nLastCleanupNodes = TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now());
         }
         DisconnectNodes();
         NotifyNumConnectionsChanged(mn_sync);
@@ -3650,9 +3650,9 @@ void CConnman::ThreadMessageHandler()
         bool fMoreWork = false;
 
         bool fSkipSendMessagesForMasternodes = true;
-        if (GetTimeMillis() - nLastSendMessagesTimeMasternodes >= 100) {
+        if (TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now()) - nLastSendMessagesTimeMasternodes >= 100) {
             fSkipSendMessagesForMasternodes = false;
-            nLastSendMessagesTimeMasternodes = GetTimeMillis();
+            nLastSendMessagesTimeMasternodes = TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now());
         }
 
         // Randomize the order in which we process messages from/to our peers.
diff --git a/src/net.h b/src/net.h
index 2dfc9d32b08a..957aff1b4fd3 100644
--- a/src/net.h
+++ b/src/net.h
@@ -1935,7 +1935,6 @@ friend class CNode;
      */
     static constexpr size_t MAX_UNUSED_I2P_SESSIONS_SIZE{10};
 
-    friend struct CConnmanTest;
     friend struct ConnmanTestMsg;
 };
 
diff --git a/src/netbase.cpp b/src/netbase.cpp
index 850044f00c3d..95d95f3f31cc 100644
--- a/src/netbase.cpp
+++ b/src/netbase.cpp
@@ -320,7 +320,7 @@ enum class IntrRecvError {
  */
 static IntrRecvError InterruptibleRecv(uint8_t* data, size_t len, int timeout, const Sock& sock)
 {
-    int64_t curTime = GetTimeMillis();
+    int64_t curTime = TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now());
     int64_t endTime = curTime + timeout;
     while (len > 0 && curTime < endTime) {
         ssize_t ret = sock.Recv(data, len, 0); // Optimistically try the recv first
@@ -345,7 +345,7 @@ static IntrRecvError InterruptibleRecv(uint8_t* data, size_t len, int timeout, c
         }
         if (interruptSocks5Recv)
             return IntrRecvError::Interrupted;
-        curTime = GetTimeMillis();
+        curTime = TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now());
     }
     return len == 0 ? IntrRecvError::OK : IntrRecvError::Timeout;
 }
diff --git a/src/outputtype.cpp b/src/outputtype.cpp
index c2c69dd9b72d..440372c0b5ea 100644
--- a/src/outputtype.cpp
+++ b/src/outputtype.cpp
@@ -25,9 +25,6 @@ bool ParseOutputType(const std::string& type, OutputType& output_type)
     if (type == OUTPUT_TYPE_STRING_LEGACY) {
         output_type = OutputType::LEGACY;
         return true;
-    } else if (type == OUTPUT_TYPE_STRING_UNKNOWN) {
-        output_type = OutputType::UNKNOWN;
-        return true;
     }
     return false;
 }
diff --git a/src/qt/askpassphrasedialog.cpp b/src/qt/askpassphrasedialog.cpp
index 829a7660491b..3b9fd117ffe6 100644
--- a/src/qt/askpassphrasedialog.cpp
+++ b/src/qt/askpassphrasedialog.cpp
@@ -106,11 +106,10 @@ void AskPassphraseDialog::accept()
     oldpass.reserve(MAX_PASSPHRASE_SIZE);
     newpass1.reserve(MAX_PASSPHRASE_SIZE);
     newpass2.reserve(MAX_PASSPHRASE_SIZE);
-    // TODO: get rid of this .c_str() by implementing SecureString::operator=(std::string)
-    // Alternately, find a way to make this input mlock()'d to begin with.
-    oldpass.assign(ui->passEdit1->text().toStdString().c_str());
-    newpass1.assign(ui->passEdit2->text().toStdString().c_str());
-    newpass2.assign(ui->passEdit3->text().toStdString().c_str());
+
+    oldpass.assign(std::string_view{ui->passEdit1->text().toStdString()});
+    newpass1.assign(std::string_view{ui->passEdit2->text().toStdString()});
+    newpass2.assign(std::string_view{ui->passEdit3->text().toStdString()});
 
     secureClearPassFields();
 
@@ -185,8 +184,19 @@ void AskPassphraseDialog::accept()
         try {
             if(!model->setWalletLocked(false, oldpass, mode == UnlockMixing))
             {
-                QMessageBox::critical(this, tr("Wallet unlock failed"),
-                                      tr("The passphrase entered for the wallet decryption was incorrect."));
+                // Check if the passphrase has a null character (see #27067 for details)
+                if (oldpass.find('\0') == std::string::npos) {
+                    QMessageBox::critical(this, tr("Wallet unlock failed"),
+                                          tr("The passphrase entered for the wallet decryption was incorrect."));
+                } else {
+                    QMessageBox::critical(this, tr("Wallet unlock failed"),
+                                          tr("The passphrase entered for the wallet decryption is incorrect. "
+                                             "It contains a null character (ie - a zero byte). "
+                                             "If the passphrase was set with a version of this software prior to 23.0, "
+                                             "please try again with only the characters up to — but not including — "
+                                             "the first null character. If this is successful, please set a new "
+                                             "passphrase to avoid this issue in the future."));
+                }
             }
             else
             {
@@ -207,8 +217,18 @@ void AskPassphraseDialog::accept()
             }
             else
             {
-                QMessageBox::critical(this, tr("Wallet encryption failed"),
-                                     tr("The passphrase entered for the wallet decryption was incorrect."));
+                // Check if the old passphrase had a null character (see #27067 for details)
+                if (oldpass.find('\0') == std::string::npos) {
+                    QMessageBox::critical(this, tr("Passphrase change failed"),
+                                          tr("The passphrase entered for the wallet decryption was incorrect."));
+                } else {
+                    QMessageBox::critical(this, tr("Passphrase change failed"),
+                                          tr("The old passphrase entered for the wallet decryption is incorrect. "
+                                             "It contains a null character (ie - a zero byte). "
+                                             "If the passphrase was set with a version of this software prior to 23.0, "
+                                             "please try again with only the characters up to — but not including — "
+                                             "the first null character."));
+                }
             }
         }
         else
diff --git a/src/qt/bitcoin.cpp b/src/qt/bitcoin.cpp
index 3b99604d8868..e78adbb79884 100644
--- a/src/qt/bitcoin.cpp
+++ b/src/qt/bitcoin.cpp
@@ -171,6 +171,7 @@ static void initTranslations(QTranslator &qtTranslatorBase, QTranslator &qtTrans
 
 static bool InitSettings()
 {
+    gArgs.EnsureDataDir();
     if (!gArgs.GetSettingsPath()) {
         return true; // Do nothing if settings file disabled.
     }
diff --git a/src/qt/clientmodel.cpp b/src/qt/clientmodel.cpp
index 7b20ce368b97..fb6b90e86ce7 100644
--- a/src/qt/clientmodel.cpp
+++ b/src/qt/clientmodel.cpp
@@ -263,7 +263,7 @@ void ClientModel::TipChanged(SynchronizationState sync_state, interfaces::BlockT
 
     // Throttle GUI notifications about (a) blocks during initial sync, and (b) both blocks and headers during reindex.
     const bool throttle = (sync_state != SynchronizationState::POST_INIT && !header) || sync_state == SynchronizationState::INIT_REINDEX;
-    const int64_t now = throttle ? GetTimeMillis() : 0;
+    const int64_t now = throttle ? TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now()) : 0;
     int64_t& nLastUpdateNotification = header ? nLastHeaderTipUpdateNotification : nLastBlockTipUpdateNotification;
     if (throttle && now < nLastUpdateNotification + count_milliseconds(MODEL_UPDATE_DELAY)) {
         return;
diff --git a/src/rest.cpp b/src/rest.cpp
index 790b55af47ac..56b4c5861407 100644
--- a/src/rest.cpp
+++ b/src/rest.cpp
@@ -3,6 +3,8 @@
 // Distributed under the MIT software license, see the accompanying
 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
 
+#include <rest.h>
+
 #include <blockfilter.h>
 #include <chain.h>
 #include <chainparams.h>
@@ -30,6 +32,8 @@
 #include <validation.h>
 #include <version.h>
 
+#include <string>
+
 #include <univalue.h>
 
 using node::GetTransaction;
@@ -39,21 +43,14 @@ using node::ReadBlockFromDisk;
 static const size_t MAX_GETUTXOS_OUTPOINTS = 15; //allow a max of 15 outpoints to be queried at once
 static constexpr unsigned int MAX_REST_HEADERS_RESULTS = 2000;
 
-enum class RetFormat {
-    UNDEF,
-    BINARY,
-    HEX,
-    JSON,
-};
-
 static const struct {
-    RetFormat rf;
+    RESTResponseFormat rf;
     const char* name;
 } rf_names[] = {
-      {RetFormat::UNDEF, ""},
-      {RetFormat::BINARY, "bin"},
-      {RetFormat::HEX, "hex"},
-      {RetFormat::JSON, "json"},
+      {RESTResponseFormat::UNDEF, ""},
+      {RESTResponseFormat::BINARY, "bin"},
+      {RESTResponseFormat::HEX, "hex"},
+      {RESTResponseFormat::JSON, "json"},
 };
 
 struct CCoin {
@@ -157,25 +154,28 @@ static LLMQContext* GetLLMQContext(const CoreContext& context, HTTPRequest* req)
     return node_context->llmq_ctx.get();
 }
 
-static RetFormat ParseDataFormat(std::string& param, const std::string& strReq)
+RESTResponseFormat ParseDataFormat(std::string& param, const std::string& strReq)
 {
-    const std::string::size_type pos = strReq.rfind('.');
-    if (pos == std::string::npos)
-    {
-        param = strReq;
+    // Remove query string (if any, separated with '?') as it should not interfere with
+    // parsing param and data format
+    param = strReq.substr(0, strReq.rfind('?'));
+    const std::string::size_type pos_format{param.rfind('.')};
+
+    // No format string is found
+    if (pos_format == std::string::npos) {
         return rf_names[0].rf;
     }
 
-    param = strReq.substr(0, pos);
-    const std::string suff(strReq, pos + 1);
-
+    // Match format string to available formats
+    const std::string suffix(param, pos_format + 1);
     for (const auto& rf_name : rf_names) {
-        if (suff == rf_name.name)
+        if (suffix == rf_name.name) {
+            param.erase(pos_format);
             return rf_name.rf;
+        }
     }
 
-    /* If no suffix is found, return original string.  */
-    param = strReq;
+    // If no suffix is found, return RESTResponseFormat::UNDEF and original string without query string
     return rf_names[0].rf;
 }
 
@@ -211,18 +211,28 @@ static bool rest_headers(const CoreContext& context,
     if (!CheckWarmup(req))
         return false;
     std::string param;
-    const RetFormat rf = ParseDataFormat(param, strURIPart);
+    const RESTResponseFormat rf = ParseDataFormat(param, strURIPart);
     std::vector<std::string> path = SplitString(param, '/');
 
-    if (path.size() != 2)
-        return RESTERR(req, HTTP_BAD_REQUEST, "No header count specified. Use /rest/headers/<count>/<hash>.<ext>.");
-
-    const auto parsed_count{ToIntegral<size_t>(path[0])};
+    std::string raw_count;
+    std::string hashStr;
+    if (path.size() == 2) {
+        // deprecated path: /rest/headers/<count>/<hash>
+        hashStr = path[1];
+        raw_count = path[0];
+    } else if (path.size() == 1) {
+        // new path with query parameter: /rest/headers/<hash>?count=<count>
+        hashStr = path[0];
+        raw_count = req->GetQueryParameter("count").value_or("5");
+    } else {
+        return RESTERR(req, HTTP_BAD_REQUEST, "Invalid URI format. Expected /rest/headers/<hash>.<ext>?count=<count>");
+    }
+
+    const auto parsed_count{ToIntegral<size_t>(raw_count)};
     if (!parsed_count.has_value() || *parsed_count < 1 || *parsed_count > MAX_REST_HEADERS_RESULTS) {
-        return RESTERR(req, HTTP_BAD_REQUEST, strprintf("Header count is invalid or out of acceptable range (1-%u): %s", MAX_REST_HEADERS_RESULTS, path[0]));
+        return RESTERR(req, HTTP_BAD_REQUEST, strprintf("Header count is invalid or out of acceptable range (1-%u): %s", MAX_REST_HEADERS_RESULTS, raw_count));
     }
 
-    std::string hashStr = path[1];
     uint256 hash;
     if (!ParseHashStr(hashStr, hash))
         return RESTERR(req, HTTP_BAD_REQUEST, "Invalid hash: " + hashStr);
@@ -248,7 +258,7 @@ static bool rest_headers(const CoreContext& context,
     }
 
     switch (rf) {
-    case RetFormat::BINARY: {
+    case RESTResponseFormat::BINARY: {
         CDataStream ssHeader(SER_NETWORK, PROTOCOL_VERSION);
         for (const CBlockIndex *pindex : headers) {
             ssHeader << pindex->GetBlockHeader();
@@ -260,7 +270,7 @@ static bool rest_headers(const CoreContext& context,
         return true;
     }
 
-    case RetFormat::HEX: {
+    case RESTResponseFormat::HEX: {
         CDataStream ssHeader(SER_NETWORK, PROTOCOL_VERSION);
         for (const CBlockIndex *pindex : headers) {
             ssHeader << pindex->GetBlockHeader();
@@ -271,7 +281,7 @@ static bool rest_headers(const CoreContext& context,
         req->WriteReply(HTTP_OK, strHex);
         return true;
     }
-    case RetFormat::JSON: {
+    case RESTResponseFormat::JSON: {
         const LLMQContext* llmq_ctx = GetLLMQContext(context, req);
         if (!llmq_ctx) return false;
 
@@ -298,7 +308,7 @@ static bool rest_block(const CoreContext& context,
     if (!CheckWarmup(req))
         return false;
     std::string hashStr;
-    const RetFormat rf = ParseDataFormat(hashStr, strURIPart);
+    const RESTResponseFormat rf = ParseDataFormat(hashStr, strURIPart);
 
     uint256 hash;
     if (!ParseHashStr(hashStr, hash))
@@ -327,7 +337,7 @@ static bool rest_block(const CoreContext& context,
     }
 
     switch (rf) {
-    case RetFormat::BINARY: {
+    case RESTResponseFormat::BINARY: {
         CDataStream ssBlock(SER_NETWORK, PROTOCOL_VERSION);
         ssBlock << block;
         std::string binaryBlock = ssBlock.str();
@@ -336,7 +346,7 @@ static bool rest_block(const CoreContext& context,
         return true;
     }
 
-    case RetFormat::HEX: {
+    case RESTResponseFormat::HEX: {
         CDataStream ssBlock(SER_NETWORK, PROTOCOL_VERSION);
         ssBlock << block;
         std::string strHex = HexStr(ssBlock) + "\n";
@@ -345,7 +355,7 @@ static bool rest_block(const CoreContext& context,
         return true;
     }
 
-    case RetFormat::JSON: {
+    case RESTResponseFormat::JSON: {
         const LLMQContext* llmq_ctx = GetLLMQContext(context, req);
         if (!llmq_ctx) return false;
 
@@ -377,16 +387,31 @@ static bool rest_filter_header(const CoreContext& context, HTTPRequest* req, con
     if (!CheckWarmup(req)) return false;
 
     std::string param;
-    const RetFormat rf = ParseDataFormat(param, strURIPart);
+    const RESTResponseFormat rf = ParseDataFormat(param, strURIPart);
 
     std::vector<std::string> uri_parts = SplitString(param, '/');
-    if (uri_parts.size() != 3) {
-        return RESTERR(req, HTTP_BAD_REQUEST, "Invalid URI format. Expected /rest/blockfilterheaders/<filtertype>/<count>/<blockhash>");
+    std::string raw_count;
+    std::string raw_blockhash;
+    if (uri_parts.size() == 3) {
+        // deprecated path: /rest/blockfilterheaders/<filtertype>/<count>/<blockhash>
+        raw_blockhash = uri_parts[2];
+        raw_count = uri_parts[1];
+    } else if (uri_parts.size() == 2) {
+        // new path with query parameter: /rest/blockfilterheaders/<filtertype>/<blockhash>?count=<count>
+        raw_blockhash = uri_parts[1];
+        raw_count = req->GetQueryParameter("count").value_or("5");
+    } else {
+        return RESTERR(req, HTTP_BAD_REQUEST, "Invalid URI format. Expected /rest/blockfilterheaders/<filtertype>/<blockhash>.<ext>?count=<count>");
+    }
+
+    const auto parsed_count{ToIntegral<size_t>(raw_count)};
+    if (!parsed_count.has_value() || *parsed_count < 1 || *parsed_count > MAX_REST_HEADERS_RESULTS) {
+        return RESTERR(req, HTTP_BAD_REQUEST, strprintf("Header count is invalid or out of acceptable range (1-%u): %s", MAX_REST_HEADERS_RESULTS, raw_count));
     }
 
     uint256 block_hash;
-    if (!ParseHashStr(uri_parts[2], block_hash)) {
-        return RESTERR(req, HTTP_BAD_REQUEST, "Invalid hash: " + uri_parts[2]);
+    if (!ParseHashStr(raw_blockhash, block_hash)) {
+        return RESTERR(req, HTTP_BAD_REQUEST, "Invalid hash: " + raw_blockhash);
     }
 
     BlockFilterType filtertype;
@@ -399,11 +424,6 @@ static bool rest_filter_header(const CoreContext& context, HTTPRequest* req, con
         return RESTERR(req, HTTP_BAD_REQUEST, "Index is not enabled for filtertype " + uri_parts[0]);
     }
 
-    const auto parsed_count{ToIntegral<size_t>(uri_parts[1])};
-    if (!parsed_count.has_value() || *parsed_count < 1 || *parsed_count > MAX_REST_HEADERS_RESULTS) {
-        return RESTERR(req, HTTP_BAD_REQUEST, strprintf("Header count is invalid or out of acceptable range (1-%u): %s", MAX_REST_HEADERS_RESULTS, uri_parts[1]));
-    }
-
     std::vector<const CBlockIndex*> headers;
     headers.reserve(*parsed_count);
     {
@@ -442,7 +462,7 @@ static bool rest_filter_header(const CoreContext& context, HTTPRequest* req, con
     }
 
     switch (rf) {
-    case RetFormat::BINARY: {
+    case RESTResponseFormat::BINARY: {
         CDataStream ssHeader{SER_NETWORK, PROTOCOL_VERSION};
         for (const uint256& header : filter_headers) {
             ssHeader << header;
@@ -453,7 +473,7 @@ static bool rest_filter_header(const CoreContext& context, HTTPRequest* req, con
         req->WriteReply(HTTP_OK, binaryHeader);
         return true;
     }
-    case RetFormat::HEX: {
+    case RESTResponseFormat::HEX: {
         CDataStream ssHeader{SER_NETWORK, PROTOCOL_VERSION};
         for (const uint256& header : filter_headers) {
             ssHeader << header;
@@ -464,7 +484,7 @@ static bool rest_filter_header(const CoreContext& context, HTTPRequest* req, con
         req->WriteReply(HTTP_OK, strHex);
         return true;
     }
-    case RetFormat::JSON: {
+    case RESTResponseFormat::JSON: {
         UniValue jsonHeaders(UniValue::VARR);
         for (const uint256& header : filter_headers) {
             jsonHeaders.push_back(header.GetHex());
@@ -486,7 +506,7 @@ static bool rest_block_filter(const CoreContext& context, HTTPRequest* req, cons
     if (!CheckWarmup(req)) return false;
 
     std::string param;
-    const RetFormat rf = ParseDataFormat(param, strURIPart);
+    const RESTResponseFormat rf = ParseDataFormat(param, strURIPart);
 
     // request is sent over URI scheme /rest/blockfilter/filtertype/blockhash
     std::vector<std::string> uri_parts = SplitString(param, '/');
@@ -541,7 +561,7 @@ static bool rest_block_filter(const CoreContext& context, HTTPRequest* req, cons
     }
 
     switch (rf) {
-    case RetFormat::BINARY: {
+    case RESTResponseFormat::BINARY: {
         CDataStream ssResp{SER_NETWORK, PROTOCOL_VERSION};
         ssResp << filter;
 
@@ -550,7 +570,7 @@ static bool rest_block_filter(const CoreContext& context, HTTPRequest* req, cons
         req->WriteReply(HTTP_OK, binaryResp);
         return true;
     }
-    case RetFormat::HEX: {
+    case RESTResponseFormat::HEX: {
         CDataStream ssResp{SER_NETWORK, PROTOCOL_VERSION};
         ssResp << filter;
 
@@ -559,7 +579,7 @@ static bool rest_block_filter(const CoreContext& context, HTTPRequest* req, cons
         req->WriteReply(HTTP_OK, strHex);
         return true;
     }
-    case RetFormat::JSON: {
+    case RESTResponseFormat::JSON: {
         UniValue ret(UniValue::VOBJ);
         ret.pushKV("filter", HexStr(filter.GetEncodedFilter()));
         std::string strJSON = ret.write() + "\n";
@@ -581,10 +601,10 @@ static bool rest_chaininfo(const CoreContext& context, HTTPRequest* req, const s
     if (!CheckWarmup(req))
         return false;
     std::string param;
-    const RetFormat rf = ParseDataFormat(param, strURIPart);
+    const RESTResponseFormat rf = ParseDataFormat(param, strURIPart);
 
     switch (rf) {
-    case RetFormat::JSON: {
+    case RESTResponseFormat::JSON: {
         JSONRPCRequest jsonRequest;
         jsonRequest.context = context;
         jsonRequest.params = UniValue(UniValue::VARR);
@@ -607,10 +627,10 @@ static bool rest_mempool_info(const CoreContext& context, HTTPRequest* req, cons
     const CTxMemPool* mempool = GetMemPool(context, req);
     if (!mempool) return false;
     std::string param;
-    const RetFormat rf = ParseDataFormat(param, strURIPart);
+    const RESTResponseFormat rf = ParseDataFormat(param, strURIPart);
 
     switch (rf) {
-    case RetFormat::JSON: {
+    case RESTResponseFormat::JSON: {
         const LLMQContext* llmq_ctx = GetLLMQContext(context, req);
         if (!llmq_ctx) return false;
 
@@ -633,10 +653,10 @@ static bool rest_mempool_contents(const CoreContext& context, HTTPRequest* req,
     const CTxMemPool* mempool = GetMemPool(context, req);
     if (!mempool) return false;
     std::string param;
-    const RetFormat rf = ParseDataFormat(param, strURIPart);
+    const RESTResponseFormat rf = ParseDataFormat(param, strURIPart);
 
     switch (rf) {
-    case RetFormat::JSON: {
+    case RESTResponseFormat::JSON: {
         const LLMQContext* llmq_ctx = GetLLMQContext(context, req);
         if (!llmq_ctx) return false;
 
@@ -658,7 +678,7 @@ static bool rest_tx(const CoreContext& context, HTTPRequest* req, const std::str
     if (!CheckWarmup(req))
         return false;
     std::string hashStr;
-    const RetFormat rf = ParseDataFormat(hashStr, strURIPart);
+    const RESTResponseFormat rf = ParseDataFormat(hashStr, strURIPart);
 
     uint256 hash;
     if (!ParseHashStr(hashStr, hash))
@@ -677,7 +697,7 @@ static bool rest_tx(const CoreContext& context, HTTPRequest* req, const std::str
     }
 
     switch (rf) {
-    case RetFormat::BINARY: {
+    case RESTResponseFormat::BINARY: {
         CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
         ssTx << tx;
 
@@ -687,7 +707,7 @@ static bool rest_tx(const CoreContext& context, HTTPRequest* req, const std::str
         return true;
     }
 
-    case RetFormat::HEX: {
+    case RESTResponseFormat::HEX: {
         CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
         ssTx << tx;
 
@@ -697,7 +717,7 @@ static bool rest_tx(const CoreContext& context, HTTPRequest* req, const std::str
         return true;
     }
 
-    case RetFormat::JSON: {
+    case RESTResponseFormat::JSON: {
         UniValue objTx(UniValue::VOBJ);
         TxToUniv(*tx, /*block_hash=*/hashBlock, /*entry=*/objTx);
         std::string strJSON = objTx.write() + "\n";
@@ -717,7 +737,7 @@ static bool rest_getutxos(const CoreContext& context, HTTPRequest* req, const st
     if (!CheckWarmup(req))
         return false;
     std::string param;
-    const RetFormat rf = ParseDataFormat(param, strURIPart);
+    const RESTResponseFormat rf = ParseDataFormat(param, strURIPart);
 
     std::vector<std::string> uriParts;
     if (param.length() > 1)
@@ -764,14 +784,14 @@ static bool rest_getutxos(const CoreContext& context, HTTPRequest* req, const st
     }
 
     switch (rf) {
-    case RetFormat::HEX: {
+    case RESTResponseFormat::HEX: {
         // convert hex to bin, continue then with bin part
         std::vector<unsigned char> strRequestV = ParseHex(strRequestMutable);
         strRequestMutable.assign(strRequestV.begin(), strRequestV.end());
         [[fallthrough]];
     }
 
-    case RetFormat::BINARY: {
+    case RESTResponseFormat::BINARY: {
         try {
             //deserialize only if user sent a request
             if (strRequestMutable.size() > 0)
@@ -791,7 +811,7 @@ static bool rest_getutxos(const CoreContext& context, HTTPRequest* req, const st
         break;
     }
 
-    case RetFormat::JSON: {
+    case RESTResponseFormat::JSON: {
         if (!fInputParsed)
             return RESTERR(req, HTTP_BAD_REQUEST, "Error: empty request");
         break;
@@ -845,7 +865,7 @@ static bool rest_getutxos(const CoreContext& context, HTTPRequest* req, const st
     }
 
     switch (rf) {
-    case RetFormat::BINARY: {
+    case RESTResponseFormat::BINARY: {
         // serialize data
         // use exact same output as mentioned in Bip64
         CDataStream ssGetUTXOResponse(SER_NETWORK, PROTOCOL_VERSION);
@@ -857,7 +877,7 @@ static bool rest_getutxos(const CoreContext& context, HTTPRequest* req, const st
         return true;
     }
 
-    case RetFormat::HEX: {
+    case RESTResponseFormat::HEX: {
         CDataStream ssGetUTXOResponse(SER_NETWORK, PROTOCOL_VERSION);
         ssGetUTXOResponse << chainman.ActiveChain().Height() << chainman.ActiveChain().Tip()->GetBlockHash() << bitmap << outs;
         std::string strHex = HexStr(ssGetUTXOResponse) + "\n";
@@ -867,7 +887,7 @@ static bool rest_getutxos(const CoreContext& context, HTTPRequest* req, const st
         return true;
     }
 
-    case RetFormat::JSON: {
+    case RESTResponseFormat::JSON: {
         UniValue objGetUTXOResponse(UniValue::VOBJ);
 
         // pack in some essentials
@@ -907,7 +927,7 @@ static bool rest_blockhash_by_height(const CoreContext& context, HTTPRequest* re
 {
     if (!CheckWarmup(req)) return false;
     std::string height_str;
-    const RetFormat rf = ParseDataFormat(height_str, str_uri_part);
+    const RESTResponseFormat rf = ParseDataFormat(height_str, str_uri_part);
 
     int32_t blockheight = -1; // Initialization done only to prevent valgrind false positive, see https://github.com/bitcoin/bitcoin/pull/18785
     if (!ParseInt32(height_str, &blockheight) || blockheight < 0) {
@@ -927,19 +947,19 @@ static bool rest_blockhash_by_height(const CoreContext& context, HTTPRequest* re
         pblockindex = active_chain[blockheight];
     }
     switch (rf) {
-    case RetFormat::BINARY: {
+    case RESTResponseFormat::BINARY: {
         CDataStream ss_blockhash(SER_NETWORK, PROTOCOL_VERSION);
         ss_blockhash << pblockindex->GetBlockHash();
         req->WriteHeader("Content-Type", "application/octet-stream");
         req->WriteReply(HTTP_OK, ss_blockhash.str());
         return true;
     }
-    case RetFormat::HEX: {
+    case RESTResponseFormat::HEX: {
         req->WriteHeader("Content-Type", "text/plain");
         req->WriteReply(HTTP_OK, pblockindex->GetBlockHash().GetHex() + "\n");
         return true;
     }
-    case RetFormat::JSON: {
+    case RESTResponseFormat::JSON: {
         req->WriteHeader("Content-Type", "application/json");
         UniValue resp = UniValue(UniValue::VOBJ);
         resp.pushKV("blockhash", pblockindex->GetBlockHash().GetHex());
diff --git a/src/rest.h b/src/rest.h
new file mode 100644
index 000000000000..49b1c333d052
--- /dev/null
+++ b/src/rest.h
@@ -0,0 +1,28 @@
+// Copyright (c) 2015-2022 The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#ifndef BITCOIN_REST_H
+#define BITCOIN_REST_H
+
+#include <string>
+
+enum class RESTResponseFormat {
+    UNDEF,
+    BINARY,
+    HEX,
+    JSON,
+};
+
+/**
+ * Parse a URI to get the data format and URI without data format
+ * and query string.
+ *
+ * @param[out]  param   The strReq without the data format string and
+ *                      without the query string (if any).
+ * @param[in]   strReq  The URI to be parsed.
+ * @return      RESTResponseFormat that was parsed from the URI.
+ */
+RESTResponseFormat ParseDataFormat(std::string& param, const std::string& strReq);
+
+#endif // BITCOIN_REST_H
diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp
index ae6ca2b23ea8..9ae25ef11ba3 100644
--- a/src/rpc/blockchain.cpp
+++ b/src/rpc/blockchain.cpp
@@ -1086,7 +1086,7 @@ static RPCHelpMan pruneblockchain()
     const CBlockIndex& block{*CHECK_NONFATAL(active_chain.Tip())};
     const CBlockIndex* last_block{active_chainstate.m_blockman.GetFirstStoredBlock(block)};
 
-    return static_cast<uint64_t>(last_block->nHeight);
+    return static_cast<int64_t>(last_block->nHeight - 1);
 },
     };
 }
@@ -1457,7 +1457,7 @@ RPCHelpMan getblockchaininfo()
                 {RPCResult::Type::STR_HEX, "chainwork", "total amount of work in active chain, in hexadecimal"},
                 {RPCResult::Type::NUM, "size_on_disk", "the estimated size of the block and undo files on disk"},
                 {RPCResult::Type::BOOL, "pruned", "if the blocks are subject to pruning"},
-                {RPCResult::Type::NUM, "pruneheight", /* optional */ true, "lowest-height complete block stored (only present if pruning is enabled)"},
+                {RPCResult::Type::NUM, "pruneheight", /* optional */ true, "height of the last block pruned, plus one (only present if pruning is enabled)"},
                 {RPCResult::Type::BOOL, "automatic_pruning", /* optional */ true, "whether automatic pruning is enabled (only present if pruning is enabled)"},
                 {RPCResult::Type::NUM, "prune_target_size", /* optional */ true, "the target size used by pruning (only present if automatic pruning is enabled)"},
                 {RPCResult::Type::OBJ, "softforks", "status of softforks in progress",
@@ -2671,6 +2671,12 @@ static RPCHelpMan dumptxoutset()
 
     FILE* file{fsbridge::fopen(temppath, "wb")};
     CAutoFile afile{file, SER_DISK, CLIENT_VERSION};
+    if (afile.IsNull()) {
+        throw JSONRPCError(
+            RPC_INVALID_PARAMETER,
+            "Couldn't open file " + temppath.utf8string() + " for writing.");
+    }
+
     NodeContext& node = EnsureAnyNodeContext(request.context);
     UniValue result = CreateUTXOSnapshot(
         node, node.chainman->ActiveChainstate(), afile, path, temppath);
diff --git a/src/rpc/net.cpp b/src/rpc/net.cpp
index 69612e171dd1..4b44ab63aa99 100644
--- a/src/rpc/net.cpp
+++ b/src/rpc/net.cpp
@@ -542,15 +542,15 @@ static RPCHelpMan getaddednodeinfo()
 static RPCHelpMan getnettotals()
 {
     return RPCHelpMan{"getnettotals",
-        "\nReturns information about network traffic, including bytes in, bytes out,\n"
-        "and current time.\n",
+        "Returns information about network traffic, including bytes in, bytes out,\n"
+        "and current system time.",
         {},
         RPCResult{
            RPCResult::Type::OBJ, "", "",
            {
                {RPCResult::Type::NUM, "totalbytesrecv", "Total bytes received"},
                {RPCResult::Type::NUM, "totalbytessent", "Total bytes sent"},
-               {RPCResult::Type::NUM_TIME, "timemillis", "Current " + UNIX_EPOCH_TIME + " in milliseconds"},
+               {RPCResult::Type::NUM_TIME, "timemillis", "Current system " + UNIX_EPOCH_TIME + " in milliseconds"},
                {RPCResult::Type::OBJ, "uploadtarget", "",
                {
                    {RPCResult::Type::NUM, "timeframe", "Length of the measuring timeframe in seconds"},
@@ -574,7 +574,7 @@ static RPCHelpMan getnettotals()
     UniValue obj(UniValue::VOBJ);
     obj.pushKV("totalbytesrecv", connman.GetTotalBytesRecv());
     obj.pushKV("totalbytessent", connman.GetTotalBytesSent());
-    obj.pushKV("timemillis", GetTimeMillis());
+    obj.pushKV("timemillis", TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now()));
 
     UniValue outboundLimit(UniValue::VOBJ);
     outboundLimit.pushKV("timeframe", count_seconds(connman.GetMaxOutboundTimeframe()));
diff --git a/src/support/allocators/secure.h b/src/support/allocators/secure.h
index 14676dbe4d8e..46be5f4876f1 100644
--- a/src/support/allocators/secure.h
+++ b/src/support/allocators/secure.h
@@ -55,6 +55,7 @@ struct secure_allocator {
 };
 
 // This is exactly like std::string, but with a custom allocator.
+// TODO: Consider finding a way to make incoming RPC request.params[i] mlock()ed as well
 typedef std::basic_string<char, std::char_traits<char>, secure_allocator<char> > SecureString;
 
 typedef std::vector<unsigned char, secure_allocator<unsigned char> > SecureVector;
diff --git a/src/support/lockedpool.cpp b/src/support/lockedpool.cpp
index 766e817a8ab5..b130b3957229 100644
--- a/src/support/lockedpool.cpp
+++ b/src/support/lockedpool.cpp
@@ -19,6 +19,7 @@
 #endif
 
 #include <algorithm>
+#include <limits>
 #include <stdexcept>
 #ifdef ARENA_DEBUG
 #include <iomanip>
diff --git a/src/support/lockedpool.h b/src/support/lockedpool.h
index 1aaa76009d67..794e51cc6baa 100644
--- a/src/support/lockedpool.h
+++ b/src/support/lockedpool.h
@@ -5,11 +5,11 @@
 #ifndef BITCOIN_SUPPORT_LOCKEDPOOL_H
 #define BITCOIN_SUPPORT_LOCKEDPOOL_H
 
-#include <stdint.h>
+#include <cstddef>
 #include <list>
 #include <map>
-#include <mutex>
 #include <memory>
+#include <mutex>
 #include <unordered_map>
 
 /**
diff --git a/src/test/blockmanager_tests.cpp b/src/test/blockmanager_tests.cpp
index dd7c32cc46c0..432b6cbf21d6 100644
--- a/src/test/blockmanager_tests.cpp
+++ b/src/test/blockmanager_tests.cpp
@@ -5,9 +5,14 @@
 #include <chainparams.h>
 #include <node/blockstorage.h>
 #include <node/context.h>
+#include <node/kernel_notifications.h>
+#include <script/solver.h>
+#include <primitives/block.h>
+#include <util/chaintype.h>
 #include <validation.h>
 
 #include <boost/test/unit_test.hpp>
+#include <test/util/logging.h>
 #include <test/util/setup_common.h>
 
 using node::BlockManager;
@@ -39,4 +44,153 @@ BOOST_AUTO_TEST_CASE(blockmanager_find_block_pos)
     BOOST_CHECK_EQUAL(actual.nPos, BLOCK_SERIALIZATION_HEADER_SIZE + ::GetSerializeSize(params->GenesisBlock(), CLIENT_VERSION) + BLOCK_SERIALIZATION_HEADER_SIZE);
 }
 
+BOOST_FIXTURE_TEST_CASE(blockmanager_scan_unlink_already_pruned_files, TestChain100Setup)
+{
+    // Cap last block file size, and mine new block in a new block file.
+    const auto& chainman = Assert(m_node.chainman);
+    auto& blockman = chainman->m_blockman;
+    const CBlockIndex* old_tip{WITH_LOCK(chainman->GetMutex(), return chainman->ActiveChain().Tip())};
+    WITH_LOCK(chainman->GetMutex(), blockman.GetBlockFileInfo(old_tip->GetBlockPos().nFile)->nSize = MAX_BLOCKFILE_SIZE);
+    CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
+
+    // Prune the older block file, but don't unlink it
+    int file_number;
+    {
+        LOCK(chainman->GetMutex());
+        file_number = old_tip->GetBlockPos().nFile;
+        blockman.PruneOneBlockFile(file_number);
+    }
+
+    const FlatFilePos pos(file_number, 0);
+
+    // Check that the file is not unlinked after ScanAndUnlinkAlreadyPrunedFiles
+    // if m_have_pruned is not yet set
+    WITH_LOCK(chainman->GetMutex(), blockman.ScanAndUnlinkAlreadyPrunedFiles());
+    BOOST_CHECK(\!AutoFile(blockman.OpenBlockFile(pos, true)).IsNull());
+
+    // Check that the file is unlinked after ScanAndUnlinkAlreadyPrunedFiles
+    // once m_have_pruned is set
+    blockman.m_have_pruned = true;
+    WITH_LOCK(chainman->GetMutex(), blockman.ScanAndUnlinkAlreadyPrunedFiles());
+    BOOST_CHECK(AutoFile(blockman.OpenBlockFile(pos, true)).IsNull());
+
+    // Check that calling with already pruned files doesn't cause an error
+    WITH_LOCK(chainman->GetMutex(), blockman.ScanAndUnlinkAlreadyPrunedFiles());
+
+    // Check that the new tip file has not been removed
+    const CBlockIndex* new_tip{WITH_LOCK(chainman->GetMutex(), return chainman->ActiveChain().Tip())};
+    BOOST_CHECK_NE(old_tip, new_tip);
+    const int new_file_number{WITH_LOCK(chainman->GetMutex(), return new_tip->GetBlockPos().nFile)};
+    const FlatFilePos new_pos(new_file_number, 0);
+    BOOST_CHECK(\!AutoFile(blockman.OpenBlockFile(new_pos, true)).IsNull());
+}
+
+BOOST_FIXTURE_TEST_CASE(blockmanager_block_data_availability, TestChain100Setup)
+{
+    // The goal of the function is to return the first not pruned block in the range [upper_block, lower_block].
+    LOCK(::cs_main);
+    auto& chainman = m_node.chainman;
+    auto& blockman = chainman->m_blockman;
+    const CBlockIndex& tip = *chainman->ActiveTip();
+
+    // Function to prune all blocks from 'last_pruned_block' down to the genesis block
+    const auto& func_prune_blocks = [&](CBlockIndex* last_pruned_block)
+    {
+        LOCK(::cs_main);
+        CBlockIndex* it = last_pruned_block;
+        while (it \!= nullptr && it->nStatus & BLOCK_HAVE_DATA) {
+            it->nStatus &= ~BLOCK_HAVE_DATA;
+            it = it->pprev;
+        }
+    };
+
+    // 1) Return genesis block when all blocks are available
+    BOOST_CHECK_EQUAL(blockman.GetFirstStoredBlock(tip), chainman->ActiveChain()[0]);
+    BOOST_CHECK(blockman.CheckBlockDataAvailability(tip, *chainman->ActiveChain()[0]));
+
+    // 2) Check lower_block when all blocks are available
+    CBlockIndex* lower_block = chainman->ActiveChain()[tip.nHeight / 2];
+    BOOST_CHECK(blockman.CheckBlockDataAvailability(tip, *lower_block));
+
+    // Prune half of the blocks
+    int height_to_prune = tip.nHeight / 2;
+    CBlockIndex* first_available_block = chainman->ActiveChain()[height_to_prune + 1];
+    CBlockIndex* last_pruned_block = first_available_block->pprev;
+    func_prune_blocks(last_pruned_block);
+
+    // 3) The last block not pruned is in-between upper-block and the genesis block
+    BOOST_CHECK_EQUAL(blockman.GetFirstStoredBlock(tip), first_available_block);
+    BOOST_CHECK(blockman.CheckBlockDataAvailability(tip, *first_available_block));
+    BOOST_CHECK(\!blockman.CheckBlockDataAvailability(tip, *last_pruned_block));
+}
+
+BOOST_AUTO_TEST_CASE(blockmanager_flush_block_file)
+{
+    KernelNotifications notifications{m_node.exit_status};
+    node::BlockManager::Options blockman_opts{
+        .chainparams = Params(),
+        .blocks_dir = m_args.GetBlocksDirPath(),
+        .notifications = notifications,
+    };
+    BlockManager blockman{m_node.kernel->interrupt, blockman_opts};
+
+    // Test blocks with no transactions, not even a coinbase
+    CBlock block1;
+    block1.nVersion = 1;
+    CBlock block2;
+    block2.nVersion = 2;
+    CBlock block3;
+    block3.nVersion = 3;
+
+    // They are 80 bytes header + 1 byte 0x00 for vtx length
+    constexpr int TEST_BLOCK_SIZE{81};
+
+    // Blockstore is empty
+    BOOST_CHECK_EQUAL(blockman.CalculateCurrentUsage(), 0);
+
+    // Write the first block; dbp=nullptr means this block doesn't already have a disk
+    // location, so allocate a free location and write it there.
+    FlatFilePos pos1{blockman.SaveBlockToDisk(block1, /*nHeight=*/1, /*dbp=*/nullptr)};
+
+    // Write second block
+    FlatFilePos pos2{blockman.SaveBlockToDisk(block2, /*nHeight=*/2, /*dbp=*/nullptr)};
+
+    // Two blocks in the file
+    BOOST_CHECK_EQUAL(blockman.CalculateCurrentUsage(), (TEST_BLOCK_SIZE + BLOCK_SERIALIZATION_HEADER_SIZE) * 2);
+
+    // First two blocks are written as expected
+    // Errors are expected because block data is junk, thrown AFTER successful read
+    CBlock read_block;
+    BOOST_CHECK_EQUAL(read_block.nVersion, 0);
+    {
+        ASSERT_DEBUG_LOG("ReadBlockFromDisk: Errors in block header");
+        BOOST_CHECK(\!blockman.ReadBlockFromDisk(read_block, pos1));
+        BOOST_CHECK_EQUAL(read_block.nVersion, 1);
+    }
+    {
+        ASSERT_DEBUG_LOG("ReadBlockFromDisk: Errors in block header");
+        BOOST_CHECK(\!blockman.ReadBlockFromDisk(read_block, pos2));
+        BOOST_CHECK_EQUAL(read_block.nVersion, 2);
+    }
+
+    // When FlatFilePos* dbp is given, SaveBlockToDisk() will not write or
+    // overwrite anything to the flat file block storage. It will, however,
+    // update the blockfile metadata. This is to facilitate reindexing
+    // when the user has the blocks on disk but the metadata is being rebuilt.
+    // Verify this behavior by attempting (and failing) to write block 3 data
+    // to block 2 location.
+    CBlockFileInfo* block_data = blockman.GetBlockFileInfo(0);
+    BOOST_CHECK_EQUAL(block_data->nBlocks, 2);
+    BOOST_CHECK(blockman.SaveBlockToDisk(block3, /*nHeight=*/3, /*dbp=*/&pos2) == pos2);
+    // Metadata is updated...
+    BOOST_CHECK_EQUAL(block_data->nBlocks, 3);
+    // ...but there are still only two blocks in the file
+    BOOST_CHECK_EQUAL(blockman.CalculateCurrentUsage(), (TEST_BLOCK_SIZE + BLOCK_SERIALIZATION_HEADER_SIZE) * 2);
+
+    // Block 2 was not overwritten:
+    //   SaveBlockToDisk() did not call WriteBlockToDisk() because `FlatFilePos* dbp` was non-null
+    blockman.ReadBlockFromDisk(read_block, pos2);
+    BOOST_CHECK_EQUAL(read_block.nVersion, 2);
+}
+
 BOOST_AUTO_TEST_SUITE_END()
diff --git a/src/test/evo_assetlocks_tests.cpp b/src/test/evo_assetlocks_tests.cpp
index 02d2353d34a9..c2179479c5e4 100644
--- a/src/test/evo_assetlocks_tests.cpp
+++ b/src/test/evo_assetlocks_tests.cpp
@@ -289,7 +289,7 @@ BOOST_FIXTURE_TEST_CASE(evo_assetlock, TestChain100Setup)
     {
         // OP_RETURN should not have any data
         CMutableTransaction txReturnData(tx);
-        txReturnData.vout[0].scriptPubKey = CScript() << OP_RETURN << ParseHex("abc");
+        txReturnData.vout[0].scriptPubKey = CScript() << OP_RETURN << ParseHex("abcd");
 
         BOOST_CHECK(!CheckAssetLockTx(CTransaction(txReturnData), tx_state));
         BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-non-empty-return");
diff --git a/src/test/httpserver_tests.cpp b/src/test/httpserver_tests.cpp
new file mode 100644
index 000000000000..ee59ec6967fc
--- /dev/null
+++ b/src/test/httpserver_tests.cpp
@@ -0,0 +1,38 @@
+// Copyright (c) 2012-2022 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 <httpserver.h>
+#include <test/util/setup_common.h>
+
+#include <boost/test/unit_test.hpp>
+
+BOOST_FIXTURE_TEST_SUITE(httpserver_tests, BasicTestingSetup)
+
+BOOST_AUTO_TEST_CASE(test_query_parameters)
+{
+    std::string uri {};
+
+    // No parameters
+    uri = "localhost:8080/rest/headers/someresource.json";
+    BOOST_CHECK(!GetQueryParameterFromUri(uri.c_str(), "p1").has_value());
+
+    // Single parameter
+    uri = "localhost:8080/rest/endpoint/someresource.json?p1=v1";
+    BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri.c_str(), "p1").value(), "v1");
+    BOOST_CHECK(!GetQueryParameterFromUri(uri.c_str(), "p2").has_value());
+
+    // Multiple parameters
+    uri = "/rest/endpoint/someresource.json?p1=v1&p2=v2";
+    BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri.c_str(), "p1").value(), "v1");
+    BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri.c_str(), "p2").value(), "v2");
+
+    // If the query string contains duplicate keys, the first value is returned
+    uri = "/rest/endpoint/someresource.json?p1=v1&p1=v2";
+    BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri.c_str(), "p1").value(), "v1");
+
+    // Invalid query string syntax is the same as not having parameters
+    uri = "/rest/endpoint/someresource.json&p1=v1&p2=v2";
+    BOOST_CHECK(!GetQueryParameterFromUri(uri.c_str(), "p1").has_value());
+}
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/src/test/miner_tests.cpp b/src/test/miner_tests.cpp
index 8447a5e927da..909ead66776c 100644
--- a/src/test/miner_tests.cpp
+++ b/src/test/miner_tests.cpp
@@ -39,7 +39,9 @@ struct MinerTestingSetup : public TestingSetup {
     bool TestSequenceLocks(const CTransaction& tx) EXCLUSIVE_LOCKS_REQUIRED(::cs_main, m_node.mempool->cs)
     {
         CCoinsViewMemPool view_mempool(&m_node.chainman->ActiveChainstate().CoinsTip(), *m_node.mempool);
-        return CheckSequenceLocksAtTip(m_node.chainman->ActiveChain().Tip(), view_mempool, tx);
+        CBlockIndex* tip{m_node.chainman->ActiveChain().Tip()};
+        const std::optional<LockPoints> lock_points{CalculateLockPointsAtTip(tip, view_mempool, tx)};
+        return lock_points.has_value() && CheckSequenceLocksAtTip(tip, *lock_points);
     }
     BlockAssembler AssemblerForTest(const CChainParams& params);
 };
@@ -386,7 +388,7 @@ BOOST_AUTO_TEST_CASE(CreateNewBlock_validity)
     //     next->pprev = prev;
     //     next->nHeight = prev->nHeight + 1;
     //     next->BuildSkip();
-    //     m_node.chainman->ActiveChain().SetTip(next);
+    //     m_node.chainman->ActiveChain().SetTip(*next);
     // }
     //BOOST_CHECK(pblocktemplate = BlockAssembler(chainparams).CreateNewBlock(scriptPubKey));
     // // Extend to a 210000-long block chain.
@@ -398,7 +400,7 @@ BOOST_AUTO_TEST_CASE(CreateNewBlock_validity)
     //     next->pprev = prev;
     //     next->nHeight = prev->nHeight + 1;
     //     next->BuildSkip();
-    //     m_node.chainman->ActiveChain().SetTip(next);
+    //     m_node.chainman->ActiveChain().SetTip(*next);
     // }
     //BOOST_CHECK(pblocktemplate = BlockAssembler(chainparams).CreateNewBlock(scriptPubKey));
 
@@ -423,7 +425,7 @@ BOOST_AUTO_TEST_CASE(CreateNewBlock_validity)
     // // Delete the dummy blocks again.
     // while (m_node.chainman->ActiveChain().Tip()->nHeight > nHeight) {
     //     CBlockIndex* del = m_node.chainman->ActiveChain().Tip();
-    //     m_node.chainman->ActiveChain().SetTip(del->pprev);
+    //     m_node.chainman->ActiveChain().SetTip(*Assert(del->pprev));
     //     m_node.chainman->ActiveChainstate().CoinsTip().SetBestBlock(del->pprev->GetBlockHash());
     //     delete del->phashBlock;
     //     delete del;
diff --git a/src/test/rest_tests.cpp b/src/test/rest_tests.cpp
new file mode 100644
index 000000000000..20dfe4b41a70
--- /dev/null
+++ b/src/test/rest_tests.cpp
@@ -0,0 +1,48 @@
+// Copyright (c) 2012-2022 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 <rest.h>
+#include <test/util/setup_common.h>
+
+#include <boost/test/unit_test.hpp>
+
+#include <string>
+
+BOOST_FIXTURE_TEST_SUITE(rest_tests, BasicTestingSetup)
+
+BOOST_AUTO_TEST_CASE(test_query_string)
+{
+    std::string param;
+    RESTResponseFormat rf;
+    // No query string
+    rf = ParseDataFormat(param, "/rest/endpoint/someresource.json");
+    BOOST_CHECK_EQUAL(param, "/rest/endpoint/someresource");
+    BOOST_CHECK_EQUAL(rf, RESTResponseFormat::JSON);
+
+    // Query string with single parameter
+    rf = ParseDataFormat(param, "/rest/endpoint/someresource.bin?p1=v1");
+    BOOST_CHECK_EQUAL(param, "/rest/endpoint/someresource");
+    BOOST_CHECK_EQUAL(rf, RESTResponseFormat::BINARY);
+
+    // Query string with multiple parameters
+    rf = ParseDataFormat(param, "/rest/endpoint/someresource.hex?p1=v1&p2=v2");
+    BOOST_CHECK_EQUAL(param, "/rest/endpoint/someresource");
+    BOOST_CHECK_EQUAL(rf, RESTResponseFormat::HEX);
+
+    // Incorrectly formed query string will not be handled
+    rf = ParseDataFormat(param, "/rest/endpoint/someresource.json&p1=v1");
+    BOOST_CHECK_EQUAL(param, "/rest/endpoint/someresource.json&p1=v1");
+    BOOST_CHECK_EQUAL(rf, RESTResponseFormat::UNDEF);
+
+    // Omitted data format with query string should return UNDEF and hide query string
+    rf = ParseDataFormat(param, "/rest/endpoint/someresource?p1=v1");
+    BOOST_CHECK_EQUAL(param, "/rest/endpoint/someresource");
+    BOOST_CHECK_EQUAL(rf, RESTResponseFormat::UNDEF);
+
+    // Data format specified after query string
+    rf = ParseDataFormat(param, "/rest/endpoint/someresource?p1=v1.json");
+    BOOST_CHECK_EQUAL(param, "/rest/endpoint/someresource");
+    BOOST_CHECK_EQUAL(rf, RESTResponseFormat::UNDEF);
+}
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/src/test/skiplist_tests.cpp b/src/test/skiplist_tests.cpp
index 7092c7fce5ba..07724a97d06d 100644
--- a/src/test/skiplist_tests.cpp
+++ b/src/test/skiplist_tests.cpp
@@ -72,7 +72,7 @@ BOOST_AUTO_TEST_CASE(getlocator_test)
 
     // Build a CChain for the main branch.
     CChain chain;
-    chain.SetTip(&vBlocksMain.back());
+    chain.SetTip(vBlocksMain.back());
 
     // Test 100 random starting points for locators.
     for (int n=0; n<100; n++) {
@@ -128,7 +128,7 @@ BOOST_AUTO_TEST_CASE(findearliestatleast_test)
 
     // Build a CChain for the main branch.
     CChain chain;
-    chain.SetTip(&vBlocksMain.back());
+    chain.SetTip(vBlocksMain.back());
 
     // Verify that FindEarliestAtLeast is correct.
     for (unsigned int i=0; i<10000; ++i) {
@@ -155,7 +155,7 @@ BOOST_AUTO_TEST_CASE(findearliestatleast_edge_test)
     }
 
     CChain chain;
-    chain.SetTip(&blocks.back());
+    chain.SetTip(blocks.back());
 
     BOOST_CHECK_EQUAL(chain.FindEarliestAtLeast(50, 0)->nHeight, 0);
     BOOST_CHECK_EQUAL(chain.FindEarliestAtLeast(100, 0)->nHeight, 0);
diff --git a/src/test/util_tests.cpp b/src/test/util_tests.cpp
index 50ed481bea03..844e61a109b5 100644
--- a/src/test/util_tests.cpp
+++ b/src/test/util_tests.cpp
@@ -145,26 +145,52 @@ BOOST_AUTO_TEST_CASE(parse_hex)
     // Basic test vector
     result = ParseHex("04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f");
     BOOST_CHECK_EQUAL_COLLECTIONS(result.begin(), result.end(), expected.begin(), expected.end());
+    result = TryParseHex<uint8_t>("04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f").value();
+    BOOST_CHECK_EQUAL_COLLECTIONS(result.begin(), result.end(), expected.begin(), expected.end());
 
     // Spaces between bytes must be supported
     result = ParseHex("12 34 56 78");
     BOOST_CHECK(result.size() == 4 && result[0] == 0x12 && result[1] == 0x34 && result[2] == 0x56 && result[3] == 0x78);
+    result = TryParseHex<uint8_t>("12 34 56 78").value();
+    BOOST_CHECK(result.size() == 4 && result[0] == 0x12 && result[1] == 0x34 && result[2] == 0x56 && result[3] == 0x78);
 
     // Leading space must be supported (used in BerkeleyEnvironment::Salvage)
     result = ParseHex(" 89 34 56 78");
     BOOST_CHECK(result.size() == 4 && result[0] == 0x89 && result[1] == 0x34 && result[2] == 0x56 && result[3] == 0x78);
+    result = TryParseHex<uint8_t>(" 89 34 56 78").value();
+    BOOST_CHECK(result.size() == 4 && result[0] == 0x89 && result[1] == 0x34 && result[2] == 0x56 && result[3] == 0x78);
+
+    // Mixed case and spaces are supported
+    result = ParseHex("     Ff        aA    ");
+    BOOST_CHECK(result.size() == 2 && result[0] == 0xff && result[1] == 0xaa);
+    result = TryParseHex<uint8_t>("     Ff        aA    ").value();
+    BOOST_CHECK(result.size() == 2 && result[0] == 0xff && result[1] == 0xaa);
 
-    // Embedded null is treated as end
+    // Empty string is supported
+    result = ParseHex("");
+    BOOST_CHECK(result.size() == 0);
+    result = TryParseHex<uint8_t>("").value();
+    BOOST_CHECK(result.size() == 0);
+
+    // Spaces between nibbles is treated as invalid
+    BOOST_CHECK_EQUAL(ParseHex("AAF F").size(), 0);
+    BOOST_CHECK(!TryParseHex("AAF F").has_value());
+
+    // Embedded null is treated as invalid
     const std::string with_embedded_null{" 11 "s
                                          " \0 "
                                          " 22 "s};
     BOOST_CHECK_EQUAL(with_embedded_null.size(), 11);
-    result = ParseHex(with_embedded_null);
-    BOOST_CHECK(result.size() == 1 && result[0] == 0x11);
+    BOOST_CHECK_EQUAL(ParseHex(with_embedded_null).size(), 0);
+    BOOST_CHECK(!TryParseHex(with_embedded_null).has_value());
+
+    // Non-hex is treated as invalid
+    BOOST_CHECK_EQUAL(ParseHex("1234 invalid 1234").size(), 0);
+    BOOST_CHECK(!TryParseHex("1234 invalid 1234").has_value());
 
-    // Stop parsing at invalid value
-    result = ParseHex("1234 invalid 1234");
-    BOOST_CHECK(result.size() == 2 && result[0] == 0x12 && result[1] == 0x34);
+    // Truncated input is treated as invalid
+    BOOST_CHECK_EQUAL(ParseHex("12 3").size(), 0);
+    BOOST_CHECK(!TryParseHex("12 3").has_value());
 }
 
 BOOST_AUTO_TEST_CASE(util_HexStr)
diff --git a/src/util/strencodings.cpp b/src/util/strencodings.cpp
index e647cd0f9c2f..a69c770ac1b9 100644
--- a/src/util/strencodings.cpp
+++ b/src/util/strencodings.cpp
@@ -79,24 +79,25 @@ bool IsHexNumber(std::string_view str)
 }
 
 template <typename Byte>
-std::vector<Byte> ParseHex(std::string_view str)
+std::optional<std::vector<Byte>> TryParseHex(std::string_view str)
 {
     std::vector<Byte> vch;
     auto it = str.begin();
-    while (it != str.end() && it + 1 != str.end()) {
+    while (it != str.end()) {
         if (IsSpace(*it)) {
             ++it;
             continue;
         }
         auto c1 = HexDigit(*(it++));
+        if (it == str.end()) return std::nullopt;
         auto c2 = HexDigit(*(it++));
-        if (c1 < 0 || c2 < 0) break;
+        if (c1 < 0 || c2 < 0) return std::nullopt;
         vch.push_back(Byte(c1 << 4) | Byte(c2));
     }
     return vch;
 }
-template std::vector<std::byte> ParseHex(std::string_view);
-template std::vector<uint8_t> ParseHex(std::string_view);
+template std::optional<std::vector<std::byte>> TryParseHex(std::string_view);
+template std::optional<std::vector<uint8_t>> TryParseHex(std::string_view);
 
 bool SplitHostPort(std::string_view in, uint16_t& portOut, std::string& hostOut)
 {
diff --git a/src/util/strencodings.h b/src/util/strencodings.h
index 75b531ecc4e6..e6de59e3a1a5 100644
--- a/src/util/strencodings.h
+++ b/src/util/strencodings.h
@@ -58,9 +58,15 @@ enum class ByteUnit : uint64_t {
 * @return           A new string without unsafe chars
 */
 std::string SanitizeString(std::string_view str, int rule = SAFE_CHARS_DEFAULT);
-/** Parse the hex string into bytes (uint8_t or std::byte). Ignores whitespace. */
+/** Parse the hex string into bytes (uint8_t or std::byte). Ignores whitespace. Returns nullopt on invalid input. */
+template <typename Byte = std::byte>
+std::optional<std::vector<Byte>> TryParseHex(std::string_view str);
+/** Like TryParseHex, but returns an empty vector on invalid input. */
 template <typename Byte = uint8_t>
-std::vector<Byte> ParseHex(std::string_view str);
+std::vector<Byte> ParseHex(std::string_view hex_str)
+{
+    return TryParseHex<Byte>(hex_str).value_or(std::vector<Byte>{});
+}
 signed char HexDigit(char c);
 /* Returns true if each character in str is a hex character, and has an even
  * number of hex digits.*/
diff --git a/src/util/system.cpp b/src/util/system.cpp
index 424037321024..caf7972e6fe2 100644
--- a/src/util/system.cpp
+++ b/src/util/system.cpp
@@ -448,8 +448,7 @@ fs::path ArgsManager::GetDataDir(bool net_specific) const
     LOCK(cs_args);
     fs::path& path = net_specific ? m_cached_network_datadir_path : m_cached_datadir_path;
 
-    // Cache the path to avoid calling fs::create_directories on every call of
-    // this function
+    // Used cached path if available
     if (!path.empty()) return path;
 
     const fs::path datadir{GetPathArg("-datadir")};
@@ -463,15 +462,8 @@ fs::path ArgsManager::GetDataDir(bool net_specific) const
         path = GetDefaultDataDir();
     }
 
-    if (!fs::exists(path)) {
-        fs::create_directories(path / "wallets");
-    }
-
     if (net_specific && !BaseParams().DataDir().empty()) {
         path /= fs::PathFromString(BaseParams().DataDir());
-        if (!fs::exists(path)) {
-            fs::create_directories(path / "wallets");
-        }
     }
 
     return path;
@@ -485,6 +477,27 @@ fs::path ArgsManager::GetBackupsDirPath()
     return fs::absolute(GetPathArg("-walletbackupsdir"));
 }
 
+void ArgsManager::EnsureDataDir() const
+{
+    /**
+     * "/wallets" subdirectories are created in all **new**
+     * datadirs, because wallet code will create new wallets in the "wallets"
+     * subdirectory only if exists already, otherwise it will create them in
+     * the top-level datadir where they could interfere with other files.
+     * Wallet init code currently avoids creating "wallets" directories itself
+     * for backwards compatibility, but this be changed in the future and
+     * wallet code here could go away (#16220).
+     */
+    auto path{GetDataDir(false)};
+    if (!fs::exists(path)) {
+        fs::create_directories(path / "wallets");
+    }
+    path = GetDataDir(true);
+    if (!fs::exists(path)) {
+        fs::create_directories(path / "wallets");
+    }
+}
+
 void ArgsManager::ClearPathCache()
 {
     LOCK(cs_args);
@@ -530,6 +543,7 @@ bool ArgsManager::IsArgSet(const std::string& strArg) const
 
 bool ArgsManager::InitSettings(std::string& error)
 {
+    EnsureDataDir();
     if (!GetSettingsPath()) {
         return true; // Do nothing if settings file disabled.
     }
@@ -963,6 +977,11 @@ bool ArgsManager::ReadConfigStream(std::istream& stream, const std::string& file
     return true;
 }
 
+fs::path ArgsManager::GetConfigFilePath() const
+{
+    return GetConfigFile(GetPathArg("-conf", BITCOIN_CONF_FILENAME));
+}
+
 bool ArgsManager::ReadConfigFiles(std::string& error, bool ignore_invalid_keys)
 {
     {
@@ -971,8 +990,8 @@ bool ArgsManager::ReadConfigFiles(std::string& error, bool ignore_invalid_keys)
         m_config_sections.clear();
     }
 
-    const fs::path conf_path = GetPathArg("-conf", BITCOIN_CONF_FILENAME);
-    std::ifstream stream{GetConfigFile(conf_path)};
+    const auto conf_path{GetConfigFilePath()};
+    std::ifstream stream{conf_path};
 
     // not ok to have a config file specified that cannot be opened
     if (IsArgSet("-conf") && !stream.good()) {
diff --git a/src/util/system.h b/src/util/system.h
index 7bf10098315c..dac0f0d82e8a 100644
--- a/src/util/system.h
+++ b/src/util/system.h
@@ -257,6 +257,11 @@ class ArgsManager
     void SelectConfigNetwork(const std::string& network);
 
     [[nodiscard]] bool ParseParameters(int argc, const char* const argv[], std::string& error);
+
+    /**
+     * Return config file path (read-only)
+     */
+    fs::path GetConfigFilePath() const;
     [[nodiscard]] bool ReadConfigFiles(std::string& error, bool ignore_invalid_keys = false);
 
     /**
@@ -495,13 +500,18 @@ class ArgsManager
      */
     void LogArgs() const;
 
+    /**
+     * If datadir does not exist, create it along with wallets/
+     * subdirectory(s).
+     */
+    void EnsureDataDir() const;
+
 private:
     /**
      * Get data directory path
      *
      * @param net_specific Append network identifier to the returned path
      * @return Absolute path on success, otherwise an empty path when a non-directory path would be returned
-     * @post Returned directory path is created unless it is empty
      */
     fs::path GetDataDir(bool net_specific) const;
 
diff --git a/src/util/time.cpp b/src/util/time.cpp
index e04f43ae6151..548b1b015c89 100644
--- a/src/util/time.cpp
+++ b/src/util/time.cpp
@@ -57,11 +57,6 @@ std::chrono::seconds GetMockTime()
     return g_mock_time.load(std::memory_order_relaxed);
 }
 
-int64_t GetTimeMillis()
-{
-    return int64_t{GetSystemTime<std::chrono::milliseconds>().count()};
-}
-
 int64_t GetTimeMicros()
 {
     return int64_t{GetSystemTime<std::chrono::microseconds>().count()};
diff --git a/src/util/time.h b/src/util/time.h
index d1fca85e85a6..8da7c650bbc7 100644
--- a/src/util/time.h
+++ b/src/util/time.h
@@ -29,6 +29,8 @@ using SteadySeconds = std::chrono::time_point<std::chrono::steady_clock, std::ch
 using SteadyMilliseconds = std::chrono::time_point<std::chrono::steady_clock, std::chrono::milliseconds>;
 using SteadyMicroseconds = std::chrono::time_point<std::chrono::steady_clock, std::chrono::microseconds>;
 
+using SystemClock = std::chrono::system_clock;
+
 void UninterruptibleSleep(const std::chrono::microseconds& n);
 
 /**
@@ -68,11 +70,8 @@ using SecondsDouble = std::chrono::duration<double, std::chrono::seconds::period
  */
 int64_t GetTime();
 
-/** Returns the system time (not mockable) */
-int64_t GetTimeMillis();
 /** Returns the system time (not mockable) */
 int64_t GetTimeMicros();
-
 /**
  * DEPRECATED
  * Use SetMockTime with chrono type
diff --git a/src/validation.cpp b/src/validation.cpp
index 823c2ca5d354..0df6f5aaa852 100644
--- a/src/validation.cpp
+++ b/src/validation.cpp
@@ -185,11 +185,89 @@ bool CheckFinalTxAtTip(const CBlockIndex& active_chain_tip, const CTransaction&
     return IsFinalTx(tx, nBlockHeight, nBlockTime);
 }
 
+namespace {
+/**
+ * A helper which calculates heights of inputs of a given transaction.
+ *
+ * @param[in] tip    The current chain tip. If an input belongs to a mempool
+ *                   transaction, we assume it will be confirmed in the next block.
+ * @param[in] coins  Any CCoinsView that provides access to the relevant coins.
+ * @param[in] tx     The transaction being evaluated.
+ *
+ * @returns A vector of input heights or nullopt, in case of an error.
+ */
+std::optional<std::vector<int>> CalculatePrevHeights(
+    const CBlockIndex& tip,
+    const CCoinsView& coins,
+    const CTransaction& tx)
+{
+    std::vector<int> prev_heights;
+    prev_heights.resize(tx.vin.size());
+    for (size_t i = 0; i < tx.vin.size(); ++i) {
+        const CTxIn& txin = tx.vin[i];
+        Coin coin;
+        if (!coins.GetCoin(txin.prevout, coin)) {
+            LogPrintf("ERROR: %s: Missing input %d in transaction \'%s\'\n", __func__, i, tx.GetHash().GetHex());
+            return std::nullopt;
+        }
+        if (coin.nHeight == MEMPOOL_HEIGHT) {
+            // Assume all mempool transaction confirm in the next block.
+            prev_heights[i] = tip.nHeight + 1;
+        } else {
+            prev_heights[i] = coin.nHeight;
+        }
+    }
+    return prev_heights;
+}
+} // namespace
+
+std::optional<LockPoints> CalculateLockPointsAtTip(
+    CBlockIndex* tip,
+    const CCoinsView& coins_view,
+    const CTransaction& tx)
+{
+    assert(tip);
+
+    auto prev_heights{CalculatePrevHeights(*tip, coins_view, tx)};
+    if (!prev_heights.has_value()) return std::nullopt;
+
+    CBlockIndex next_tip;
+    next_tip.pprev = tip;
+    // When SequenceLocks() is called within ConnectBlock(), the height
+    // of the block *being* evaluated is what is used.
+    // Thus if we want to know if a transaction can be part of the
+    // *next* block, we need to use one more than active_chainstate.m_chain.Height()
+    next_tip.nHeight = tip->nHeight + 1;
+    const auto [min_height, min_time] = CalculateSequenceLocks(tx, STANDARD_LOCKTIME_VERIFY_FLAGS, prev_heights.value(), next_tip);
+
+    // Also store the hash of the block with the highest height of
+    // all the blocks which have sequence locked prevouts.
+    // This hash needs to still be on the chain
+    // for these LockPoint calculations to be valid
+    // Note: It is impossible to correctly calculate a maxInputBlock
+    // if any of the sequence locked inputs depend on unconfirmed txs,
+    // except in the special case where the relative lock time/height
+    // is 0, which is equivalent to no sequence lock. Since we assume
+    // input height of tip+1 for mempool txs and test the resulting
+    // min_height and min_time from CalculateSequenceLocks against tip+1.
+    int max_input_height{0};
+    for (const int height : prev_heights.value()) {
+        // Can ignore mempool inputs since we'll fail if they had non-zero locks
+        if (height != next_tip.nHeight) {
+            max_input_height = std::max(max_input_height, height);
+        }
+    }
+
+    // tip->GetAncestor(max_input_height) should never return a nullptr
+    // because max_input_height is always less than the tip height.
+    // It would, however, be a bad bug to continue execution, since a
+    // LockPoints object with the maxInputBlock member set to nullptr
+    // signifies no relative lock time.
+    return LockPoints{min_height, min_time, Assert(tip->GetAncestor(max_input_height))};
+}
+
 bool CheckSequenceLocksAtTip(CBlockIndex* tip,
-                        const CCoinsView& coins_view,
-                        const CTransaction& tx,
-                        LockPoints* lp,
-                        bool useExistingLockPoints)
+                             const LockPoints& lock_points)
 {
     assert(tip != nullptr);
 
@@ -203,61 +281,7 @@ bool CheckSequenceLocksAtTip(CBlockIndex* tip,
     // *next* block, we need to use one more than active_chainstate.m_chain.Height()
     index.nHeight = tip->nHeight + 1;
 
-    std::pair<int, int64_t> lockPair;
-    if (useExistingLockPoints) {
-        assert(lp);
-        lockPair.first = lp->height;
-        lockPair.second = lp->time;
-    }
-    else {
-        std::vector<int> prevheights;
-        prevheights.resize(tx.vin.size());
-        for (size_t txinIndex = 0; txinIndex < tx.vin.size(); txinIndex++) {
-            const CTxIn& txin = tx.vin[txinIndex];
-            Coin coin;
-            if (!coins_view.GetCoin(txin.prevout, coin)) {
-                return error("%s: Missing input", __func__);
-            }
-            if (coin.nHeight == MEMPOOL_HEIGHT) {
-                // Assume all mempool transaction confirm in the next block
-                prevheights[txinIndex] = tip->nHeight + 1;
-            } else {
-                prevheights[txinIndex] = coin.nHeight;
-            }
-        }
-        lockPair = CalculateSequenceLocks(tx, STANDARD_LOCKTIME_VERIFY_FLAGS, prevheights, index);
-        if (lp) {
-            lp->height = lockPair.first;
-            lp->time = lockPair.second;
-            // Also store the hash of the block with the highest height of
-            // all the blocks which have sequence locked prevouts.
-            // This hash needs to still be on the chain
-            // for these LockPoint calculations to be valid
-            // Note: It is impossible to correctly calculate a maxInputBlock
-            // if any of the sequence locked inputs depend on unconfirmed txs,
-            // except in the special case where the relative lock time/height
-            // is 0, which is equivalent to no sequence lock. Since we assume
-            // input height of tip+1 for mempool txs and test the resulting
-            // lockPair from CalculateSequenceLocks against tip+1.  We know
-            // EvaluateSequenceLocks will fail if there was a non-zero sequence
-            // lock on a mempool input, so we can use the return value of
-            // CheckSequenceLocksAtTip to indicate the LockPoints validity
-            int maxInputHeight = 0;
-            for (const int height : prevheights) {
-                // Can ignore mempool inputs since we'll fail if they had non-zero locks
-                if (height != tip->nHeight+1) {
-                    maxInputHeight = std::max(maxInputHeight, height);
-                }
-            }
-            // tip->GetAncestor(maxInputHeight) should never return a nullptr
-            // because maxInputHeight is always less than the tip height.
-            // It would, however, be a bad bug to continue execution, since a
-            // LockPoints object with the maxInputBlock member set to nullptr
-            // signifies no relative lock time.
-            lp->maxInputBlock = Assert(tip->GetAncestor(maxInputHeight));
-        }
-    }
-    return EvaluateSequenceLocks(index, lockPair);
+    return EvaluateSequenceLocks(index, {lock_points.height, lock_points.time});
 }
 
 bool GetUTXOCoin(CChainState& active_chainstate, const COutPoint& outpoint, Coin& coin)
@@ -405,20 +429,23 @@ void CChainState::MaybeUpdateMempoolForReorg(
 
         // The transaction must be final.
         if (!CheckFinalTxAtTip(*Assert(m_chain.Tip()), tx)) return true;
-        LockPoints lp = it->GetLockPoints();
-        const bool validLP{TestLockPointValidity(m_chain, lp)};
-        CCoinsViewMemPool view_mempool(&CoinsTip(), *m_mempool);
+
+        const LockPoints& lp = it->GetLockPoints();
         // CheckSequenceLocksAtTip checks if the transaction will be final in the next block to be
-        // created on top of the new chain. We use useExistingLockPoints=false so that, instead of
-        // using the information in lp (which might now refer to a block that no longer exists in
-        // the chain), it will update lp to contain LockPoints relevant to the new chain.
-        if (!CheckSequenceLocksAtTip(m_chain.Tip(), view_mempool, tx, &lp, validLP)) {
-            // If CheckSequenceLocksAtTip fails, remove the tx and don't depend on the LockPoints.
-            return true;
-        } else if (!validLP) {
-            // If CheckSequenceLocksAtTip succeeded, it also updated the LockPoints.
-            // Now update the mempool entry lockpoints as well.
-            m_mempool->mapTx.modify(it, [&lp](CTxMemPoolEntry& e) { e.UpdateLockPoints(lp); });
+        // created on top of the new chain.
+        if (TestLockPointValidity(m_chain, lp)) {
+            if (!CheckSequenceLocksAtTip(m_chain.Tip(), lp)) {
+                return true;
+            }
+        } else {
+            const CCoinsViewMemPool view_mempool{&CoinsTip(), *m_mempool};
+            const std::optional<LockPoints> new_lock_points{CalculateLockPointsAtTip(m_chain.Tip(), view_mempool, tx)};
+            if (new_lock_points.has_value() && CheckSequenceLocksAtTip(m_chain.Tip(), *new_lock_points)) {
+                // Now update the mempool entry lockpoints as well.
+                m_mempool->mapTx.modify(it, [&new_lock_points](CTxMemPoolEntry& e) { e.UpdateLockPoints(*new_lock_points); });
+            } else {
+                return true;
+            }
         }
 
         // If the transaction spends any coinbase outputs, it must be mature.
@@ -781,7 +808,6 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
             }
         }
     }
-    LockPoints lp;
     m_view.SetBackend(m_viewmempool);
 
     const CCoinsViewCache& coins_cache = m_active_chainstate.CoinsTip();
@@ -823,7 +849,8 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
     // be mined yet.
     // Pass in m_view which has all of the relevant inputs cached. Note that, since m_view's
     // backend was removed, it no longer pulls coins from the mempool.
-    if (!CheckSequenceLocksAtTip(m_active_chainstate.m_chain.Tip(), m_view, tx, &lp)) {
+    const std::optional<LockPoints> lock_points{CalculateLockPointsAtTip(m_active_chainstate.m_chain.Tip(), m_view, tx)};
+    if (!lock_points.has_value() || !CheckSequenceLocksAtTip(m_active_chainstate.m_chain.Tip(), *lock_points)) {
         return state.Invalid(TxValidationResult::TX_PREMATURE_SPEND, "non-BIP68-final");
     }
 
@@ -854,7 +881,7 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
     }
 
     entry.reset(new CTxMemPoolEntry(ptx, ws.m_base_fees, nAcceptTime, m_active_chainstate.m_chain.Height(),
-            fSpendsCoinbase, nSigOps, lp));
+            fSpendsCoinbase, nSigOps, lock_points.value()));
     ws.m_vsize = entry->GetTxSize();
 
     if (nSigOps > MAX_STANDARD_TX_SIGOPS)
@@ -1930,11 +1957,21 @@ DisconnectResult CChainState::DisconnectBlock(const CBlock& block, const CBlockI
         return DISCONNECT_FAILED;
     }
 
+    // Ignore blocks that contain transactions which are 'overwritten' by later transactions,
+    // unless those are already completely spent.
+    // See https://github.com/bitcoin/bitcoin/issues/22596 for additional information.
+    // Note: the blocks specified here are different than the ones used in ConnectBlock because DisconnectBlock
+    // unwinds the blocks in reverse. As a result, the inconsistency is not discovered until the earlier
+    // blocks with the duplicate coinbase transactions are disconnected.
+    bool fEnforceBIP30 = !((pindex->nHeight==91722 && pindex->GetBlockHash() == uint256S("0x00000000000271a2dc26e7667f8419f2e15416dc6955e5a6c6cdf3f2574dd08e")) ||
+                           (pindex->nHeight==91812 && pindex->GetBlockHash() == uint256S("0x00000000000af0aed4792b1acee3d966af36cf5def14935db8de83d6f9306f2f")));
+
     // undo transactions in reverse order
     for (int i = block.vtx.size() - 1; i >= 0; i--) {
         const CTransaction &tx = *(block.vtx[i]);
         uint256 hash = tx.GetHash();
         bool is_coinbase = tx.IsCoinBase();
+        bool is_bip30_exception = (is_coinbase && !fEnforceBIP30);
 
         if (fAddressIndex) {
             for (unsigned int k = tx.vout.size(); k-- > 0;) {
@@ -1963,7 +2000,9 @@ DisconnectResult CChainState::DisconnectBlock(const CBlock& block, const CBlockI
                 Coin coin;
                 bool is_spent = view.SpendCoin(out, &coin);
                 if (!is_spent || tx.vout[o] != coin.out || pindex->nHeight != coin.nHeight || is_coinbase != coin.fCoinBase) {
-                    fClean = false; // transaction output mismatch
+                    if (!is_bip30_exception) {
+                        fClean = false; // transaction output mismatch
+                    }
                 }
             }
         }
@@ -2922,6 +2961,7 @@ bool CChainState::DisconnectTip(BlockValidationState& state, DisconnectedBlockTr
 
     CBlockIndex *pindexDelete = m_chain.Tip();
     assert(pindexDelete);
+    assert(pindexDelete->pprev);
     // Read block from disk.
     std::shared_ptr<CBlock> pblock = std::make_shared<CBlock>();
     CBlock& block = *pblock;
@@ -2972,7 +3012,7 @@ bool CChainState::DisconnectTip(BlockValidationState& state, DisconnectedBlockTr
         }
     }
 
-    m_chain.SetTip(pindexDelete->pprev);
+    m_chain.SetTip(*pindexDelete->pprev);
 
     UpdateTip(pindexDelete->pprev);
     // Let wallets know transactions went from 1-confirmed to
@@ -3103,7 +3143,7 @@ bool CChainState::ConnectTip(BlockValidationState& state, CBlockIndex* pindexNew
         disconnectpool.removeForBlock(blockConnecting.vtx);
     }
     // Update m_chain & related variables.
-    m_chain.SetTip(pindexNew);
+    m_chain.SetTip(*pindexNew);
     UpdateTip(pindexNew);
 
     int64_t nTime6 = GetTimeMicros(); nTimePostConnect += nTime6 - nTime5; nTimeTotal += nTime6 - nTime1;
@@ -4448,7 +4488,7 @@ bool CChainState::LoadChainTip()
     if (!pindex) {
         return false;
     }
-    m_chain.SetTip(pindex);
+    m_chain.SetTip(*pindex);
     PruneBlockIndexCandidates();
 
     tip = m_chain.Tip();
@@ -5827,7 +5867,7 @@ bool ChainstateManager::PopulateAndValidateSnapshot(
         return false;
     }
 
-    snapshot_chainstate.m_chain.SetTip(snapshot_start_block);
+    snapshot_chainstate.m_chain.SetTip(*snapshot_start_block);
 
     // The remainder of this function requires modifying data protected by cs_main.
     LOCK(::cs_main);
diff --git a/src/validation.h b/src/validation.h
index 814a75d842aa..6eb7608491f3 100644
--- a/src/validation.h
+++ b/src/validation.h
@@ -281,27 +281,39 @@ int GetUTXOConfirmations(CChainState& active_chainstate, const COutPoint& outpoi
 bool CheckFinalTxAtTip(const CBlockIndex& active_chain_tip, const CTransaction& tx) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
 
 /**
- * Check if transaction will be BIP68 final in the next block to be created on top of tip.
- * @param[in]   tip             Chain tip to check tx sequence locks against. For example,
- *                              the tip of the current active chain.
+ * Calculate LockPoints required to check if transaction will be BIP68 final in the next block
+ * to be created on top of tip.
+ *
+ * @param[in]   tip             Chain tip for which tx sequence locks are calculated. For
+ *                              example, the tip of the current active chain.
  * @param[in]   coins_view      Any CCoinsView that provides access to the relevant coins for
  *                              checking sequence locks. For example, it can be a CCoinsViewCache
  *                              that isn't connected to anything but contains all the relevant
  *                              coins, or a CCoinsViewMemPool that is connected to the
- *                              mempool and chainstate UTXO set. In the latter case, the caller is
- *                              responsible for holding the appropriate locks to ensure that
+ *                              mempool and chainstate UTXO set. In the latter case, the caller
+ *                              is responsible for holding the appropriate locks to ensure that
  *                              calls to GetCoin() return correct coins.
+ * @param[in]   tx              The transaction being evaluated.
+ *
+ * @returns The resulting height and time calculated and the hash of the block needed for
+ *          calculation, or std::nullopt if there is an error.
+ */
+std::optional<LockPoints> CalculateLockPointsAtTip(
+    CBlockIndex* tip,
+    const CCoinsView& coins_view,
+    const CTransaction& tx);
+
+/**
+ * Check if transaction will be BIP68 final in the next block to be created on top of tip.
+ * @param[in]   tip             Chain tip to check tx sequence locks against. For example,
+ *                              the tip of the current active chain.
+ * @param[in]   lock_points     LockPoints containing the height and time at which this
+ *                              transaction is final.
  * Simulates calling SequenceLocks() with data from the tip passed in.
- * Optionally stores in LockPoints the resulting height and time calculated and the hash
- * of the block needed for calculation or skips the calculation and uses the LockPoints
- * passed in for evaluation.
  * The LockPoints should not be considered valid if CheckSequenceLocksAtTip returns false.
  */
 bool CheckSequenceLocksAtTip(CBlockIndex* tip,
-                        const CCoinsView& coins_view,
-                        const CTransaction& tx,
-                        LockPoints* lp = nullptr,
-                        bool useExistingLockPoints = false);
+                             const LockPoints& lock_points);
 
 /**
  * Closure representing one script verification
diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp
index af293c83b3d7..617d905170df 100644
--- a/src/wallet/rpc/backup.cpp
+++ b/src/wallet/rpc/backup.cpp
@@ -1980,7 +1980,8 @@ RPCHelpMan listdescriptors()
                         {RPCResult::Type::NUM, "", "Range start inclusive"},
                         {RPCResult::Type::NUM, "", "Range end inclusive"},
                     }},
-                    {RPCResult::Type::NUM, "next", /*optional=*/true, "The next index to generate addresses from; defined only for ranged descriptors"},
+                    {RPCResult::Type::NUM, "next", /*optional=*/true, "Same as next_index field. Kept for compatibility reason."},
+                    {RPCResult::Type::NUM, "next_index", /*optional=*/true, "The next index to generate addresses from; defined only for ranged descriptors"},
                 }},
             }}
         }},
@@ -2041,6 +2042,7 @@ RPCHelpMan listdescriptors()
             range.push_back(wallet_descriptor.range_end - 1);
             spk.pushKV("range", range);
             spk.pushKV("next", wallet_descriptor.next_index);
+            spk.pushKV("next_index", wallet_descriptor.next_index);
         }
         descriptors.push_back(spk);
     }
diff --git a/src/wallet/rpc/encrypt.cpp b/src/wallet/rpc/encrypt.cpp
index 13cdef3d3317..211b255ec1fb 100644
--- a/src/wallet/rpc/encrypt.cpp
+++ b/src/wallet/rpc/encrypt.cpp
@@ -51,9 +51,7 @@ RPCHelpMan walletpassphrase()
         // Note that the walletpassphrase is stored in request.params[0] which is not mlock()ed
         SecureString strWalletPass;
         strWalletPass.reserve(100);
-        // TODO: get rid of this .c_str() by implementing SecureString::operator=(std::string)
-        // Alternately, find a way to make request.params[0] mlock()'d to begin with.
-        strWalletPass = request.params[0].get_str().c_str();
+        strWalletPass = std::string_view{request.params[0].get_str()};
 
         // Get the timeout
         nSleepTime = request.params[1].getInt<int64_t>();
@@ -83,7 +81,17 @@ RPCHelpMan walletpassphrase()
         }
 
         if (!pwallet->Unlock(strWalletPass, fForMixingOnly)) {
-            throw JSONRPCError(RPC_WALLET_PASSPHRASE_INCORRECT, "Error: The wallet passphrase entered was incorrect.");
+            // Check if the passphrase has a null character (see #27067 for details)
+            if (strWalletPass.find('\0') == std::string::npos) {
+                throw JSONRPCError(RPC_WALLET_PASSPHRASE_INCORRECT, "Error: The wallet passphrase entered was incorrect.");
+            } else {
+                throw JSONRPCError(RPC_WALLET_PASSPHRASE_INCORRECT, "Error: The wallet passphrase entered is incorrect. "
+                                                                    "It contains a null character (ie - a zero byte). "
+                                                                    "If the passphrase was set with a version of this software prior to 23.0, "
+                                                                    "please try again with only the characters up to — but not including — "
+                                                                    "the first null character. If this is successful, please set a new "
+                                                                    "passphrase to avoid this issue in the future.");
+            }
         }
 
         pwallet->TopUpKeyPool();
@@ -138,23 +146,29 @@ RPCHelpMan walletpassphrasechange()
     if (!pwallet->IsCrypted()) {
         throw JSONRPCError(RPC_WALLET_WRONG_ENC_STATE, "Error: running with an unencrypted wallet, but walletpassphrasechange was called.");
     }
-
-    // TODO: get rid of these .c_str() calls by implementing SecureString::operator=(std::string)
-    // Alternately, find a way to make request.params[0] mlock()'d to begin with.
     SecureString strOldWalletPass;
     strOldWalletPass.reserve(100);
-    strOldWalletPass = request.params[0].get_str().c_str();
+    strOldWalletPass = std::string_view{request.params[0].get_str()};
 
     SecureString strNewWalletPass;
     strNewWalletPass.reserve(100);
-    strNewWalletPass = request.params[1].get_str().c_str();
+    strNewWalletPass = std::string_view{request.params[1].get_str()};
 
     if (strOldWalletPass.empty() || strNewWalletPass.empty()) {
         throw JSONRPCError(RPC_INVALID_PARAMETER, "passphrase cannot be empty");
     }
 
     if (!pwallet->ChangeWalletPassphrase(strOldWalletPass, strNewWalletPass)) {
-        throw JSONRPCError(RPC_WALLET_PASSPHRASE_INCORRECT, "Error: The wallet passphrase entered was incorrect.");
+        // Check if the old passphrase had a null character (see #27067 for details)
+        if (strOldWalletPass.find('\0') == std::string::npos) {
+            throw JSONRPCError(RPC_WALLET_PASSPHRASE_INCORRECT, "Error: The wallet passphrase entered was incorrect.");
+        } else {
+            throw JSONRPCError(RPC_WALLET_PASSPHRASE_INCORRECT, "Error: The old wallet passphrase entered is incorrect. "
+                                                                "It contains a null character (ie - a zero byte). "
+                                                                "If the old passphrase was set with a version of this software prior to 23.0, "
+                                                                "please try again with only the characters up to — but not including — "
+                                                                "the first null character.");
+        }
     }
 
     return NullUniValue;
@@ -237,12 +251,9 @@ RPCHelpMan encryptwallet()
     if (pwallet->IsCrypted()) {
         throw JSONRPCError(RPC_WALLET_WRONG_ENC_STATE, "Error: running with an encrypted wallet, but encryptwallet was called.");
     }
-
-    // TODO: get rid of this .c_str() by implementing SecureString::operator=(std::string)
-    // Alternately, find a way to make request.params[0] mlock()'d to begin with.
     SecureString strWalletPass;
     strWalletPass.reserve(100);
-    strWalletPass = request.params[0].get_str().c_str();
+    strWalletPass = std::string_view{request.params[0].get_str()};
 
     if (strWalletPass.empty()) {
         throw JSONRPCError(RPC_INVALID_PARAMETER, "passphrase cannot be empty");
diff --git a/src/wallet/rpc/transactions.cpp b/src/wallet/rpc/transactions.cpp
index dbbe2ec394a6..5cfbdc6adbd0 100644
--- a/src/wallet/rpc/transactions.cpp
+++ b/src/wallet/rpc/transactions.cpp
@@ -409,6 +409,7 @@ static void ListTransactions(const CWallet& wallet, const CWalletTx& wtx, int nM
                 entry.pushKV("label", label);
             }
             entry.pushKV("vout", r.vout);
+            entry.pushKV("abandoned", wtx.isAbandoned());
             if (fLong)
                 WalletTxToJSON(wallet, wtx, entry);
             ret.push_back(entry);
@@ -475,8 +476,7 @@ RPCHelpMan listtransactions()
                     },
                     TransactionDescriptionString()),
                     {
-                       {RPCResult::Type::BOOL, "abandoned", /* optional */ true, "'true' if the transaction has been abandoned (inputs are respendable). Only available for the \n"
-                                                            "'send' category of transactions."},
+                       {RPCResult::Type::BOOL, "abandoned", "'true' if the transaction has been abandoned (inputs are respendable)."},
                     })},
             }
         },
@@ -588,7 +588,7 @@ RPCHelpMan listsinceblock()
                     },
                     TransactionDescriptionString()),
                     {
-                        {RPCResult::Type::BOOL, "abandoned", /* optional */ true, "'true' if the transaction has been abandoned (inputs are respendable). Only available for the 'send' category of transactions."},
+                        {RPCResult::Type::BOOL, "abandoned", "'true' if the transaction has been abandoned (inputs are respendable)."},
                         {RPCResult::Type::STR, "label", /* optional */ true, "A comment for the address/transaction, if any."},
                         {RPCResult::Type::STR, "to", "If a comment to is associated with the transaction."},
                     })},
@@ -728,8 +728,7 @@ RPCHelpMan gettransaction()
                                     {RPCResult::Type::NUM, "vout", "the vout value"},
                                     {RPCResult::Type::STR_AMOUNT, "fee", /* optional */ true, "The amount of the fee in " + CURRENCY_UNIT + ". This is negative and only available for the \n"
                                                                                                                        "'send' category of transactions."},
-                                    {RPCResult::Type::BOOL, "abandoned", /* optional */ true, "'true' if the transaction has been abandoned (inputs are respendable). Only available for the \n"
-                                                                         "'send' category of transactions."},
+                                    {RPCResult::Type::BOOL, "abandoned", "'true' if the transaction has been abandoned (inputs are respendable)."},
                             }},
                     }},
                   {RPCResult::Type::STR_HEX, "hex", "Raw data for transaction"},
diff --git a/src/wallet/rpc/wallet.cpp b/src/wallet/rpc/wallet.cpp
index 38597ef270d3..9a3cc0c288e8 100644
--- a/src/wallet/rpc/wallet.cpp
+++ b/src/wallet/rpc/wallet.cpp
@@ -597,7 +597,7 @@ static RPCHelpMan createwallet()
     passphrase.reserve(100);
     std::vector<bilingual_str> warnings;
     if (!request.params[3].isNull()) {
-        passphrase = request.params[3].get_str().c_str();
+        passphrase = std::string_view{request.params[3].get_str()};
         if (passphrase.empty()) {
             // Empty string means unencrypted
             warnings.emplace_back(Untranslated("Empty string given as passphrase, wallet will not be encrypted."));
diff --git a/src/wallet/spend.cpp b/src/wallet/spend.cpp
index ae1ded29979d..fe2bb67acab8 100644
--- a/src/wallet/spend.cpp
+++ b/src/wallet/spend.cpp
@@ -778,7 +778,7 @@ static std::optional<CreatedTransactionResult> CreateTransactionInternal(
     }
     if (feeCalc.reason == FeeReason::FALLBACK && !wallet.m_allow_fallback_fee) {
         // eventually allow a fallback fee
-        error = _("Fee estimation failed. Fallbackfee is disabled. Wait a few blocks or enable -fallbackfee.");
+        error = strprintf(_("Fee estimation failed. Fallbackfee is disabled. Wait a few blocks or enable %s."), "-fallbackfee");
         return std::nullopt;
     }
 
@@ -816,6 +816,13 @@ static std::optional<CreatedTransactionResult> CreateTransactionInternal(
     const CAmount not_input_fees = coin_selection_params.m_effective_feerate.GetFee(coin_selection_params.tx_noinputs_size);
     CAmount selection_target = recipients_sum + not_input_fees;
 
+    // This can only happen if feerate is 0, and requested destinations are value of 0 (e.g. OP_RETURN)
+    // and no pre-selected inputs. This will result in 0-input transaction, which is consensus-invalid anyways
+    if (selection_target == 0 && !coin_control.HasSelected()) {
+        error = _("Transaction requires one destination of non-0 value, a non-0 feerate, or a pre-selected input");
+        return std::nullopt;
+    }
+
     // Get available coins
     auto res_available_coins = AvailableCoins(wallet,
                                               &coin_control,
@@ -1060,12 +1067,13 @@ std::optional<CreatedTransactionResult> CreateTransaction(
         }
 
         std::optional<CreatedTransactionResult> txr_grouped = CreateTransactionInternal(wallet, vecSend, change_pos, error2, tmp_cc, fee_calc_out, sign, nExtraPayloadSize);
+        // if fee of this alternative one is within the range of the max fee, we use this one
+        const bool use_aps{txr_grouped.has_value() ? (txr_grouped->fee <= txr_ungrouped->fee + wallet.m_max_aps_fee) : false};
+        TRACE5(coin_selection, aps_create_tx_internal, wallet.GetName().c_str(), use_aps, txr_grouped.has_value(),
+               txr_grouped.has_value() ? txr_grouped->fee : 0, txr_grouped.has_value() ? txr_grouped->change_pos : 0);
         if (txr_grouped) {
-            // if fee of this alternative one is within the range of the max fee, we use this one
-            const bool use_aps = txr_grouped->fee <= txr_ungrouped->fee + wallet.m_max_aps_fee;
             wallet.WalletLogPrintf("Fee non-grouped = %lld, grouped = %lld, using %s\n",
                 txr_ungrouped->fee, txr_grouped->fee, use_aps ? "grouped" : "non-grouped");
-            TRACE5(coin_selection, aps_create_tx_internal, wallet.GetName().c_str(), use_aps, true, txr_grouped->fee, txr_grouped->change_pos);
             if (use_aps) return txr_grouped;
         }
     }
diff --git a/src/wallet/sqlite.cpp b/src/wallet/sqlite.cpp
index fc0fe8ff9e67..2e31688712c2 100644
--- a/src/wallet/sqlite.cpp
+++ b/src/wallet/sqlite.cpp
@@ -23,9 +23,6 @@
 namespace wallet {
 static constexpr int32_t WALLET_SCHEMA_VERSION = 0;
 
-static Mutex g_sqlite_mutex;
-static int g_sqlite_count GUARDED_BY(g_sqlite_mutex) = 0;
-
 static Span<const std::byte> SpanFromBlob(sqlite3_stmt* stmt, int col)
 {
     return {reinterpret_cast<const std::byte*>(sqlite3_column_blob(stmt, col)),
@@ -104,6 +101,9 @@ static void SetPragma(sqlite3* db, const std::string& key, const std::string& va
     }
 }
 
+Mutex SQLiteDatabase::g_sqlite_mutex;
+int SQLiteDatabase::g_sqlite_count = 0;
+
 SQLiteDatabase::SQLiteDatabase(const fs::path& dir_path, const fs::path& file_path, const DatabaseOptions& options, bool mock)
     : WalletDatabase(), m_mock(mock), m_dir_path(fs::PathToString(dir_path)), m_file_path(fs::PathToString(file_path)), m_use_unsafe_sync(options.use_unsafe_sync)
 {
@@ -167,6 +167,8 @@ SQLiteDatabase::~SQLiteDatabase()
 
 void SQLiteDatabase::Cleanup() noexcept
 {
+    AssertLockNotHeld(g_sqlite_mutex);
+
     Close();
 
     LOCK(g_sqlite_mutex);
diff --git a/src/wallet/sqlite.h b/src/wallet/sqlite.h
index 47b7ebb0ecde..d7135679ff8a 100644
--- a/src/wallet/sqlite.h
+++ b/src/wallet/sqlite.h
@@ -5,6 +5,7 @@
 #ifndef BITCOIN_WALLET_SQLITE_H
 #define BITCOIN_WALLET_SQLITE_H
 
+#include <sync.h>
 #include <wallet/db.h>
 
 #include <sqlite3.h>
@@ -63,7 +64,16 @@ class SQLiteDatabase : public WalletDatabase
 
     const std::string m_file_path;
 
-    void Cleanup() noexcept;
+    /**
+     * This mutex protects SQLite initialization and shutdown.
+     * sqlite3_config() and sqlite3_shutdown() are not thread-safe (sqlite3_initialize() is).
+     * Concurrent threads that execute SQLiteDatabase::SQLiteDatabase() should have just one
+     * of them do the init and the rest wait for it to complete before all can proceed.
+     */
+    static Mutex g_sqlite_mutex;
+    static int g_sqlite_count GUARDED_BY(g_sqlite_mutex);
+
+    void Cleanup() noexcept EXCLUSIVE_LOCKS_REQUIRED(!g_sqlite_mutex);
 
 public:
     SQLiteDatabase() = delete;
diff --git a/src/wallet/test/fuzz/coincontrol.cpp b/src/wallet/test/fuzz/coincontrol.cpp
new file mode 100644
index 000000000000..4a47b33f71c9
--- /dev/null
+++ b/src/wallet/test/fuzz/coincontrol.cpp
@@ -0,0 +1,89 @@
+// Copyright (c) 2022 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 <test/fuzz/FuzzedDataProvider.h>
+#include <test/fuzz/fuzz.h>
+#include <test/fuzz/util.h>
+#include <test/util/setup_common.h>
+#include <wallet/coincontrol.h>
+#include <wallet/test/util.h>
+
+namespace wallet {
+namespace {
+
+const TestingSetup* g_setup;
+
+void initialize_coincontrol()
+{
+    static const auto testing_setup = MakeNoLogFileContext<const TestingSetup>();
+    g_setup = testing_setup.get();
+}
+
+FUZZ_TARGET(coincontrol, .init = initialize_coincontrol)
+{
+    FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size());
+    const auto& node = g_setup->m_node;
+    ArgsManager& args = *node.args;
+
+    // for GetBoolArg to return true sometimes
+    args.ForceSetArg("-avoidpartialspends", fuzzed_data_provider.ConsumeBool()?"1":"0");
+
+    CCoinControl coin_control;
+    COutPoint out_point;
+
+    LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 10000)
+    {
+        CallOneOf(
+            fuzzed_data_provider,
+            [&] {
+                std::optional<COutPoint> optional_out_point = ConsumeDeserializable<COutPoint>(fuzzed_data_provider);
+                if (!optional_out_point) {
+                    return;
+                }
+                out_point = *optional_out_point;
+            },
+            [&] {
+                (void)coin_control.HasSelected();
+            },
+            [&] {
+                (void)coin_control.IsSelected(out_point);
+            },
+            [&] {
+                (void)coin_control.IsExternalSelected(out_point);
+            },
+            [&] {
+                CTxOut txout;
+                (void)coin_control.GetExternalOutput(out_point, txout);
+            },
+            [&] {
+                (void)coin_control.Select(out_point);
+            },
+            [&] {
+                const CTxOut tx_out{ConsumeMoney(fuzzed_data_provider), ConsumeScript(fuzzed_data_provider)};
+                (void)coin_control.SelectExternal(out_point, tx_out);
+            },
+            [&] {
+                (void)coin_control.UnSelect(out_point);
+            },
+            [&] {
+                (void)coin_control.UnSelectAll();
+            },
+            [&] {
+                std::vector<COutPoint> selected;
+                coin_control.ListSelected(selected);
+            },
+            [&] {
+                int64_t weight{fuzzed_data_provider.ConsumeIntegral<int64_t>()};
+                (void)coin_control.SetInputWeight(out_point, weight);
+            },
+            [&] {
+                // Condition to avoid the assertion in GetInputWeight
+                if (coin_control.HasInputWeight(out_point)) {
+                    (void)coin_control.GetInputWeight(out_point);
+                }
+            });
+    }
+}
+} // namespace
+} // namespace wallet
diff --git a/src/wallet/test/fuzz/coinselection.cpp b/src/wallet/test/fuzz/coinselection.cpp
index 39f9346f3ef9..e76064573ac9 100644
--- a/src/wallet/test/fuzz/coinselection.cpp
+++ b/src/wallet/test/fuzz/coinselection.cpp
@@ -15,13 +15,13 @@
 
 namespace wallet {
 
-static void AddCoin(const CAmount& value, int n_input, int n_input_bytes, int locktime, std::vector<COutput>& coins)
+static void AddCoin(const CAmount& value, int n_input, int n_input_bytes, int locktime, std::vector<COutput>& coins, CFeeRate fee_rate)
 {
     CMutableTransaction tx;
     tx.vout.resize(n_input + 1);
     tx.vout[n_input].nValue = value;
     tx.nLockTime = locktime; // all transactions get different hashes
-    coins.emplace_back(COutPoint(tx.GetHash(), n_input), tx.vout.at(n_input), /*depth=*/0, n_input_bytes, /*spendable=*/true, /*solvable=*/true, /*safe=*/true, /*time=*/0, /*from_me=*/true);
+    coins.emplace_back(COutPoint(tx.GetHash(), n_input), tx.vout.at(n_input), /*depth=*/0, n_input_bytes, /*spendable=*/true, /*solvable=*/true, /*safe=*/true, /*time=*/0, /*from_me=*/true, fee_rate);
 }
 
 // Randomly distribute coins to instances of OutputGroup
@@ -71,7 +71,7 @@ FUZZ_TARGET(coinselection)
         if (total_balance + amount >= MAX_MONEY) {
             break;
         }
-        AddCoin(amount, n_input, n_input_bytes, ++next_locktime, utxo_pool);
+        AddCoin(amount, n_input, n_input_bytes, ++next_locktime, utxo_pool, coin_params.m_effective_feerate);
         total_balance += amount;
     }
 
diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp
index 6872fc06b963..6be001466e2c 100644
--- a/src/wallet/test/wallet_tests.cpp
+++ b/src/wallet/test/wallet_tests.cpp
@@ -458,7 +458,7 @@ BOOST_AUTO_TEST_CASE(LoadReceiveRequests)
 
 // Test some watch-only LegacyScriptPubKeyMan methods by the procedure of loading (LoadWatchOnly),
 // checking (HaveWatchOnly), getting (GetWatchPubKey) and removing (RemoveWatchOnly) a
-// given PubKey, resp. its corresponding P2PK Script. Results of the the impact on
+// given PubKey, resp. its corresponding P2PK Script. Results of the impact on
 // the address -> PubKey map is dependent on whether the PubKey is a point on the curve
 static void TestWatchOnlyPubKey(LegacyScriptPubKeyMan* spk_man, const CPubKey& add_pubkey)
 {
diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp
index 6979c5d49fae..dc6be0d2d13e 100644
--- a/src/wallet/wallet.cpp
+++ b/src/wallet/wallet.cpp
@@ -10,7 +10,6 @@
 #include <chainparams.h>
 #include <consensus/amount.h>
 #include <consensus/consensus.h>
-#include <consensus/validation.h>
 #include <crypto/common.h>
 #include <fs.h>
 #include <interfaces/chain.h>
@@ -19,7 +18,6 @@
 #include <key_io.h>
 #include <policy/fees.h>
 #include <policy/policy.h>
-#include <policy/settings.h>
 #include <primitives/block.h>
 #include <primitives/transaction.h>
 #include <script/descriptor.h>
@@ -28,10 +26,8 @@
 #include <script/signingprovider.h>
 #include <support/cleanse.h>
 #include <txmempool.h>
-#include <util/bip32.h>
 #include <util/check.h>
 #include <util/error.h>
-#include <util/fees.h>
 #include <util/moneystr.h>
 #include <util/string.h>
 #include <util/translation.h>
@@ -40,11 +36,9 @@
 #endif
 #include <wallet/bip39.h> // TODO(refactor): move dependency it to scriptpubkeyman.cpp
 #include <wallet/coincontrol.h>
-#include <wallet/coinselection.h>
 #include <wallet/context.h>
 #include <warnings.h>
 
-#include <coinjoin/common.h>
 #include <coinjoin/options.h>
 #include <evo/providertx.h>
 #include <governance/vote.h>
@@ -488,13 +482,13 @@ bool CWallet::ChangeWalletPassphrase(const SecureString& strOldWalletPassphrase,
                 return false;
             if (Unlock(_vMasterKey))
             {
-                int64_t nStartTime = GetTimeMillis();
+                int64_t nStartTime = TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now());
                 crypter.SetKeyFromPassphrase(strNewWalletPassphrase, pMasterKey.second.vchSalt, pMasterKey.second.nDeriveIterations, pMasterKey.second.nDerivationMethod);
-                pMasterKey.second.nDeriveIterations = static_cast<unsigned int>(pMasterKey.second.nDeriveIterations * (100 / ((double)(GetTimeMillis() - nStartTime))));
+                pMasterKey.second.nDeriveIterations = static_cast<unsigned int>(pMasterKey.second.nDeriveIterations * (100 / ((double)(TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now()) - nStartTime))));
 
-                nStartTime = GetTimeMillis();
+                nStartTime = TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now());
                 crypter.SetKeyFromPassphrase(strNewWalletPassphrase, pMasterKey.second.vchSalt, pMasterKey.second.nDeriveIterations, pMasterKey.second.nDerivationMethod);
-                pMasterKey.second.nDeriveIterations = (pMasterKey.second.nDeriveIterations + static_cast<unsigned int>(pMasterKey.second.nDeriveIterations * 100 / ((double)(GetTimeMillis() - nStartTime)))) / 2;
+                pMasterKey.second.nDeriveIterations = (pMasterKey.second.nDeriveIterations + static_cast<unsigned int>(pMasterKey.second.nDeriveIterations * 100 / ((double)(TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now()) - nStartTime)))) / 2;
 
                 if (pMasterKey.second.nDeriveIterations < 25000)
                     pMasterKey.second.nDeriveIterations = 25000;
@@ -694,13 +688,13 @@ bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase)
     GetStrongRandBytes(kMasterKey.vchSalt);
 
     CCrypter crypter;
-    int64_t nStartTime = GetTimeMillis();
+    int64_t nStartTime = TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now());
     crypter.SetKeyFromPassphrase(strWalletPassphrase, kMasterKey.vchSalt, 25000, kMasterKey.nDerivationMethod);
-    kMasterKey.nDeriveIterations = static_cast<unsigned int>(2500000 / ((double)(GetTimeMillis() - nStartTime)));
+    kMasterKey.nDeriveIterations = static_cast<unsigned int>(2500000 / ((double)(TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now()) - nStartTime)));
 
-    nStartTime = GetTimeMillis();
+    nStartTime = TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now());
     crypter.SetKeyFromPassphrase(strWalletPassphrase, kMasterKey.vchSalt, kMasterKey.nDeriveIterations, kMasterKey.nDerivationMethod);
-    kMasterKey.nDeriveIterations = (kMasterKey.nDeriveIterations + static_cast<unsigned int>(kMasterKey.nDeriveIterations * 100 / ((double)(GetTimeMillis() - nStartTime)))) / 2;
+    kMasterKey.nDeriveIterations = (kMasterKey.nDeriveIterations + static_cast<unsigned int>(kMasterKey.nDeriveIterations * 100 / ((double)(TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now()) - nStartTime)))) / 2;
 
     if (kMasterKey.nDeriveIterations < 25000)
         kMasterKey.nDeriveIterations = 25000;
@@ -2971,7 +2965,7 @@ std::shared_ptr<CWallet> CWallet::Create(WalletContext& context, const std::stri
     if (args.IsArgSet("-fallbackfee")) {
         std::optional<CAmount> fallback_fee = ParseMoney(args.GetArg("-fallbackfee", ""));
         if (!fallback_fee) {
-            error = strprintf(_("Invalid amount for -fallbackfee=<amount>: '%s'"), args.GetArg("-fallbackfee", ""));
+            error = strprintf(_("Invalid amount for %s=<amount>: '%s'"), "-fallbackfee", args.GetArg("-fallbackfee", ""));
             return nullptr;
         } else if (fallback_fee.value() > HIGH_TX_FEE_PER_KB) {
             warnings.push_back(AmountHighWarn("-fallbackfee") + Untranslated(" ") +
@@ -2985,7 +2979,7 @@ std::shared_ptr<CWallet> CWallet::Create(WalletContext& context, const std::stri
     if (args.IsArgSet("-discardfee")) {
         std::optional<CAmount> discard_fee = ParseMoney(args.GetArg("-discardfee", ""));
         if (!discard_fee) {
-            error = strprintf(_("Invalid amount for -discardfee=<amount>: '%s'"), args.GetArg("-discardfee", ""));
+            error = strprintf(_("Invalid amount for %s=<amount>: '%s'"), "-discardfee", args.GetArg("-discardfee", ""));
             return nullptr;
         } else if (discard_fee.value() > HIGH_TX_FEE_PER_KB) {
             warnings.push_back(AmountHighWarn("-discardfee") + Untranslated(" ") +
@@ -3005,8 +2999,8 @@ std::shared_ptr<CWallet> CWallet::Create(WalletContext& context, const std::stri
         }
         walletInstance->m_pay_tx_fee = CFeeRate{pay_tx_fee.value(), 1000};
         if (chain && walletInstance->m_pay_tx_fee < chain->relayMinFee()) {
-            error = strprintf(_("Invalid amount for -paytxfee=<amount>: '%s' (must be at least %s)"),
-                args.GetArg("-paytxfee", ""), chain->relayMinFee().ToString());
+            error = strprintf(_("Invalid amount for %s=<amount>: '%s' (must be at least %s)"),
+                "-paytxfee", args.GetArg("-paytxfee", ""), chain->relayMinFee().ToString());
             return nullptr;
         }
     }
@@ -3017,11 +3011,11 @@ std::shared_ptr<CWallet> CWallet::Create(WalletContext& context, const std::stri
             error = AmountErrMsg("maxtxfee", args.GetArg("-maxtxfee", ""));
             return nullptr;
         } else if (max_fee.value() > HIGH_MAX_TX_FEE) {
-            warnings.push_back(_("-maxtxfee is set very high! Fees this large could be paid on a single transaction."));
+            warnings.push_back(strprintf(_("%s is set very high! Fees this large could be paid on a single transaction."), "-maxtxfee"));
         }
         if (chain && CFeeRate{max_fee.value(), 1000} < chain->relayMinFee()) {
-            error = strprintf(_("Invalid amount for -maxtxfee=<amount>: '%s' (must be at least the minrelay fee of %s to prevent stuck transactions)"),
-                args.GetArg("-maxtxfee", ""), chain->relayMinFee().ToString());
+            error = strprintf(_("Invalid amount for %s=<amount>: '%s' (must be at least the minrelay fee of %s to prevent stuck transactions)"),
+                "-maxtxfee", args.GetArg("-maxtxfee", ""), chain->relayMinFee().ToString());
             return nullptr;
         }
 
@@ -3228,7 +3222,24 @@ bool CWallet::UpgradeToHD(const SecureString& secureMnemonic, const SecureString
     SetMinVersion(FEATURE_HD);
 
     if (IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) {
+        if (IsCrypted()) {
+            if (secureWalletPassphrase.empty()) {
+                error = Untranslated("Error: Wallet encrypted but supplied empty wallet passphrase");
+                return false;
+            }
+
+            // Unlock the wallet
+            if (!Unlock(secureWalletPassphrase)) {
+                error = Untranslated("Error: The wallet passphrase entered was incorrect");
+                return false;
+            }
+        }
         SetupDescriptorScriptPubKeyMans(secureMnemonic, secureMnemonicPassphrase);
+
+        if (IsCrypted()) {
+            // Relock the wallet
+            Lock();
+        }
     } else {
         if (!GenerateNewHDChain(secureMnemonic, secureMnemonicPassphrase, secureWalletPassphrase)) {
             error = Untranslated("Failed to generate HD wallet");
diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h
index 0e89400962a2..7630a7ab03ea 100644
--- a/src/wallet/wallet.h
+++ b/src/wallet/wallet.h
@@ -574,7 +574,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
     void AbortRescan() { fAbortRescan = true; }
     bool IsAbortingRescan() const { return fAbortRescan; }
     bool IsScanning() const { return fScanningWallet; }
-    int64_t ScanningDuration() const { return fScanningWallet ? GetTimeMillis() - m_scanning_start : 0; }
+    int64_t ScanningDuration() const { return fScanningWallet ? TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now()) - m_scanning_start : 0; }
     double ScanningProgress() const { return fScanningWallet ? (double) m_scanning_progress : 0; }
 
     //! Upgrade stored CKeyMetadata objects to store key origin info as KeyOriginInfo
@@ -1086,7 +1086,7 @@ class WalletRescanReserver
         if (m_wallet.fScanningWallet.exchange(true)) {
             return false;
         }
-        m_wallet.m_scanning_start = GetTimeMillis();
+        m_wallet.m_scanning_start = TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now());
         m_wallet.m_scanning_progress = 0;
         m_wallet.fAbortRescan = false;
         m_could_reserve = true;
diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp
index 5808dfbffef4..f402a626a981 100644
--- a/src/wallet/walletdb.cpp
+++ b/src/wallet/walletdb.cpp
@@ -1235,13 +1235,36 @@ std::unique_ptr<WalletDatabase> CreateDummyWalletDatabase()
 }
 
 /** Return object for accessing temporary in-memory database. */
-std::unique_ptr<WalletDatabase> CreateMockWalletDatabase()
+std::unique_ptr<WalletDatabase> CreateMockWalletDatabase(DatabaseOptions& options)
 {
-    DatabaseOptions options;
+
+    std::optional<DatabaseFormat> format;
+    if (options.require_format) format = options.require_format;
+    if (!format) {
+#ifdef USE_BDB
+        format = DatabaseFormat::BERKELEY;
+#endif
 #ifdef USE_SQLITE
-    return std::make_unique<SQLiteDatabase>("", "", options, true);
-#elif defined(USE_BDB)
+        format = DatabaseFormat::SQLITE;
+#endif
+    }
+
+    if (format == DatabaseFormat::SQLITE) {
+#ifdef USE_SQLITE
+        return std::make_unique<SQLiteDatabase>(":memory:", "", options, true);
+#endif
+        assert(false);
+    }
+
+#ifdef USE_BDB
     return std::make_unique<BerkeleyDatabase>(std::make_shared<BerkeleyEnvironment>(), "", options);
 #endif
+    assert(false);
+}
+
+std::unique_ptr<WalletDatabase> CreateMockWalletDatabase()
+{
+    DatabaseOptions options;
+    return CreateMockWalletDatabase(options);
 }
 } // namespace wallet
diff --git a/src/wallet/walletdb.h b/src/wallet/walletdb.h
index 43cd4885acc5..574cea38061c 100644
--- a/src/wallet/walletdb.h
+++ b/src/wallet/walletdb.h
@@ -267,6 +267,7 @@ bool ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue, st
 std::unique_ptr<WalletDatabase> CreateDummyWalletDatabase();
 
 /** Return object for accessing temporary in-memory database. */
+std::unique_ptr<WalletDatabase> CreateMockWalletDatabase(DatabaseOptions& options);
 std::unique_ptr<WalletDatabase> CreateMockWalletDatabase();
 } // namespace wallet
 
diff --git a/test/README.md b/test/README.md
index fac2d4506d05..3f0d7c2c9cd4 100644
--- a/test/README.md
+++ b/test/README.md
@@ -112,34 +112,57 @@ how many jobs to run, append `--jobs=n`
 The individual tests and the test_runner harness have many command-line
 options. Run `test/functional/test_runner.py -h` to see them all.
 
-#### Speed up test runs with a ramdisk
+#### Speed up test runs with a RAM disk
 
-If you have available RAM on your system you can create a ramdisk to use as the `cache` and `tmp` directories for the functional tests in order to speed them up.
-Speed-up amount varies on each system (and according to your ram speed and other variables), but a 2-3x speed-up is not uncommon.
+If you have available RAM on your system you can create a RAM disk to use as the `cache` and `tmp` directories for the functional tests in order to speed them up.
+Speed-up amount varies on each system (and according to your RAM speed and other variables), but a 2-3x speed-up is not uncommon.
 
-To create a 4GB ramdisk on Linux at `/mnt/tmp/`:
+**Linux**
+
+To create a 4 GiB RAM disk at `/mnt/tmp/`:
 
 ```bash
 sudo mkdir -p /mnt/tmp
 sudo mount -t tmpfs -o size=4g tmpfs /mnt/tmp/
 ```
 
-Configure the size of the ramdisk using the `size=` option.
-The size of the ramdisk needed is relative to the number of concurrent jobs the test suite runs.
-For example running the test suite with `--jobs=100` might need a 16GB ramdisk, but running with `--jobs=4` will only need a 4GB ramdisk.
+Configure the size of the RAM disk using the `size=` option.
+The size of the RAM disk needed is relative to the number of concurrent jobs the test suite runs.
+For example running the test suite with `--jobs=100` might need a 4 GiB RAM disk, but running with `--jobs=32` will only need a 2.5 GiB RAM disk.
 
-To use, run the test suite specifying the ramdisk as the `cachedir` and `tmpdir`:
+To use, run the test suite specifying the RAM disk as the `cachedir` and `tmpdir`:
 
 ```bash
 test/functional/test_runner.py --cachedir=/mnt/tmp/cache --tmpdir=/mnt/tmp
 ```
 
-Once finished with the tests and the disk, and to free the ram, simply unmount the disk:
+Once finished with the tests and the disk, and to free the RAM, simply unmount the disk:
 
 ```bash
 sudo umount /mnt/tmp
 ```
 
+**macOS**
+
+To create a 4 GiB RAM disk named "ramdisk" at `/Volumes/ramdisk/`:
+
+```bash
+diskutil erasevolume HFS+ ramdisk $(hdiutil attach -nomount ram://8388608)
+```
+
+Configure the RAM disk size, expressed as the number of blocks, at the end of the command
+(`4096 MiB * 2048 blocks/MiB = 8388608 blocks` for 4 GiB). To run the tests using the RAM disk:
+
+```bash
+test/functional/test_runner.py --cachedir=/Volumes/ramdisk/cache --tmpdir=/Volumes/ramdisk/tmp
+```
+
+To unmount:
+
+```bash
+umount /Volumes/ramdisk
+```
+
 #### Troubleshooting and debugging test failures
 
 ##### Resource contention
@@ -311,6 +334,7 @@ Use the `-v` option for verbose output.
 | Lint test | Dependency |
 |-----------|:----------:|
 | [`lint-python.py`](lint/lint-python.py) | [flake8](https://gitlab.com/pycqa/flake8)
+| [`lint-python.py`](lint/lint-python.py) | [lief](https://github.com/lief-project/LIEF)
 | [`lint-python.py`](lint/lint-python.py) | [mypy](https://github.com/python/mypy)
 | [`lint-python.py`](lint/lint-python.py) | [pyzmq](https://github.com/zeromq/pyzmq)
 | [`lint-python-dead-code.py`](lint/lint-python-dead-code.py) | [vulture](https://github.com/jendrikseipp/vulture)
diff --git a/test/functional/feature_addrman.py b/test/functional/feature_addrman.py
index 51203f8d4910..490b554a3225 100755
--- a/test/functional/feature_addrman.py
+++ b/test/functional/feature_addrman.py
@@ -9,11 +9,11 @@
 import struct
 
 from test_framework.messages import ser_uint256, hash256, MAGIC_BYTES
+from test_framework.netutil import ADDRMAN_NEW_BUCKET_COUNT, ADDRMAN_TRIED_BUCKET_COUNT, ADDRMAN_BUCKET_SIZE
 from test_framework.test_framework import BitcoinTestFramework
 from test_framework.test_node import ErrorMatch
 from test_framework.util import assert_equal
 
-
 def serialize_addrman(
     *,
     format=1,
@@ -116,17 +116,34 @@ def run_test(self):
 
         self.log.info("Check that corrupt addrman cannot be read (len_tried)")
         self.stop_node(0)
+        max_len_tried = ADDRMAN_TRIED_BUCKET_COUNT * ADDRMAN_BUCKET_SIZE
         write_addrman(peers_dat, len_tried=-1)
         self.nodes[0].assert_start_raises_init_error(
-            expected_msg=init_error("Corrupt AddrMan serialization: nTried=-1, should be in \\[0, 16384\\]:.*"),
+            expected_msg=init_error(f"Corrupt AddrMan serialization: nTried=-1, should be in \\[0, {max_len_tried}\\]:.*"),
+            match=ErrorMatch.FULL_REGEX,
+        )
+
+        self.log.info("Check that corrupt addrman cannot be read (large len_tried)")
+        write_addrman(peers_dat, len_tried=max_len_tried + 1)
+        self.nodes[0].assert_start_raises_init_error(
+            expected_msg=init_error(f"Corrupt AddrMan serialization: nTried={max_len_tried + 1}, should be in \\[0, {max_len_tried}\\]:.*"),
             match=ErrorMatch.FULL_REGEX,
         )
 
         self.log.info("Check that corrupt addrman cannot be read (len_new)")
         self.stop_node(0)
+        max_len_new = ADDRMAN_NEW_BUCKET_COUNT * ADDRMAN_BUCKET_SIZE
         write_addrman(peers_dat, len_new=-1)
         self.nodes[0].assert_start_raises_init_error(
-            expected_msg=init_error("Corrupt AddrMan serialization: nNew=-1, should be in \\[0, 65536\\]:.*"),
+            expected_msg=init_error(f"Corrupt AddrMan serialization: nNew=-1, should be in \\[0, {max_len_new}\\]:.*"),
+            match=ErrorMatch.FULL_REGEX,
+        )
+
+        self.log.info("Check that corrupt addrman cannot be read (large len_new)")
+        self.stop_node(0)
+        write_addrman(peers_dat, len_new=max_len_new + 1)
+        self.nodes[0].assert_start_raises_init_error(
+            expected_msg=init_error(f"Corrupt AddrMan serialization: nNew={max_len_new + 1}, should be in \\[0, {max_len_new}\\]:.*"),
             match=ErrorMatch.FULL_REGEX,
         )
 
diff --git a/test/functional/feature_block.py b/test/functional/feature_block.py
index bc7a45d7e903..99e70478f71f 100755
--- a/test/functional/feature_block.py
+++ b/test/functional/feature_block.py
@@ -171,7 +171,7 @@ def run_test(self):
 
             self.log.info(f"Reject block with invalid tx: {TxTemplate.__name__}")
             blockname = f"for_invalid.{TxTemplate.__name__}"
-            badblock = self.next_block(blockname)
+            self.next_block(blockname)
             badtx = template.get_tx()
             if TxTemplate != invalid_txs.InputMissing:
                 self.sign_tx(badtx, attempt_spend_tx)
@@ -483,7 +483,7 @@ def run_test(self):
         #
         self.log.info("Check P2SH SIGOPS are correctly counted")
         self.move_tip(35)
-        b39 = self.next_block(39)
+        self.next_block(39)
         b39_outputs = 0
         b39_sigops_per_output = 6
 
@@ -682,7 +682,7 @@ def run_test(self):
 
         self.log.info("Reject a block with two coinbase transactions")
         self.move_tip(44)
-        b51 = self.next_block(51)
+        self.next_block(51)
         cb2 = create_coinbase(51, self.coinbase_pubkey)
         b51 = self.update_block(51, [cb2])
         self.send_blocks([b51], success=False, reject_reason='bad-cb-multiple', reconnect=True)
@@ -762,7 +762,7 @@ def run_test(self):
 
         # b57 - a good block with 2 txs, don't submit until end
         self.move_tip(55)
-        b57 = self.next_block(57)
+        self.next_block(57)
         tx = self.create_and_sign_transaction(out[16], 1)
         tx1 = self.create_tx(tx, 0, 1)
         b57 = self.update_block(57, [tx, tx1])
@@ -779,7 +779,7 @@ def run_test(self):
 
         # b57p2 - a good block with 6 tx'es, don't submit until end
         self.move_tip(55)
-        b57p2 = self.next_block("57p2")
+        self.next_block("57p2")
         tx = self.create_and_sign_transaction(out[16], 1)
         tx1 = self.create_tx(tx, 0, 1)
         tx2 = self.create_tx(tx1, 0, 1)
@@ -813,7 +813,7 @@ def run_test(self):
         # tx with prevout.n out of range
         self.log.info("Reject a block with a transaction with prevout.n out of range")
         self.move_tip(57)
-        b58 = self.next_block(58, spend=out[17])
+        self.next_block(58, spend=out[17])
         tx = CTransaction()
         assert len(out[17].vout) < 42
         tx.vin.append(CTxIn(COutPoint(out[17].sha256, 42), CScript([OP_TRUE]), SEQUENCE_FINAL))
@@ -825,7 +825,7 @@ def run_test(self):
         # tx with output value > input value
         self.log.info("Reject a block with a transaction with outputs > inputs")
         self.move_tip(57)
-        b59 = self.next_block(59)
+        self.next_block(59)
         tx = self.create_and_sign_transaction(out[17], 510 * COIN)
         b59 = self.update_block(59, [tx])
         self.send_blocks([b59], success=False, reject_reason='bad-txns-in-belowout', reconnect=True)
@@ -861,7 +861,7 @@ def run_test(self):
         #            \-> b_spend_dup_cb (b_dup_cb) -> b_dup_2 ()
         #
         self.move_tip(57)
-        b_spend_dup_cb = self.next_block('spend_dup_cb')
+        self.next_block('spend_dup_cb')
         tx = CTransaction()
         tx.vin.append(CTxIn(COutPoint(duplicate_tx.sha256, 0)))
         tx.vout.append(CTxOut(0, CScript([OP_TRUE])))
@@ -886,7 +886,7 @@ def run_test(self):
         #
         self.log.info("Reject a block with a transaction with a nonfinal locktime")
         self.move_tip('dup_2')
-        b62 = self.next_block(62)
+        self.next_block(62)
         tx = CTransaction()
         tx.nLockTime = 0xffffffff  # this locktime is non-final
         tx.vin.append(CTxIn(COutPoint(out[18].sha256, 0)))  # don't set nSequence
@@ -967,7 +967,7 @@ def run_test(self):
         #
         self.log.info("Accept a block with a transaction spending an output created in the same block")
         self.move_tip(64)
-        b65 = self.next_block(65)
+        self.next_block(65)
         tx1 = self.create_and_sign_transaction(out[19], out[19].vout[0].nValue)
         tx2 = self.create_and_sign_transaction(tx1, 0)
         b65 = self.update_block(65, [tx1, tx2])
@@ -980,7 +980,7 @@ def run_test(self):
         #                        \-> b66 (20)
         self.log.info("Reject a block with a transaction spending an output created later in the same block")
         self.move_tip(65)
-        b66 = self.next_block(66)
+        self.next_block(66)
         tx1 = self.create_and_sign_transaction(out[20], out[20].vout[0].nValue)
         tx2 = self.create_and_sign_transaction(tx1, 1)
         b66 = self.update_block(66, [tx2, tx1])
@@ -994,7 +994,7 @@ def run_test(self):
         #
         self.log.info("Reject a block with a transaction double spending a transaction created in the same block")
         self.move_tip(65)
-        b67 = self.next_block(67)
+        self.next_block(67)
         tx1 = self.create_and_sign_transaction(out[20], out[20].vout[0].nValue)
         tx2 = self.create_and_sign_transaction(tx1, 1)
         tx3 = self.create_and_sign_transaction(tx1, 2)
@@ -1015,7 +1015,7 @@ def run_test(self):
         #
         self.log.info("Reject a block trying to claim too much subsidy in the coinbase transaction")
         self.move_tip(65)
-        b68 = self.next_block(68, additional_coinbase_value=10)
+        self.next_block(68, additional_coinbase_value=10)
         tx = self.create_and_sign_transaction(out[20], out[20].vout[0].nValue - 9)
         b68 = self.update_block(68, [tx])
         self.send_blocks([b68], success=False, reject_reason='bad-cb-amount', reconnect=False)
@@ -1035,7 +1035,7 @@ def run_test(self):
         #
         self.log.info("Reject a block containing a transaction spending from a non-existent input")
         self.move_tip(69)
-        b70 = self.next_block(70, spend=out[21])
+        self.next_block(70, spend=out[21])
         bogus_tx = CTransaction()
         bogus_tx.sha256 = uint256_from_str(b"23c70ed7c0506e9178fc1a987f40a33946d4ad4c962b5ae3a52546da53af0c5c")
         tx = CTransaction()
@@ -1053,7 +1053,7 @@ def run_test(self):
         # b71 is a copy of 72, but re-adds one of its transactions.  However, it has the same hash as b72.
         self.log.info("Reject a block containing a duplicate transaction but with the same Merkle root (Merkle tree malleability")
         self.move_tip(69)
-        b72 = self.next_block(72)
+        self.next_block(72)
         tx1 = self.create_and_sign_transaction(out[21], 2)
         tx2 = self.create_and_sign_transaction(tx1, 1)
         b72 = self.update_block(72, [tx1, tx2])  # now tip is 72
@@ -1091,7 +1091,7 @@ def run_test(self):
         #       bytearray[20,526]       : OP_CHECKSIG (this puts us over the limit)
         self.log.info("Reject a block containing too many sigops after a large script element")
         self.move_tip(72)
-        b73 = self.next_block(73)
+        self.next_block(73)
         size = MAX_BLOCK_SIGOPS - 1 + MAX_SCRIPT_ELEMENT_SIZE + 1 + 5 + 1
         a = bytearray([OP_CHECKSIG] * size)
         a[MAX_BLOCK_SIGOPS - 1] = int("4e", 16)  # OP_PUSHDATA4
@@ -1119,7 +1119,7 @@ def run_test(self):
         #       b75 succeeds because we put MAX_BLOCK_SIGOPS before the element
         self.log.info("Check sigops are counted correctly after an invalid script element")
         self.move_tip(72)
-        b74 = self.next_block(74)
+        self.next_block(74)
         size = MAX_BLOCK_SIGOPS - 1 + MAX_SCRIPT_ELEMENT_SIZE + 42  # total = 20,561
         a = bytearray([OP_CHECKSIG] * size)
         a[MAX_BLOCK_SIGOPS] = 0x4e
@@ -1132,7 +1132,7 @@ def run_test(self):
         self.send_blocks([b74], success=False, reject_reason='bad-blk-sigops', reconnect=True)
 
         self.move_tip(72)
-        b75 = self.next_block(75)
+        self.next_block(75)
         size = MAX_BLOCK_SIGOPS - 1 + MAX_SCRIPT_ELEMENT_SIZE + 42
         a = bytearray([OP_CHECKSIG] * size)
         a[MAX_BLOCK_SIGOPS - 1] = 0x4e
@@ -1147,7 +1147,7 @@ def run_test(self):
 
         # Check that if we push an element filled with CHECKSIGs, they are not counted
         self.move_tip(75)
-        b76 = self.next_block(76)
+        self.next_block(76)
         size = MAX_BLOCK_SIGOPS - 1 + MAX_SCRIPT_ELEMENT_SIZE + 1 + 5
         a = bytearray([OP_CHECKSIG] * size)
         a[MAX_BLOCK_SIGOPS - 1] = 0x4e  # PUSHDATA4, but leave the following bytes as just checksigs
@@ -1175,18 +1175,18 @@ def run_test(self):
         #    updated.  (Perhaps to spend to a P2SH OP_TRUE script)
         self.log.info("Test transaction resurrection during a re-org")
         self.move_tip(76)
-        b77 = self.next_block(77)
+        self.next_block(77)
         tx77 = self.create_and_sign_transaction(out[24], 10 * COIN)
         b77 = self.update_block(77, [tx77])
         self.send_blocks([b77], True)
         self.save_spendable_output()
 
-        b78 = self.next_block(78)
+        self.next_block(78)
         tx78 = self.create_tx(tx77, 0, 9 * COIN)
         b78 = self.update_block(78, [tx78])
         self.send_blocks([b78], True)
 
-        b79 = self.next_block(79)
+        self.next_block(79)
         tx79 = self.create_tx(tx78, 0, 8 * COIN)
         b79 = self.update_block(79, [tx79])
         self.send_blocks([b79], True)
@@ -1218,7 +1218,7 @@ def run_test(self):
         #  -> b81 (26) -> b82 (27) -> b83 (28)
         #
         self.log.info("Accept a block with invalid opcodes in dead execution paths")
-        b83 = self.next_block(83)
+        self.next_block(83)
         op_codes = [OP_IF, OP_INVALIDOPCODE, OP_ELSE, OP_TRUE, OP_ENDIF]
         script = CScript(op_codes)
         tx1 = self.create_and_sign_transaction(out[28], out[28].vout[0].nValue, script)
@@ -1237,7 +1237,7 @@ def run_test(self):
         #                                    \-> b85 (29) -> b86 (30)            \-> b89a (32)
         #
         self.log.info("Test re-orging blocks with OP_RETURN in them")
-        b84 = self.next_block(84)
+        self.next_block(84)
         tx1 = self.create_tx(out[29], 0, 0, CScript([OP_RETURN]))
         tx1.vout.append(CTxOut(0, CScript([OP_TRUE])))
         tx1.vout.append(CTxOut(0, CScript([OP_TRUE])))
@@ -1275,7 +1275,7 @@ def run_test(self):
         self.save_spendable_output()
 
         # trying to spend the OP_RETURN output is rejected
-        b89a = self.next_block("89a", spend=out[32])
+        self.next_block("89a", spend=out[32])
         tx = self.create_tx(tx1, 0, 0, CScript([OP_TRUE]))
         b89a = self.update_block("89a", [tx])
         self.send_blocks([b89a], success=False, reject_reason='bad-txns-inputs-missingorspent', reconnect=True)
diff --git a/test/functional/feature_index_prune.py b/test/functional/feature_index_prune.py
index a699990c45e2..1f39a0dd327b 100755
--- a/test/functional/feature_index_prune.py
+++ b/test/functional/feature_index_prune.py
@@ -78,7 +78,7 @@ def run_test(self):
                 pruneheight_new = node.pruneblockchain(400)
                 # the prune heights used here and below are magic numbers that are determined by the
                 # thresholds at which block files wrap, so they depend on disk serialization and default block file size.
-                assert_equal(pruneheight_new, 368)
+                assert_equal(pruneheight_new, 367)
 
         self.log.info("check if we can access the tips blockfilter and coinstats when we have pruned some blocks")
         tip = self.nodes[0].getbestblockhash()
@@ -113,7 +113,7 @@ def run_test(self):
         self.log.info("prune exactly up to the indices best blocks while the indices are disabled")
         for i in range(3):
             pruneheight_2 = self.nodes[i].pruneblockchain(1000)
-            assert_equal(pruneheight_2, 736)
+            assert_equal(pruneheight_2, 735)
             # Restart the nodes again with the indices activated
             self.restart_node(i, extra_args=self.extra_args[i], expected_stderr=EXPECTED_STDERR_NO_GOV_PRUNE)
 
@@ -148,7 +148,7 @@ def run_test(self):
         for node in self.nodes[:2]:
             with node.assert_debug_log(['limited pruning to height 2489']):
                 pruneheight_new = node.pruneblockchain(2500)
-                assert_equal(pruneheight_new, 2208)
+                assert_equal(pruneheight_new, 2207)
 
         self.log.info("ensure that prune locks don't prevent indices from failing in a reorg scenario")
         with self.nodes[0].assert_debug_log(['basic block filter index prune lock moved back to 2480']):
diff --git a/test/functional/feature_pruning.py b/test/functional/feature_pruning.py
index 8c6e1e312437..a58ea8c514d9 100755
--- a/test/functional/feature_pruning.py
+++ b/test/functional/feature_pruning.py
@@ -313,7 +313,7 @@ def height(index):
 
         def prune(index):
             ret = node.pruneblockchain(height=height(index))
-            assert_equal(ret, node.getblockchaininfo()['pruneheight'])
+            assert_equal(ret + 1, node.getblockchaininfo()['pruneheight'])
 
         def has_block(index):
             return os.path.isfile(os.path.join(self.nodes[node_number].datadir, self.chain, "blocks", f"blk{index:05}.dat"))
diff --git a/test/functional/feature_reindex_readonly.py b/test/functional/feature_reindex_readonly.py
new file mode 100755
index 000000000000..e4b29c3d3c1f
--- /dev/null
+++ b/test/functional/feature_reindex_readonly.py
@@ -0,0 +1,65 @@
+#!/usr/bin/env python3
+# Copyright (c) 2023-present The Bitcoin Core developers
+# Copyright (c) 2024 The Dash Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+"""Test running bitcoind with -reindex from a read-only blockstore
+- Start a node, generate blocks, then restart with -reindex after setting blk files to read-only
+"""
+
+import platform
+import stat
+import subprocess
+from test_framework.test_framework import BitcoinTestFramework
+
+
+class BlockstoreReindexTest(BitcoinTestFramework):
+    def set_test_params(self):
+        self.setup_clean_chain = True
+        self.num_nodes = 1
+        self.extra_args = [["-fastprune"]]
+
+    def reindex_readonly(self):
+        self.log.debug("Generate block big enough to start second block file")
+        fastprune_blockfile_size = 0x10000
+        opreturn = "6a"
+        nulldata = fastprune_blockfile_size * "ff"
+        self.generateblock(self.nodes[0], output=f"raw({opreturn}{nulldata})", transactions=[])
+        self.stop_node(0)
+
+        assert (self.nodes[0].chain_path / "blocks" / "blk00000.dat").exists()
+        assert (self.nodes[0].chain_path / "blocks" / "blk00001.dat").exists()
+
+        self.log.debug("Make the first block file read-only")
+        filename = self.nodes[0].chain_path / "blocks" / "blk00000.dat"
+        filename.chmod(stat.S_IREAD)
+
+        used_chattr = False
+        if platform.system() == "Linux":
+            try:
+                subprocess.run(['chattr', '+i', filename], capture_output=True, check=True)
+                used_chattr = True
+                self.log.info("Made file immutable with chattr")
+            except subprocess.CalledProcessError as e:
+                self.log.warning(str(e))
+                if e.stdout:
+                    self.log.warning(f"stdout: {e.stdout}")
+                if e.stderr:
+                    self.log.warning(f"stderr: {e.stderr}")
+
+        self.log.debug("Attempt to restart and reindex the node with the unwritable block file")
+        with self.nodes[0].assert_debug_log(expected_msgs=['FlushStateToDisk', 'failed to open file'], unexpected_msgs=[]):
+            self.nodes[0].assert_start_raises_init_error(extra_args=['-reindex', '-fastprune'],
+                                                        expected_msg="Error: A fatal internal error occurred, see debug.log for details")
+
+        if used_chattr:
+            subprocess.check_call(['chattr', '-i', filename])
+
+        filename.chmod(0o777)
+
+    def run_test(self):
+        self.reindex_readonly()
+
+
+if __name__ == '__main__':
+    BlockstoreReindexTest().main()
diff --git a/test/functional/interface_rest.py b/test/functional/interface_rest.py
index 2e36aa217484..5f63718c4bba 100755
--- a/test/functional/interface_rest.py
+++ b/test/functional/interface_rest.py
@@ -10,6 +10,7 @@
 from io import BytesIO
 import json
 from struct import pack, unpack
+import typing
 import urllib.parse
 
 
@@ -57,14 +58,21 @@ def set_test_params(self):
             args.append("-whitelist=noban@127.0.0.1")
         self.supports_cli = False
 
-    def test_rest_request(self, uri, http_method='GET', req_type=ReqType.JSON, body='', status=200, ret_type=RetType.JSON):
+    def test_rest_request(
+            self,
+            uri: str,
+            http_method: str = 'GET',
+            req_type: ReqType = ReqType.JSON,
+            body: str = '',
+            status: int = 200,
+            ret_type: RetType = RetType.JSON,
+            query_params: typing.Dict[str, typing.Any] = None,
+            ) -> typing.Union[http.client.HTTPResponse, bytes, str, None]:
         rest_uri = '/rest' + uri
-        if req_type == ReqType.JSON:
-            rest_uri += '.json'
-        elif req_type == ReqType.BIN:
-            rest_uri += '.bin'
-        elif req_type == ReqType.HEX:
-            rest_uri += '.hex'
+        if req_type in ReqType:
+            rest_uri += f'.{req_type.name.lower()}'
+        if query_params:
+            rest_uri += f'?{urllib.parse.urlencode(query_params)}'
 
         conn = http.client.HTTPConnection(self.url.hostname, self.url.port)
         self.log.debug(f'{http_method} {rest_uri} {body}')
@@ -83,6 +91,8 @@ def test_rest_request(self, uri, http_method='GET', req_type=ReqType.JSON, body=
         elif ret_type == RetType.JSON:
             return json.loads(resp.read().decode('utf-8'), parse_float=Decimal)
 
+        return None
+
     def run_test(self):
         self.url = urllib.parse.urlparse(self.nodes[0].url)
         self.wallet = MiniWallet(self.nodes[0])
@@ -209,16 +219,16 @@ def run_test(self):
 
         self.generate(self.nodes[0], 1)  # generate block to not affect upcoming tests
 
-        self.log.info("Test the /block, /blockhashbyheight and /headers URIs")
+        self.log.info("Test the /block, /blockhashbyheight, /headers, and /blockfilterheaders URIs")
         bb_hash = self.nodes[0].getbestblockhash()
 
         # Check result if block does not exists
-        assert_equal(self.test_rest_request(f"/headers/1/{UNKNOWN_PARAM}"), [])
+        assert_equal(self.test_rest_request(f"/headers/{UNKNOWN_PARAM}", query_params={"count": 1}), [])
         self.test_rest_request(f"/block/{UNKNOWN_PARAM}", status=404, ret_type=RetType.OBJ)
 
         # Check result if block is not in the active chain
         self.nodes[0].invalidateblock(bb_hash)
-        assert_equal(self.test_rest_request(f'/headers/1/{bb_hash}'), [])
+        assert_equal(self.test_rest_request(f'/headers/{bb_hash}', query_params={'count': 1}), [])
         self.test_rest_request(f'/block/{bb_hash}')
         self.nodes[0].reconsiderblock(bb_hash)
 
@@ -228,7 +238,7 @@ def run_test(self):
         response_bytes = response.read()
 
         # Compare with block header
-        response_header = self.test_rest_request(f"/headers/1/{bb_hash}", req_type=ReqType.BIN, ret_type=RetType.OBJ)
+        response_header = self.test_rest_request(f"/headers/{bb_hash}", req_type=ReqType.BIN, ret_type=RetType.OBJ, query_params={"count": 1})
         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)
@@ -240,7 +250,7 @@ def run_test(self):
         assert_equal(response_bytes.hex().encode(), response_hex_bytes)
 
         # Compare with hex block header
-        response_header_hex = self.test_rest_request(f"/headers/1/{bb_hash}", req_type=ReqType.HEX, ret_type=RetType.OBJ)
+        response_header_hex = self.test_rest_request(f"/headers/{bb_hash}", req_type=ReqType.HEX, ret_type=RetType.OBJ, query_params={"count": 1})
         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(response_bytes[:BLOCK_HEADER_SIZE].hex().encode(), response_header_hex_bytes)
@@ -267,7 +277,7 @@ def run_test(self):
         self.test_rest_request("/blockhashbyheight/", ret_type=RetType.OBJ, status=400)
 
         # Compare with json block header
-        json_obj = self.test_rest_request(f"/headers/1/{bb_hash}")
+        json_obj = self.test_rest_request(f"/headers/{bb_hash}", query_params={"count": 1})
         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
 
@@ -278,9 +288,14 @@ def run_test(self):
 
         # See if we can get 5 headers in one response
         self.generate(self.nodes[1], 5)
-        json_obj = self.test_rest_request(f"/headers/5/{bb_hash}")
+        expected_filter = {
+            'txindex': {'synced': True, 'best_block_height': 208},
+            'basic block filter index': {'synced': True, 'best_block_height': 208},
+        }
+        self.wait_until(lambda: self.nodes[0].getindexinfo() == expected_filter)
+        json_obj = self.test_rest_request(f"/headers/{bb_hash}", query_params={"count": 5})
         assert_equal(len(json_obj), 5)  # now we should have 5 header objects
-        json_obj = self.test_rest_request(f"/blockfilterheaders/basic/5/{bb_hash}")
+        json_obj = self.test_rest_request(f"/blockfilterheaders/basic/{bb_hash}", query_params={"count": 5})
         first_filter_header = json_obj[0]
         assert_equal(len(json_obj), 5)  # now we should have 5 filter header objects
         json_obj = self.test_rest_request(f"/blockfilter/basic/{bb_hash}")
@@ -290,11 +305,17 @@ def run_test(self):
         assert_equal(first_filter_header, rpc_blockfilter['header'])
         assert_equal(json_obj['filter'], rpc_blockfilter['filter'])
 
+        # Test blockfilterheaders with an invalid hash and filtertype
+        resp = self.test_rest_request(f"/blockfilterheaders/{INVALID_PARAM}/{bb_hash}", ret_type=RetType.OBJ, status=400)
+        assert_equal(resp.read().decode('utf-8').rstrip(), f"Unknown filtertype {INVALID_PARAM}")
+        resp = self.test_rest_request(f"/blockfilterheaders/basic/{INVALID_PARAM}", ret_type=RetType.OBJ, status=400)
+        assert_equal(resp.read().decode('utf-8').rstrip(), f"Invalid hash: {INVALID_PARAM}")
+
         # Test number parsing
         for num in ['5a', '-5', '0', '2001', '99999999999999999999999999999999999']:
             assert_equal(
                 bytes(f'Header count is invalid or out of acceptable range (1-2000): {num}\r\n', 'ascii'),
-                self.test_rest_request(f"/headers/{num}/{bb_hash}", ret_type=RetType.BYTES, status=400),
+                self.test_rest_request(f"/headers/{bb_hash}", ret_type=RetType.BYTES, status=400, query_params={"count": num}),
             )
 
         self.log.info("Test tx inclusion in the /mempool and /block URIs")
@@ -362,6 +383,11 @@ def run_test(self):
         blockchain_info = self.nodes[0].getblockchaininfo()
         assert_equal(blockchain_info, json_obj)
 
+        # Test compatibility of deprecated and newer endpoints
+        self.log.info("Test compatibility of deprecated and newer endpoints")
+        assert_equal(self.test_rest_request(f"/headers/{bb_hash}", query_params={"count": 1}), self.test_rest_request(f"/headers/1/{bb_hash}"))
+        assert_equal(self.test_rest_request(f"/blockfilterheaders/basic/{bb_hash}", query_params={"count": 1}), self.test_rest_request(f"/blockfilterheaders/basic/5/{bb_hash}"))
+
 
 if __name__ == '__main__':
     RESTTest().main()
diff --git a/test/functional/mempool_datacarrier.py b/test/functional/mempool_datacarrier.py
new file mode 100755
index 000000000000..13df564a378f
--- /dev/null
+++ b/test/functional/mempool_datacarrier.py
@@ -0,0 +1,71 @@
+#!/usr/bin/env python3
+# Copyright (c) 2020-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 datacarrier functionality"""
+from test_framework.messages import (
+    CTxOut,
+    MAX_OP_RETURN_RELAY,
+)
+from test_framework.script import (
+    CScript,
+    OP_RETURN,
+)
+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.wallet import MiniWallet
+
+
+class DataCarrierTest(BitcoinTestFramework):
+    def set_test_params(self):
+        self.num_nodes = 3
+        self.extra_args = [
+            [],
+            ["-datacarrier=0"],
+            ["-datacarrier=1", f"-datacarriersize={MAX_OP_RETURN_RELAY - 1}"]
+        ]
+
+    def test_null_data_transaction(self, node: TestNode, data: bytes, success: bool) -> None:
+        tx = self.wallet.create_self_transfer(fee_rate=0)["tx"]
+        tx.vout.append(CTxOut(nValue=0, scriptPubKey=CScript([OP_RETURN, data])))
+        tx.vout[0].nValue -= tx.get_vsize()  # simply pay 1sat/vbyte fee
+
+        tx_hex = tx.serialize().hex()
+
+        if success:
+            self.wallet.sendrawtransaction(from_node=node, tx_hex=tx_hex)
+            assert tx.rehash() in node.getrawmempool(True), f'{tx_hex} not in mempool'
+        else:
+            assert_raises_rpc_error(-26, "scriptpubkey", self.wallet.sendrawtransaction, from_node=node, tx_hex=tx_hex)
+
+    def run_test(self):
+        self.wallet = MiniWallet(self.nodes[0])
+        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)
+
+        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)
+
+        self.log.info("Testing a null data transaction larger than allowed by the default -datacarriersize value.")
+        self.test_null_data_transaction(node=self.nodes[0], data=too_long_data, success=False)
+
+        self.log.info("Testing a null data transaction with -datacarrier=false.")
+        self.test_null_data_transaction(node=self.nodes[1], data=default_size_data, success=False)
+
+        self.log.info("Testing a null data transaction with a size larger than accepted by -datacarriersize.")
+        self.test_null_data_transaction(node=self.nodes[2], data=default_size_data, success=False)
+
+        self.log.info("Testing a null data transaction with a size smaller than accepted by -datacarriersize.")
+        self.test_null_data_transaction(node=self.nodes[2], data=small_data, success=True)
+
+
+if __name__ == '__main__':
+    DataCarrierTest().main()
diff --git a/test/functional/mempool_package_onemore.py b/test/functional/mempool_package_onemore.py
index 15ebf39a15ce..9abb1c6dcf12 100755
--- a/test/functional/mempool_package_onemore.py
+++ b/test/functional/mempool_package_onemore.py
@@ -7,6 +7,9 @@
    size.
 """
 
+from test_framework.messages import (
+    DEFAULT_ANCESTOR_LIMIT,
+)
 from test_framework.test_framework import BitcoinTestFramework
 from test_framework.util import (
     assert_equal,
@@ -15,10 +18,6 @@
 from test_framework.wallet import MiniWallet
 
 
-MAX_ANCESTORS = 25
-MAX_DESCENDANTS = 25
-
-
 class MempoolPackagesTest(BitcoinTestFramework):
     def set_test_params(self):
         self.num_nodes = 1
@@ -34,19 +33,19 @@ def run_test(self):
         self.wallet = MiniWallet(self.nodes[0])
         self.wallet.rescan_utxos()
 
-        # MAX_ANCESTORS transactions off a confirmed tx should be fine
+        # DEFAULT_ANCESTOR_LIMIT transactions off a confirmed tx should be fine
         chain = []
         utxo = self.wallet.get_utxo()
         for _ in range(4):
             utxo, utxo2 = self.chain_tx([utxo], num_outputs=2)
             chain.append(utxo2)
-        for _ in range(MAX_ANCESTORS - 4):
+        for _ in range(DEFAULT_ANCESTOR_LIMIT - 4):
             utxo, = self.chain_tx([utxo])
             chain.append(utxo)
         second_chain, = self.chain_tx([self.wallet.get_utxo()])
 
-        # Check mempool has MAX_ANCESTORS + 1 transactions in it
-        assert_equal(len(self.nodes[0].getrawmempool()), MAX_ANCESTORS + 1)
+        # Check mempool has DEFAULT_ANCESTOR_LIMIT + 1 transactions in it
+        assert_equal(len(self.nodes[0].getrawmempool()), DEFAULT_ANCESTOR_LIMIT + 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]", self.chain_tx, [utxo])
@@ -63,7 +62,7 @@ def run_test(self):
         self.chain_tx([second_chain])
 
         # Finally, check that we added two transactions
-        assert_equal(len(self.nodes[0].getrawmempool()), MAX_ANCESTORS + 3)
+        assert_equal(len(self.nodes[0].getrawmempool()), DEFAULT_ANCESTOR_LIMIT + 3)
 
 
 if __name__ == '__main__':
diff --git a/test/functional/mempool_packages.py b/test/functional/mempool_packages.py
index cbb91db21be5..0a9d227aadeb 100755
--- a/test/functional/mempool_packages.py
+++ b/test/functional/mempool_packages.py
@@ -7,7 +7,11 @@
 from decimal import Decimal
 
 from test_framework.blocktools import COINBASE_MATURITY
-from test_framework.messages import COIN
+from test_framework.messages import (
+    COIN,
+    DEFAULT_ANCESTOR_LIMIT,
+    DEFAULT_DESCENDANT_LIMIT,
+)
 from test_framework.p2p import P2PTxInvStore
 from test_framework.test_framework import BitcoinTestFramework
 from test_framework.util import (
@@ -16,13 +20,12 @@
     chain_transaction,
 )
 
-# default limits
-MAX_ANCESTORS = 25
-MAX_DESCENDANTS = 25
+
 # custom limits for node1
-MAX_ANCESTORS_CUSTOM = 5
-MAX_DESCENDANTS_CUSTOM = 10
-assert MAX_DESCENDANTS_CUSTOM >= MAX_ANCESTORS_CUSTOM
+CUSTOM_ANCESTOR_LIMIT = 5
+CUSTOM_DESCENDANT_LIMIT = 10
+assert CUSTOM_DESCENDANT_LIMIT >= CUSTOM_ANCESTOR_LIMIT
+
 
 class MempoolPackagesTest(BitcoinTestFramework):
     def set_test_params(self):
@@ -34,8 +37,8 @@ def set_test_params(self):
             ],
             [
                 "-maxorphantxsize=1000",
-                "-limitancestorcount={}".format(MAX_ANCESTORS_CUSTOM),
-                "-limitdescendantcount={}".format(MAX_DESCENDANTS_CUSTOM),
+                "-limitancestorcount={}".format(CUSTOM_ANCESTOR_LIMIT),
+                "-limitdescendantcount={}".format(CUSTOM_DESCENDANT_LIMIT),
             ],
         ]
 
@@ -55,11 +58,11 @@ def run_test(self):
         assert 'ancestorfees' not in utxo[0]
 
         fee = Decimal("0.0001")
-        # MAX_ANCESTORS transactions off a confirmed tx should be fine
+        # DEFAULT_ANCESTOR_LIMIT transactions off a confirmed tx should be fine
         chain = []
         ancestor_vsize = 0
         ancestor_fees = Decimal(0)
-        for i in range(MAX_ANCESTORS):
+        for i in range(DEFAULT_ANCESTOR_LIMIT):
             (txid, sent_value) = chain_transaction(self.nodes[0], [txid], [0], value, fee, 1)
             value = sent_value
             chain.append(txid)
@@ -77,16 +80,16 @@ def run_test(self):
         # Otherwise, getrawmempool may be inconsistent with getmempoolentry if unbroadcast changes in between
         peer_inv_store.wait_for_broadcast(chain)
 
-        # Check mempool has MAX_ANCESTORS transactions in it, and descendant and ancestor
+        # Check mempool has DEFAULT_ANCESTOR_LIMIT transactions in it, and descendant and ancestor
         # count and fees should look correct
         mempool = self.nodes[0].getrawmempool(True)
-        assert_equal(len(mempool), MAX_ANCESTORS)
+        assert_equal(len(mempool), DEFAULT_ANCESTOR_LIMIT)
         descendant_count = 1
         descendant_fees = 0
         descendant_vsize = 0
 
         assert_equal(ancestor_vsize, sum([mempool[tx]['vsize'] for tx in mempool]))
-        ancestor_count = MAX_ANCESTORS
+        ancestor_count = DEFAULT_ANCESTOR_LIMIT
         assert_equal(ancestor_fees, sum([mempool[tx]['fees']['base'] for tx in mempool]))
 
         descendants = []
@@ -203,9 +206,9 @@ def run_test(self):
         # Check that node1's mempool is as expected (-> custom ancestor limit)
         mempool0 = self.nodes[0].getrawmempool(False)
         mempool1 = self.nodes[1].getrawmempool(False)
-        assert_equal(len(mempool1), MAX_ANCESTORS_CUSTOM)
+        assert_equal(len(mempool1), CUSTOM_ANCESTOR_LIMIT)
         assert set(mempool1).issubset(set(mempool0))
-        for tx in chain[:MAX_ANCESTORS_CUSTOM]:
+        for tx in chain[:CUSTOM_ANCESTOR_LIMIT]:
             assert tx in mempool1
             entry0 = self.nodes[0].getmempoolentry(tx)
             entry1 = self.nodes[1].getmempoolentry(tx)
@@ -230,7 +233,7 @@ def run_test(self):
 
         # Sign and send up to MAX_DESCENDANT transactions chained off the parent tx
         chain = [] # save sent txs for the purpose of checking node1's mempool later (see below)
-        for _ in range(MAX_DESCENDANTS - 1):
+        for _ in range(DEFAULT_DESCENDANT_LIMIT - 1):
             utxo = transaction_package.pop(0)
             (txid, sent_value) = chain_transaction(self.nodes[0], [utxo['txid']], [utxo['vout']], utxo['amount'], fee, 10)
             chain.append(txid)
@@ -240,7 +243,7 @@ def run_test(self):
                 transaction_package.append({'txid': txid, 'vout': j, 'amount': sent_value})
 
         mempool = self.nodes[0].getrawmempool(True)
-        assert_equal(mempool[parent_transaction]['descendantcount'], MAX_DESCENDANTS)
+        assert_equal(mempool[parent_transaction]['descendantcount'], DEFAULT_DESCENDANT_LIMIT)
         assert_equal(sorted(mempool[parent_transaction]['spentby']), sorted(tx_children))
 
         for child in tx_children:
@@ -255,14 +258,14 @@ def run_test(self):
         # - parent tx for descendant test
         # - txs chained off parent tx (-> custom descendant limit)
         self.wait_until(lambda: len(self.nodes[1].getrawmempool()) ==
-                                MAX_ANCESTORS_CUSTOM + 1 + MAX_DESCENDANTS_CUSTOM, timeout=10)
+                                CUSTOM_ANCESTOR_LIMIT + 1 + CUSTOM_DESCENDANT_LIMIT, timeout=10)
         mempool0 = self.nodes[0].getrawmempool(False)
         mempool1 = self.nodes[1].getrawmempool(False)
         assert set(mempool1).issubset(set(mempool0))
         assert parent_transaction in mempool1
-        for tx in chain[:MAX_DESCENDANTS_CUSTOM]:
+        for tx in chain[:CUSTOM_DESCENDANT_LIMIT]:
             assert tx in mempool1
-        for tx in chain[MAX_DESCENDANTS_CUSTOM:]:
+        for tx in chain[CUSTOM_DESCENDANT_LIMIT:]:
             assert tx not in mempool1
         for tx in mempool1:
             entry0 = self.nodes[0].getmempoolentry(tx)
diff --git a/test/functional/mining_basic.py b/test/functional/mining_basic.py
index daf634b16125..a9720eea30c3 100755
--- a/test/functional/mining_basic.py
+++ b/test/functional/mining_basic.py
@@ -179,7 +179,7 @@ def assert_submitblock(block, result_str_1, result_str_2=None):
 
         self.log.info("getblocktemplate: Test bad timestamps")
         bad_block = copy.deepcopy(block)
-        bad_block.nTime = 2**31 - 1
+        bad_block.nTime = 2**32 - 1
         assert_template(node, bad_block, 'time-too-new')
         assert_submitblock(bad_block, 'time-too-new', 'time-too-new')
         bad_block.nTime = 0
diff --git a/test/functional/p2p_disconnect_ban.py b/test/functional/p2p_disconnect_ban.py
index 35c47a12c06a..9ffdcb63bba1 100755
--- a/test/functional/p2p_disconnect_ban.py
+++ b/test/functional/p2p_disconnect_ban.py
@@ -3,6 +3,7 @@
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or http://www.opensource.org/licenses/mit-license.php.
 """Test node disconnect and ban behavior"""
+from pathlib import Path
 
 from test_framework.test_framework import BitcoinTestFramework
 from test_framework.util import (
@@ -35,6 +36,17 @@ def run_test(self):
         self.log.info("clearbanned: successfully clear ban list")
         self.nodes[1].clearbanned()
         assert_equal(len(self.nodes[1].listbanned()), 0)
+
+        self.log.info('Test banlist database recreation')
+        self.stop_node(1)
+        target_file = self.nodes[1].chain_path / "banlist.json"
+        Path.unlink(target_file)
+        with self.nodes[1].assert_debug_log(["Recreating the banlist database"]):
+            self.start_node(1)
+
+        assert Path.exists(target_file)
+        assert_equal(self.nodes[1].listbanned(), [])
+
         self.nodes[1].setban("127.0.0.0/24", "add")
 
         self.log.info("setban: fail to ban an already banned subnet")
diff --git a/test/functional/rpc_blockchain.py b/test/functional/rpc_blockchain.py
index b933d359f58d..d18c366d0476 100755
--- a/test/functional/rpc_blockchain.py
+++ b/test/functional/rpc_blockchain.py
@@ -92,6 +92,7 @@ def run_test(self):
         self._test_stopatheight()
         self._test_waitforblockheight()
         self._test_getblock()
+        self._test_y2106()
         assert self.nodes[0].verifychain(4, 0)
 
     def mine_chain(self):
@@ -247,6 +248,14 @@ def _test_getblockchaininfo(self):
                 'active': False},
         })
 
+    def _test_y2106(self):
+        self.log.info("Check that block timestamps work until year 2106")
+        self.generate(self.nodes[0], 8)[-1]
+        time_2106 = 2**32 - 1
+        self.nodes[0].setmocktime(time_2106)
+        last = self.generate(self.nodes[0], 6)[-1]
+        assert_equal(self.nodes[0].getblockheader(last)["mediantime"], time_2106)
+
     def _test_getchaintxstats(self):
         self.log.info("Test getchaintxstats")
 
diff --git a/test/functional/rpc_dumptxoutset.py b/test/functional/rpc_dumptxoutset.py
index a4651e1f0545..6214768034a8 100755
--- a/test/functional/rpc_dumptxoutset.py
+++ b/test/functional/rpc_dumptxoutset.py
@@ -49,9 +49,13 @@ def run_test(self):
             out['txoutset_hash'], 'b2d7429106c96f5ab831843d5c96ba131ca8793111d0a0e30e7d7d8b4841e6cc')
         assert_equal(out['nchaintx'], 101)
 
-        # Specifying a path to an existing file will fail.
+        # Specifying a path to an existing or invalid file will fail.
         assert_raises_rpc_error(
             -8, '{} already exists'.format(FILENAME),  node.dumptxoutset, FILENAME)
+        invalid_path = str(Path(node.datadir) / "invalid" / "path")
+        assert_raises_rpc_error(
+            -8, "Couldn't open file {}.incomplete for writing".format(invalid_path), node.dumptxoutset, invalid_path)
+
 
 if __name__ == '__main__':
     DumptxoutsetTest().main()
diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py
index 747f47c6e2de..7077764a3733 100755
--- a/test/functional/rpc_psbt.py
+++ b/test/functional/rpc_psbt.py
@@ -501,5 +501,8 @@ def test_psbt_input_keys(psbt_input, keys):
         assert signed['complete']
         self.nodes[0].finalizepsbt(signed['psbt'])
 
+        self.log.info("Test we don't crash when making a 0-value funded transaction at 0 fee without forcing an input selection")
+        assert_raises_rpc_error(-4, "Transaction requires one destination of non-0 value, a non-0 feerate, or a pre-selected input", self.nodes[0].walletcreatefundedpsbt, [], [{"data": "deadbeef"}], 0, {"fee_rate": "0"})
+
 if __name__ == '__main__':
     PSBTTest().main()
diff --git a/test/functional/test_framework/address.py b/test/functional/test_framework/address.py
index 31813e6cd739..1b217b04f4ed 100644
--- a/test/functional/test_framework/address.py
+++ b/test/functional/test_framework/address.py
@@ -12,7 +12,6 @@
 import unittest
 
 from .script import hash160, hash256, CScript
-from .util import assert_equal
 
 # Note unlike in bitcoin, this address isn't bech32 since we don't (at this time) support bech32.
 ADDRESS_BCRT1_UNSPENDABLE = 'yVg3NBUHNEhgDceqwVUjsZHreC5PBHnUo9'
@@ -20,7 +19,7 @@
 ADDRESS_BCRT1_P2SH_OP_TRUE = '8zJctvfrzGZ5s1zQ3kagwyW1DsPYSQ4V2P'
 
 
-chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
+b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
 
 
 def byte_to_base58(b, version):
@@ -29,10 +28,10 @@ def byte_to_base58(b, version):
     b += hash256(b)[:4]       # append checksum
     value = int.from_bytes(b, 'big')
     while value > 0:
-        result = chars[value % 58] + result
+        result = b58chars[value % 58] + result
         value //= 58
     while b[0] == 0:
-        result = chars[0] + result
+        result = b58chars[0] + result
         b = b[1:]
     return result
 
@@ -46,8 +45,8 @@ def base58_to_byte(s):
     n = 0
     for c in s:
         n *= 58
-        assert c in chars
-        digit = chars.index(c)
+        assert c in b58chars
+        digit = b58chars.index(c)
         n += digit
     h = '%x' % n
     if len(h) % 2:
@@ -55,14 +54,14 @@ def base58_to_byte(s):
     res = n.to_bytes((n.bit_length() + 7) // 8, 'big')
     pad = 0
     for c in s:
-        if c == chars[0]:
+        if c == b58chars[0]:
             pad += 1
         else:
             break
     res = b'\x00' * pad + res
 
-    # Assert if the checksum is invalid
-    assert_equal(hash256(res[:-4])[:4], res[-4:])
+    if hash256(res[:-4])[:4] != res[-4:]:
+        raise ValueError('Invalid Base58Check checksum')
 
     return res[1:-4], int(res[0])
 
diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py
index 5892c7c0d1ac..d55010faeed4 100755
--- a/test/functional/test_framework/messages.py
+++ b/test/functional/test_framework/messages.py
@@ -65,6 +65,12 @@
 
 FILTER_TYPE_BASIC = 0
 
+DEFAULT_ANCESTOR_LIMIT = 25    # default max number of in-mempool ancestors
+DEFAULT_DESCENDANT_LIMIT = 25  # default max number of in-mempool descendants
+
+# Default setting for -datacarriersize. 80 bytes of data, +1 for OP_RETURN, +2 for the pushdata opcodes.
+MAX_OP_RETURN_RELAY = 83
+
 MAGIC_BYTES = {
     "mainnet": b"\xbf\x0c\x6b\xbd",   # mainnet
     "testnet3": b"\xce\xe2\xca\xff",  # testnet3
diff --git a/test/functional/test_framework/netutil.py b/test/functional/test_framework/netutil.py
index 7d6d813f15da..7669ba55f15e 100644
--- a/test/functional/test_framework/netutil.py
+++ b/test/functional/test_framework/netutil.py
@@ -25,6 +25,11 @@
 STATE_LISTEN = '0A'
 # STATE_CLOSING = '0B'
 
+# Address manager size constants as defined in addrman_impl.h
+ADDRMAN_NEW_BUCKET_COUNT = 1 << 10
+ADDRMAN_TRIED_BUCKET_COUNT = 1 << 8
+ADDRMAN_BUCKET_SIZE = 1 << 6
+
 def get_socket_inodes(pid):
     '''
     Get list of socket inodes for process pid.
diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py
index 8b325b480bd4..db886de61a68 100755
--- a/test/functional/test_framework/test_framework.py
+++ b/test/functional/test_framework/test_framework.py
@@ -148,8 +148,6 @@ def __init__(self):
             print("DEPRECATED: --timeoutscale option is no longer available, please use --timeout-factor instead")
             if self.options.timeout_factor == 1:
                 self.options.timeout_factor = self.options.timeout_scale
-        if self.options.timeout_factor == 0 :
-            self.options.timeout_factor = 99999
         self.rpc_timeout = int(self.rpc_timeout * self.options.timeout_factor) # optionally, increase timeout by a factor
 
     def main(self):
@@ -223,7 +221,7 @@ def parse_args(self):
                             help="run nodes under the valgrind memory error detector: expect at least a ~10x slowdown, valgrind 3.14 or later required. Does not apply to previous release binaries.")
         parser.add_argument("--randomseed", type=int,
                             help="set a random seed for deterministically reproducing a previous test run")
-        parser.add_argument('--timeout-factor', dest="timeout_factor", type=float, default=1.0, help='adjust test timeouts by a factor. Setting it to 0 disables all timeouts')
+        parser.add_argument("--timeout-factor", dest="timeout_factor", type=float, help="adjust test timeouts by a factor. Setting it to 0 disables all timeouts")
         parser.add_argument("--v2transport", dest="v2transport", default=False, action="store_true",
                             help="use BIP324 v2 connections between all nodes by default")
         parser.add_argument("--v1transport", dest="v1transport", default=False, action="store_true",
@@ -237,6 +235,9 @@ def parse_args(self):
 
         self.add_options(parser)
         self.options = parser.parse_args()
+        if self.options.timeout_factor == 0:
+            self.options.timeout_factor = 99999
+        self.options.timeout_factor = self.options.timeout_factor or (4 if self.options.valgrind else 1)
         self.options.previous_releases_path = previous_releases_path
 
         config = configparser.ConfigParser()
@@ -263,6 +264,23 @@ def parse_args(self):
 
         PortSeed.n = self.options.port_seed
 
+    def set_binary_paths(self):
+        """Update self.options with the paths of all binaries from environment variables or their default values"""
+
+        binaries = {
+            "dashd": ("bitcoind", "DASHD"),
+            "dash-cli": ("bitcoincli", "DASHCLI"),
+            "dash-util": ("bitcoinutil", "DASHUTIL"),
+            "dash-wallet": ("bitcoinwallet", "DASHWALLET"),
+        }
+        for binary, [attribute_name, env_variable_name] in binaries.items():
+            default_filename = os.path.join(
+                self.config["environment"]["BUILDDIR"],
+                "src",
+                binary + self.config["environment"]["EXEEXT"],
+            )
+            setattr(self.options, attribute_name, os.getenv(env_variable_name, default=default_filename))
+
     def setup(self):
         """Call this method to start up the test framework object with options set."""
 
@@ -272,18 +290,7 @@ def setup(self):
 
         config = self.config
 
-        fname_bitcoind = os.path.join(
-            config["environment"]["BUILDDIR"],
-            "src",
-            "dashd" + config["environment"]["EXEEXT"],
-        )
-        fname_bitcoincli = os.path.join(
-            config["environment"]["BUILDDIR"],
-            "src",
-            "dash-cli" + config["environment"]["EXEEXT"],
-        )
-        self.options.bitcoind = os.getenv("BITCOIND", default=fname_bitcoind)
-        self.options.bitcoincli = os.getenv("BITCOINCLI", default=fname_bitcoincli)
+        self.set_binary_paths()
 
         self.extra_args_from_options = self.options.dashd_extra_args
 
@@ -2072,13 +2079,15 @@ def check_dkg_comitments():
                         continue
                     if c["quorumHash"] != quorum_hash:
                         continue
+                    if c["quorumPublicKey"] == '0' * 96:
+                        continue
                     c_ok = True
                     break
                 if not c_ok:
                     return False
             return True
 
-        self.wait_until(check_dkg_comitments, timeout=timeout, sleep=1)
+        self.wait_until(check_dkg_comitments, timeout=timeout)
 
     def wait_for_quorum_list(self, quorum_hash, nodes, timeout=15, llmq_type_name="llmq_test"):
         def wait_func():
diff --git a/test/functional/test_framework/util.py b/test/functional/test_framework/util.py
index f85c80befb8a..18e068f4d4ef 100644
--- a/test/functional/test_framework/util.py
+++ b/test/functional/test_framework/util.py
@@ -536,6 +536,29 @@ def force_finish_mnsync(node):
     while not node.mnsync("status")['IsSynced']:
         node.mnsync("next")
 
+
+def get_mnemonic(node):
+    """
+    Return mnemonic if known from legacy HD wallets and Descriptor Wallets
+    Raises exception if there is none.
+    """
+    if not node.getwalletinfo()['descriptors']:
+        return node.dumphdinfo()["mnemonic"]
+
+    mnemonic = None
+    descriptors = node.listdescriptors(True)['descriptors']
+    for desc in descriptors:
+        if desc['desc'][:4] == 'pkh(':
+            if mnemonic is None:
+                mnemonic = desc['mnemonic']
+            else:
+                assert_equal(mnemonic, desc['mnemonic'])
+        elif desc['desc'][:6] == 'combo(':
+            assert 'mnemonic' not in desc
+        else:
+            raise AssertionError(f"Unknown descriptor type: {desc['desc']}")
+    return mnemonic
+
 # Transaction/Block functions
 #############################
 
diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py
index 66b5bb231c7d..df18db1afe10 100755
--- a/test/functional/test_runner.py
+++ b/test/functional/test_runner.py
@@ -156,10 +156,10 @@
     'wallet_abandonconflict.py --descriptors',
     'feature_csv_activation.py',
     'feature_reindex.py',
+    'feature_reindex_readonly.py',
     'feature_abortnode.py',
     # vv Tests less than 30s vv
-    'rpc_quorum.py',
-    'wallet_keypool_topup.py --legacy-wallet',
+    'rpc_quorum.py',    'wallet_keypool_topup.py --legacy-wallet',
     'wallet_keypool_topup.py --descriptors',
     'feature_fee_estimation.py',
     'interface_zmq_dash.py --legacy-wallet',
@@ -321,6 +321,7 @@
     'wallet_encryption.py --legacy-wallet',
     'wallet_encryption.py --descriptors',
     'wallet_upgradetohd.py --legacy-wallet',
+    'wallet_upgradetohd.py --descriptors',
     'feature_dersig.py',
     'feature_cltv.py',
     'feature_new_quorum_type_activation.py',
@@ -374,6 +375,7 @@
     'feature_unsupported_utxo_db.py',
     'feature_logging.py',
     'feature_anchors.py',
+    'mempool_datacarrier.py',
     'feature_coinstatsindex.py',
     'wallet_orphanedreward.py',
     'wallet_timelock.py',
diff --git a/test/functional/tool_wallet.py b/test/functional/tool_wallet.py
index 7f5b47bb6fec..585a229d4d96 100755
--- a/test/functional/tool_wallet.py
+++ b/test/functional/tool_wallet.py
@@ -28,12 +28,11 @@ def skip_test_if_missing_module(self):
         self.skip_if_no_wallet_tool()
 
     def dash_wallet_process(self, *args):
-        binary = self.config["environment"]["BUILDDIR"] + '/src/dash-wallet' + self.config["environment"]["EXEEXT"]
         default_args = ['-datadir={}'.format(self.nodes[0].datadir), '-chain=%s' % self.chain]
         if self.options.descriptors and 'create' in args:
             default_args.append('-descriptors')
 
-        return subprocess.Popen([binary] + default_args + list(args), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
+        return subprocess.Popen([self.options.bitcoinwallet] + default_args + list(args), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
 
     def assert_raises_tool_error(self, error, *args):
         p = self.dash_wallet_process(*args)
diff --git a/test/functional/wallet_abandonconflict.py b/test/functional/wallet_abandonconflict.py
index 88880a3d8b5e..c00dde7745c1 100755
--- a/test/functional/wallet_abandonconflict.py
+++ b/test/functional/wallet_abandonconflict.py
@@ -129,6 +129,13 @@ def run_test(self):
             assert_equal(tx['confirmations'], 0)
             assert_equal(tx['trusted'], False)
 
+        self.log.info("Check abandoned field is present for all transaction categories in listtransactions")
+        txAB1_listtransactions = [d for d in self.nodes[0].listtransactions() if d['txid'] == txAB1]
+        assert len(txAB1_listtransactions) > 0, "Should have transactions for abandoned txid"
+        for tx in txAB1_listtransactions:
+            assert 'abandoned' in tx, f"abandoned field should be present for category '{tx['category']}'"
+            assert_equal(tx['abandoned'], True)
+
         # Verify that even with a low min relay fee, the tx is not reaccepted from wallet on startup once abandoned
         self.restart_node(0, extra_args=["-minrelaytxfee=0.00001"])
         assert self.nodes[0].getmempoolinfo()['loaded']
diff --git a/test/functional/wallet_createwallet.py b/test/functional/wallet_createwallet.py
index 47652d9a1e1f..f8ba2cab7318 100755
--- a/test/functional/wallet_createwallet.py
+++ b/test/functional/wallet_createwallet.py
@@ -29,7 +29,7 @@ def run_test(self):
         self.log.info("Run createwallet with invalid parameters.")
         # Run createwallet with invalid parameters. This must not prevent a new wallet with the same name from being created with the correct parameters.
         assert_raises_rpc_error(-4, "Passphrase provided but private keys are disabled. A passphrase is only used to encrypt private keys, so cannot be used for wallets with private keys disabled.",
-            self.nodes[0].createwallet, wallet_name='w0',  descriptors=True, disable_private_keys=True, passphrase="passphrase")
+            self.nodes[0].createwallet, wallet_name='w0', disable_private_keys=True, passphrase="passphrase")
 
         self.nodes[0].createwallet(wallet_name='w0')
         w0 = node.get_wallet_rpc('w0')
diff --git a/test/functional/wallet_encryption.py b/test/functional/wallet_encryption.py
index 37a79640664d..fb1459b6808f 100755
--- a/test/functional/wallet_encryption.py
+++ b/test/functional/wallet_encryption.py
@@ -87,6 +87,17 @@ def run_test(self):
         self.nodes[0].walletpassphrase(passphrase2, MAX_VALUE + 1000)
         actual_time = self.nodes[0].getwalletinfo()['unlocked_until']
         assert_equal(actual_time, expected_time)
+        self.nodes[0].walletlock()
+
+        # Test passphrase with null characters
+        passphrase_with_nulls = "Phrase\0With\0Nulls"
+        self.nodes[0].walletpassphrasechange(passphrase2, passphrase_with_nulls)
+        # walletpassphrasechange should not stop at null characters
+        assert_raises_rpc_error(-14, "wallet passphrase entered was incorrect", self.nodes[0].walletpassphrase, passphrase_with_nulls.partition("\0")[0], 10)
+        self.nodes[0].walletpassphrase(passphrase_with_nulls, 10)
+        sig = self.nodes[0].signmessage(address, msg)
+        assert self.nodes[0].verifymessage(address, sig, msg)
+        self.nodes[0].walletlock()
 
 
 if __name__ == '__main__':
diff --git a/test/functional/wallet_listdescriptors.py b/test/functional/wallet_listdescriptors.py
index e9295c2bfbd0..28de6b2e313b 100755
--- a/test/functional/wallet_listdescriptors.py
+++ b/test/functional/wallet_listdescriptors.py
@@ -51,7 +51,7 @@ def run_test(self):
         assert_equal(1, len([d for d in result['descriptors'] if d['internal']]))
         for item in result['descriptors']:
             assert item['desc'] != ''
-            assert item['next'] == 0
+            assert item['next_index'] == 0
             assert item['range'] == [0, 0]
             assert item['timestamp'] is not None
 
@@ -71,7 +71,8 @@ def run_test(self):
                  'timestamp': TIME_GENESIS_BLOCK,
                  'active': False,
                  'range': [0, 0],
-                 'next': 0},
+                 'next': 0,
+                 'next_index': 0},
             ],
         }
         assert_equal(expected, wallet.listdescriptors())
@@ -85,7 +86,8 @@ def run_test(self):
                  'timestamp': TIME_GENESIS_BLOCK,
                  'active': False,
                  'range': [0, 0],
-                 'next': 0},
+                 'next': 0,
+                 'next_index': 0},
             ],
         }
         assert_equal(expected_private, wallet.listdescriptors(True))
diff --git a/test/functional/wallet_listreceivedby.py b/test/functional/wallet_listreceivedby.py
index 1fab78d9a1cf..6da3f236c89b 100755
--- a/test/functional/wallet_listreceivedby.py
+++ b/test/functional/wallet_listreceivedby.py
@@ -27,9 +27,6 @@ def skip_test_if_missing_module(self):
         self.skip_if_no_cli()
 
     def run_test(self):
-        # Generate block to get out of IBD
-        self.generate(self.nodes[0], 1)
-
         # save the number of coinbase reward addresses so far
         num_cb_reward_addresses = len(self.nodes[1].listreceivedbyaddress(minconf=0, include_empty=True, include_watchonly=True))
 
@@ -178,7 +175,7 @@ def run_test(self):
         address = self.nodes[0].getnewaddress(label)
 
         reward = Decimal("464.28571429")
-        self.generatetoaddress(self.nodes[0], 1, address, sync_fun=self.no_op)
+        self.generatetoaddress(self.nodes[0], 1, address)
         hash = self.nodes[0].getbestblockhash()
 
         self.log.info("getreceivedbyaddress returns nothing with defaults")
@@ -218,7 +215,7 @@ def run_test(self):
                             {"label": label, "amount": reward})
 
         self.log.info("Generate 100 more blocks")
-        self.generate(self.nodes[0], COINBASE_MATURITY, sync_fun=self.no_op)
+        self.generate(self.nodes[0], COINBASE_MATURITY)
 
         self.log.info("getreceivedbyaddress returns reward with defaults")
         balance = self.nodes[0].getreceivedbyaddress(address)
diff --git a/test/functional/wallet_mnemonicbits.py b/test/functional/wallet_mnemonicbits.py
index 02c70a72551e..4cc40c09ef77 100755
--- a/test/functional/wallet_mnemonicbits.py
+++ b/test/functional/wallet_mnemonicbits.py
@@ -7,6 +7,7 @@
 from test_framework.test_framework import BitcoinTestFramework
 from test_framework.util import (
     assert_equal,
+    get_mnemonic,
 )
 
 class WalletMnemonicbitsTest(BitcoinTestFramework):
@@ -17,24 +18,6 @@ def set_test_params(self):
     def skip_test_if_missing_module(self):
         self.skip_if_no_wallet()
 
-    def get_mnemonic(self, node):
-        if not self.options.descriptors:
-            return node.dumphdinfo()["mnemonic"]
-
-        mnemonic = None
-        descriptors = node.listdescriptors(True)['descriptors']
-        for desc in descriptors:
-            if desc['desc'][:4] == 'pkh(':
-                if mnemonic is None:
-                    mnemonic = desc['mnemonic']
-                else:
-                    assert_equal(mnemonic, desc['mnemonic'])
-            elif desc['desc'][:6] == 'combo(':
-                assert 'mnemonic' not in desc
-            else:
-                raise AssertionError(f"Unknown descriptor type: {desc['desc']}")
-        return mnemonic
-
     def run_test(self):
         self.log.info("Test -mnemonicbits")
 
@@ -43,7 +26,7 @@ def run_test(self):
         self.nodes[0].assert_start_raises_init_error(['-mnemonicbits=123'], "Error: Invalid '-mnemonicbits'. Allowed values: 128, 160, 192, 224, 256.")
         self.start_node(0)
 
-        mnemonic_pre = self.get_mnemonic(self.nodes[0])
+        mnemonic_pre = get_mnemonic(self.nodes[0])
 
 
         self.nodes[0].encryptwallet('pass')
@@ -88,22 +71,14 @@ def run_test(self):
         self.nodes[0].loadwallet("wallet_160")
         self.nodes[0].loadwallet("wallet_192")
         self.nodes[0].loadwallet("wallet_224")
-        if self.options.descriptors:
-            self.nodes[0].createwallet("wallet_256", False, True, "", False, True)  # blank Descriptors
-            self.nodes[0].get_wallet_rpc("wallet_256").upgradetohd()
-            assert_equal(len(self.get_mnemonic(self.nodes[0].get_wallet_rpc(self.default_wallet_name)).split()), 12)  # 12 words by default
-            assert_equal(len(self.nodes[0].get_wallet_rpc("wallet_160").listdescriptors(True)["descriptors"][0]["mnemonic"].split()), 15)              # 15 words
-            assert_equal(len(self.nodes[0].get_wallet_rpc("wallet_192").listdescriptors(True)["descriptors"][0]["mnemonic"].split()), 18)              # 18 words
-            assert_equal(len(self.nodes[0].get_wallet_rpc("wallet_224").listdescriptors(True)["descriptors"][0]["mnemonic"].split()), 21)              # 21 words
-            assert_equal(len(self.nodes[0].get_wallet_rpc("wallet_256").listdescriptors(True)["descriptors"][0]["mnemonic"].split()), 24)              # 24 words
-        else:
-            self.nodes[0].createwallet("wallet_256", False, True)  # blank HD legacy
-            self.nodes[0].get_wallet_rpc("wallet_256").upgradetohd()
-            assert_equal(len(self.nodes[0].get_wallet_rpc(self.default_wallet_name).dumphdinfo()["mnemonic"].split()), 12)  # 12 words by default
-            assert_equal(len(self.nodes[0].get_wallet_rpc("wallet_160").dumphdinfo()["mnemonic"].split()), 15)              # 15 words
-            assert_equal(len(self.nodes[0].get_wallet_rpc("wallet_192").dumphdinfo()["mnemonic"].split()), 18)              # 18 words
-            assert_equal(len(self.nodes[0].get_wallet_rpc("wallet_224").dumphdinfo()["mnemonic"].split()), 21)              # 21 words
-            assert_equal(len(self.nodes[0].get_wallet_rpc("wallet_256").dumphdinfo()["mnemonic"].split()), 24)              # 24 words
+        self.nodes[0].createwallet("wallet_256", blank=True, descriptors=self.options.descriptors)  # blank wallet
+        self.nodes[0].get_wallet_rpc("wallet_256").upgradetohd()
+
+        assert_equal(len(get_mnemonic(self.nodes[0].get_wallet_rpc(self.default_wallet_name)).split()), 12)  # 12 words by default
+        assert_equal(len(get_mnemonic(self.nodes[0].get_wallet_rpc("wallet_160")).split()), 15)              # 15 words
+        assert_equal(len(get_mnemonic(self.nodes[0].get_wallet_rpc("wallet_192")).split()), 18)              # 18 words
+        assert_equal(len(get_mnemonic(self.nodes[0].get_wallet_rpc("wallet_224")).split()), 21)              # 21 words
+        assert_equal(len(get_mnemonic(self.nodes[0].get_wallet_rpc("wallet_256")).split()), 24)              # 24 words
 
 
 if __name__ == '__main__':
diff --git a/test/functional/wallet_upgradetohd.py b/test/functional/wallet_upgradetohd.py
index 0fc1c7e4d12b..aa770716cff1 100755
--- a/test/functional/wallet_upgradetohd.py
+++ b/test/functional/wallet_upgradetohd.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright (c) 2016 The Bitcoin Core developers
+# Copyright (c) 2016-2025 The Dash Core developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or http://www.opensource.org/licenses/mit-license.php.
 """
@@ -15,6 +15,7 @@
 from test_framework.util import (
     assert_equal,
     assert_raises_rpc_error,
+    get_mnemonic,
 )
 
 
@@ -29,7 +30,8 @@ def skip_test_if_missing_module(self):
     def setup_network(self):
         self.add_nodes(self.num_nodes, self.extra_args)
         self.start_nodes()
-        self.import_deterministic_coinbase_privkeys()
+        self.nodes[0].createwallet(self.default_wallet_name, blank=True, load_on_startup=True)
+        self.nodes[0].importprivkey(privkey=self.nodes[0].get_deterministic_priv_key().key, label='coinbase', rescan=True)
 
     def recover_non_hd(self):
         self.log.info("Recover non-HD wallet to check different upgrade paths")
@@ -37,7 +39,8 @@ def recover_non_hd(self):
         self.stop_node(0)
         shutil.copyfile(os.path.join(node.datadir, "non_hd.bak"), os.path.join(node.datadir, self.chain, self.default_wallet_name, self.wallet_data_filename))
         self.start_node(0)
-        assert 'hdchainid' not in node.getwalletinfo()
+        if not self.options.descriptors:
+            assert 'hdchainid' not in node.getwalletinfo()
 
     def run_test(self):
         node = self.nodes[0]
@@ -47,9 +50,10 @@ def run_test(self):
         assert 'hdchainid' not in node.getwalletinfo()
         balance_before = node.getbalance()
         assert node.upgradetohd()
-        mnemonic = node.dumphdinfo()['mnemonic']
-        chainid = node.getwalletinfo()['hdchainid']
-        assert_equal(len(chainid), 64)
+        mnemonic = get_mnemonic(node)
+        if not self.options.descriptors:
+            chainid = node.getwalletinfo()['hdchainid']
+            assert_equal(len(chainid), 64)
         assert_equal(balance_before, node.getbalance())
 
         self.log.info("Should be spendable and should use correct paths")
@@ -82,8 +86,9 @@ def run_test(self):
 
         self.log.info("No mnemonic, no mnemonic passphrase, no wallet passphrase, should result in completely different keys")
         assert node.upgradetohd()
-        assert mnemonic != node.dumphdinfo()['mnemonic']
-        assert chainid != node.getwalletinfo()['hdchainid']
+        assert mnemonic != get_mnemonic(node)
+        if not self.options.descriptors:
+            assert chainid != node.getwalletinfo()['hdchainid']
         assert_equal(balance_non_HD, node.getbalance())
         node.keypoolrefill(5)
         node.rescanblockchain()
@@ -96,8 +101,9 @@ def run_test(self):
         self.restart_node(0, extra_args=['-keypool=10'])
         assert node.upgradetohd("", "", "", True)
         # Completely different keys, no HD coins should be recovered
-        assert mnemonic != node.dumphdinfo()['mnemonic']
-        assert chainid != node.getwalletinfo()['hdchainid']
+        assert mnemonic != get_mnemonic(node)
+        if not self.options.descriptors:
+            assert chainid != node.getwalletinfo()['hdchainid']
         assert_equal(balance_non_HD, node.getbalance())
 
         self.recover_non_hd()
@@ -105,9 +111,10 @@ def run_test(self):
         self.log.info("Same mnemonic, another mnemonic passphrase, no wallet passphrase, should result in a different set of keys")
         new_mnemonic_passphrase = "somewords"
         assert node.upgradetohd(mnemonic, new_mnemonic_passphrase)
-        assert_equal(mnemonic, node.dumphdinfo()['mnemonic'])
-        new_chainid = node.getwalletinfo()['hdchainid']
-        assert chainid != new_chainid
+        assert_equal(mnemonic, get_mnemonic(node))
+        if not self.options.descriptors:
+            new_chainid = node.getwalletinfo()['hdchainid']
+            assert chainid != new_chainid
         assert_equal(balance_non_HD, node.getbalance())
         node.keypoolrefill(5)
         node.rescanblockchain()
@@ -119,8 +126,9 @@ def run_test(self):
 
         self.log.info("Same mnemonic, another mnemonic passphrase, no wallet passphrase, should result in a different set of keys (again)")
         assert node.upgradetohd(mnemonic, new_mnemonic_passphrase)
-        assert_equal(mnemonic, node.dumphdinfo()['mnemonic'])
-        assert_equal(new_chainid, node.getwalletinfo()['hdchainid'])
+        assert_equal(mnemonic, get_mnemonic(node))
+        if not self.options.descriptors:
+            assert_equal(new_chainid, node.getwalletinfo()['hdchainid'])
         assert_equal(balance_non_HD, node.getbalance())
         node.keypoolrefill(5)
         node.rescanblockchain()
@@ -132,8 +140,9 @@ def run_test(self):
 
         self.log.info("Same mnemonic, no mnemonic passphrase, no wallet passphrase, should recover all coins after rescan")
         assert node.upgradetohd(mnemonic)
-        assert_equal(mnemonic, node.dumphdinfo()['mnemonic'])
-        assert_equal(chainid, node.getwalletinfo()['hdchainid'])
+        assert_equal(mnemonic, get_mnemonic(node))
+        if not self.options.descriptors:
+            assert_equal(chainid, node.getwalletinfo()['hdchainid'])
         node.keypoolrefill(5)
         assert balance_after != node.getbalance()
         node.rescanblockchain()
@@ -144,8 +153,9 @@ def run_test(self):
         self.log.info("Same mnemonic, no mnemonic passphrase, no wallet passphrase, large enough keepool, should recover all coins with no extra rescan")
         self.restart_node(0, extra_args=['-keypool=10'])
         assert node.upgradetohd(mnemonic)
-        assert_equal(mnemonic, node.dumphdinfo()['mnemonic'])
-        assert_equal(chainid, node.getwalletinfo()['hdchainid'])
+        assert_equal(mnemonic, get_mnemonic(node))
+        if not self.options.descriptors:
+            assert_equal(chainid, node.getwalletinfo()['hdchainid'])
         # All coins should be recovered
         assert_equal(balance_after, node.getbalance())
 
@@ -154,8 +164,9 @@ def run_test(self):
         self.log.info("Same mnemonic, no mnemonic passphrase, no wallet passphrase, large enough keepool, rescan is skipped initially, should recover all coins after rescanblockchain")
         self.restart_node(0, extra_args=['-keypool=10'])
         assert node.upgradetohd(mnemonic, "", "", False)
-        assert_equal(mnemonic, node.dumphdinfo()['mnemonic'])
-        assert_equal(chainid, node.getwalletinfo()['hdchainid'])
+        assert_equal(mnemonic, get_mnemonic(node))
+        if not self.options.descriptors:
+            assert_equal(chainid, node.getwalletinfo()['hdchainid'])
         assert balance_after != node.getbalance()
         node.rescanblockchain()
         # All coins should be recovered
@@ -171,8 +182,9 @@ def run_test(self):
         self.start_node(0, extra_args=['-rescan'])
         assert_raises_rpc_error(-13, "Error: Please enter the wallet passphrase with walletpassphrase first.", node.dumphdinfo)
         node.walletpassphrase(walletpass, 100)
-        assert_equal(mnemonic, node.dumphdinfo()['mnemonic'])
-        assert_equal(chainid, node.getwalletinfo()['hdchainid'])
+        assert_equal(mnemonic, get_mnemonic(node))
+        if not self.options.descriptors:
+            assert_equal(chainid, node.getwalletinfo()['hdchainid'])
         # Note: wallet encryption results in additional keypool topup,
         # so we can't compare new balance to balance_non_HD here,
         # assert_equal(balance_non_HD, node.getbalance())  # won't work
@@ -191,12 +203,19 @@ def run_test(self):
         node.wait_until_stopped()
         self.start_node(0, extra_args=['-rescan'])
         assert_raises_rpc_error(-13, "Error: Wallet encrypted but passphrase not supplied to RPC.", node.upgradetohd, mnemonic)
-        assert_raises_rpc_error(-1,  "Error: The wallet passphrase entered was incorrect", node.upgradetohd, mnemonic, "", "wrongpass")
+        if not self.options.descriptors:
+            assert_raises_rpc_error(-1,  "Error: The wallet passphrase entered was incorrect", node.upgradetohd, mnemonic, "", "wrongpass")
+        else:
+            assert_raises_rpc_error(-4,  "Error: The wallet passphrase entered was incorrect", node.upgradetohd, mnemonic, "", "wrongpass")
         assert node.upgradetohd(mnemonic, "", walletpass)
-        assert_raises_rpc_error(-13, "Error: Please enter the wallet passphrase with walletpassphrase first.", node.dumphdinfo)
+        if not self.options.descriptors:
+            assert_raises_rpc_error(-13, "Error: Please enter the wallet passphrase with walletpassphrase first.", node.dumphdinfo)
+        else:
+            assert_raises_rpc_error(-13, "Error: Please enter the wallet passphrase with walletpassphrase first.", node.listdescriptors, True)
         node.walletpassphrase(walletpass, 100)
-        assert_equal(mnemonic, node.dumphdinfo()['mnemonic'])
-        assert_equal(chainid, node.getwalletinfo()['hdchainid'])
+        assert_equal(mnemonic, get_mnemonic(node))
+        if not self.options.descriptors:
+            assert_equal(chainid, node.getwalletinfo()['hdchainid'])
         # Note: wallet encryption results in additional keypool topup,
         # so we can't compare new balance to balance_non_HD here,
         # assert_equal(balance_non_HD, node.getbalance())  # won't work
@@ -206,6 +225,28 @@ def run_test(self):
         # All coins should be recovered
         assert_equal(balance_after, node.getbalance())
 
+        self.log.info("Test upgradetohd with user defined mnemonic")
+        custom_mnemonic = "similar behave slot swim scissors throw planet view ghost laugh drift calm"
+        # this address belongs to custom mnemonic with no passphrase
+        custom_address_1 = "yLpq97zZUsFQ2rdMqhcPKkYT36MoPK4Hob"
+        # this address belongs to custom mnemonic with passphrase "custom-passphrase"
+        custom_address_2 = "yYBPeZQcqgQHu9dxA5pKBWtYbK2hwfFHxf"
+        node.sendtoaddress(custom_address_1, 11)
+        node.sendtoaddress(custom_address_2, 12)
+        self.generate(node, 1)
+
+        node.createwallet("wallet-11", blank=True)
+        w11 = node.get_wallet_rpc("wallet-11")
+        w11.upgradetohd(custom_mnemonic)
+        assert_equal(11, w11.getbalance())
+        w11.unloadwallet()
+
+        node.createwallet("wallet-12", blank=True)
+        w12 = node.get_wallet_rpc("wallet-12")
+        w12.upgradetohd(custom_mnemonic, "custom-passphrase")
+        assert_equal(12, w12.getbalance())
+        w12.unloadwallet()
+
 
 if __name__ == '__main__':
     WalletUpgradeToHDTest().main ()
diff --git a/test/lint/lint-python.py b/test/lint/lint-python.py
index 61d2c49a4629..574cb65cc05e 100755
--- a/test/lint/lint-python.py
+++ b/test/lint/lint-python.py
@@ -13,7 +13,7 @@
 import subprocess
 import sys
 
-DEPS = ['flake8', 'mypy', 'pyzmq']
+DEPS = ['flake8', 'lief', 'mypy', 'pyzmq']
 MYPY_CACHE_DIR = f"{os.getenv('BASE_ROOT_DIR', '')}/test/.mypy_cache"
 FILES_ARGS = ['git', 'ls-files', '--','test/functional/*.py', 'contrib/devtools/*.py', ':(exclude)contrib/devtools/github-merge.py']
 EXCLUDE_DIRS = ['src/dashbls/',
