diff --git a/ci/README.md b/ci/README.md
index 3c5f04c39e..de798607df 100644
--- a/ci/README.md
+++ b/ci/README.md
@@ -8,8 +8,7 @@ Be aware that the tests will be built and run in-place, so please run at your ow
 If the repository is not a fresh git clone, you might have to clean files from previous builds or test runs first.
 
 The ci needs to perform various sysadmin tasks such as installing packages or writing to the user's home directory.
-While most of the actions are done inside a docker container, this is not possible for all. Thus, cache directories,
-such as the depends cache, previous release binaries, or ccache, are mounted as read-write into the docker container. While it should be fine to run
+While it should be fine to run
 the ci system locally on you development box, the ci scripts can generally be assumed to have received less review and
 testing compared to other parts of the codebase. If you want to keep the work tree clean, you might want to run the ci
 system in a virtual machine with a Linux operating system of your choice.
diff --git a/ci/test/00_setup_env.sh b/ci/test/00_setup_env.sh
index 07c20f632d..ab830b8ec0 100755
--- a/ci/test/00_setup_env.sh
+++ b/ci/test/00_setup_env.sh
@@ -8,11 +8,10 @@ export LC_ALL=C.UTF-8
 
 # The root dir.
 # The ci system copies this folder.
-# This is where the depends build is done.
 BASE_ROOT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )"/../../ >/dev/null 2>&1 && pwd )
 export BASE_ROOT_DIR
 # The depends dir.
-# This folder exists on the ci host and ci guest. Changes are propagated back and forth.
+# This folder exists only on the ci guest, and on the ci host as a volume.
 export DEPENDS_DIR=${DEPENDS_DIR:-$BASE_ROOT_DIR/depends}
 # A folder for the ci system to put temporary files (ccache, datadirs for tests, ...)
 # This folder only exists on the ci host.
@@ -58,12 +57,14 @@ export CCACHE_SIZE=${CCACHE_SIZE:-100M}
 export CCACHE_TEMPDIR=${CCACHE_TEMPDIR:-/tmp/.ccache-temp}
 export CCACHE_COMPRESS=${CCACHE_COMPRESS:-1}
 # The cache dir.
-# This folder exists on the ci host and ci guest. Changes are propagated back and forth.
+# This folder exists only on the ci guest, and on the ci host as a volume.
 export CCACHE_DIR=${CCACHE_DIR:-$BASE_SCRATCH_DIR/.ccache}
 # Folder where the build result is put (bin and lib).
 export BASE_OUTDIR=${BASE_OUTDIR:-$BASE_SCRATCH_DIR/out/$HOST}
 # Folder where the build is done (dist and out-of-tree build).
 export BASE_BUILD_DIR=${BASE_BUILD_DIR:-$BASE_SCRATCH_DIR/build}
+# The folder for previous release binaries.
+# This folder exists only on the ci guest, and on the ci host as a volume.
 export PREVIOUS_RELEASES_DIR=${PREVIOUS_RELEASES_DIR:-$BASE_ROOT_DIR/releases/$HOST}
 export SDK_URL=${SDK_URL:-https://bitcoincore.org/depends-sources/sdks}
 export CI_BASE_PACKAGES=${CI_BASE_PACKAGES:-build-essential libtool autotools-dev automake pkg-config bsdmainutils curl ca-certificates ccache python3 rsync git procps bison}
diff --git a/ci/test/04_install.sh b/ci/test/04_install.sh
index 05bef79a3d..62bc3a963d 100755
--- a/ci/test/04_install.sh
+++ b/ci/test/04_install.sh
@@ -39,6 +39,9 @@ if [ -z "$DANGER_RUN_CI_ON_HOST" ]; then
       --build-arg "FILE_ENV=${FILE_ENV}" \
       --tag="${CONTAINER_NAME}" \
       "${BASE_ROOT_DIR}"
+  docker volume create "${CONTAINER_NAME}_ccache" || true
+  docker volume create "${CONTAINER_NAME}_depends" || true
+  docker volume create "${CONTAINER_NAME}_previous_releases" || true
 
   if [ -n "${RESTART_CI_DOCKER_BEFORE_RUN}" ] ; then
     echo "Restart docker before run to stop and clear all containers started with --rm"
@@ -48,9 +51,9 @@ if [ -z "$DANGER_RUN_CI_ON_HOST" ]; then
   # shellcheck disable=SC2086
   CI_CONTAINER_ID=$(docker run $CI_CONTAINER_CAP --rm --interactive --detach --tty \
                   --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 \
-                  --mount type=bind,src=$PREVIOUS_RELEASES_DIR,dst=$PREVIOUS_RELEASES_DIR \
+                  --mount "type=volume,src=${CONTAINER_NAME}_ccache,dst=$CCACHE_DIR" \
+                  --mount "type=volume,src=${CONTAINER_NAME}_depends,dst=$DEPENDS_DIR" \
+                  --mount "type=volume,src=${CONTAINER_NAME}_previous_releases,dst=$PREVIOUS_RELEASES_DIR" \
                   -w $BASE_ROOT_DIR \
                   --env-file /tmp/env \
                   --name $CONTAINER_NAME \
diff --git a/configure.ac b/configure.ac
index dacc65a7f1..586db4dc69 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1418,14 +1418,15 @@ if test "$use_upnp" != "no"; then
     [AC_CHECK_LIB([miniupnpc], [upnpDiscover], [MINIUPNPC_LIBS="$MINIUPNPC_LIBS -lminiupnpc"], [have_miniupnpc=no], [$MINIUPNPC_LIBS])],
     [have_miniupnpc=no]
   )
-  dnl The minimum supported miniUPnPc API version is set to 10. This keeps compatibility
-  dnl with Ubuntu 16.04 LTS and Debian 8 libminiupnpc-dev packages.
+
+  dnl The minimum supported miniUPnPc API version is set to 17. This excludes
+  dnl versions with known vulnerabilities.
   if test "$have_miniupnpc" != "no"; then
     AC_MSG_CHECKING([whether miniUPnPc API version is supported])
     AC_PREPROC_IFELSE([AC_LANG_PROGRAM([[
         @%:@include <miniupnpc/miniupnpc.h>
       ]], [[
-        #if MINIUPNPC_API_VERSION >= 10
+        #if MINIUPNPC_API_VERSION >= 17
         // Everything is okay
         #else
         #  error miniUPnPc API version is too old
@@ -1434,7 +1435,7 @@ if test "$use_upnp" != "no"; then
         AC_MSG_RESULT([yes])
       ],[
       AC_MSG_RESULT([no])
-      AC_MSG_WARN([miniUPnPc API version < 10 is unsupported, disabling UPnP support.])
+      AC_MSG_WARN([miniUPnPc API version < 17 is unsupported, disabling UPnP support.])
       have_miniupnpc=no
     ])
   fi
diff --git a/contrib/verify-commits/allow-revsig-commits b/contrib/verify-commits/allow-revsig-commits
index 0bb299b8fa..0c43d9cce5 100644
--- a/contrib/verify-commits/allow-revsig-commits
+++ b/contrib/verify-commits/allow-revsig-commits
@@ -643,3 +643,178 @@ b7365f0545b1a6862e3277b2b2139ee0d5aee1cf
 4bd0e9b90a39c5c6a016b83882ae44cb4d28f1f8
 7438ceac716fdfe6621728c05e718eaa89dd89aa
 4e3efd47e0d50c6cd1dc81ccc9669a5b2658f495
+5ab6a942764bf6577ae311f2551153dde3d4830c
+b04f42efe31e23e15cc945efe0de906ed2eadb2b
+ceae0eb7e31f9d3495a13a23df7372e5e870b572
+5bf65ec66e5986c9188e3f6234f1c5c0f8dc7f90
+55c9e2d790fa2e137ccd0d91e6cf3e2d0bff4813
+ba29911e21c88f49780c6c87f94ff8ed6e764a9d
+fffff0abb9c71f0af83a7925db3c293b3bb12158
+aaeb315ff0f7956449a92736160795f0140369e3
+0dd34773334c7f4db7b05df30ee61b011795b46d
+2598720d6c1ef15288045599de7e65f8707d09bc
+bc83710fdcc09d8e427e77457df107acc9db1be5
+ddd7a39aa960ee3639ef1e59b2e53852e0862c52
+0808c88d7bd992d5c9ded0009c9563f6177b4035
+a085a554913ae8f4ed83afac830ce6dc39c9cc65
+b1a824dd06aa58618947783edee2dd891b5204dc
+a4e066af8573dcefb11dff120e1c09e8cf7f40c2
+58b9d6cf9e9b801be9c677a3ae121e5d2950ce66
+7377ed778c6d832ecd291e65b2789af7bac2ae2b
+c3a41ad980cc5149de3f9ec8414962c183b1fed9
+5884a47c367f6ff1aff3ae1ef6894881c5a5e0b7
+1d39c9ca0672e7ad4c1f0959f9d58d2fcc7dc46b
+e16f6441044fc2123e0cbdcbd8a5842ec3aae7a0
+6c6cc7989cac79450bf83b932ca82d390a37e17b
+bc28ca3afb7f6656a0bf50038a5e383ee7f9b219
+57a491bee17af88f75c2cea8c109d93b1cdbc9a8
+f8586b25f6a4f1e30a54e58f45dd28aaf580bbc6
+e5df0ba0d97e5f8cfd748f09d6ed77b7bfc45471
+1b0469199bdaedfd452eea718268be7fd50db3c0
+015717e2b873b7a2ce433bd3be2328a782aa5d91
+3b3c66f85959f3393a3a9e87a29004b526f91b93
+874529665c1c326fc86fc0d0d6c3512fab087ab8
+7f2c983e1cfdb58b6f84eabe5ff6a16f143f39aa
+0ea92cad5274f3939f09d6890da31a21b8481282
+489b5876698f9bb2d93b1b1d62d514148b31effd
+faf25b09d9e78f2ff129e25b90f67930d2fc1c4f
+df933596e7e9aa17f7e5cd6e1c850520f5b56f1b
+9e4fbebcc8e497016563e46de4c64fa094edab2d
+1557014378cc5a6234a9244fa60132955206fd27
+c5fbcf5f8d7b36bee54ac80d1027d0dccea2aa75
+cccbc5fe3ea5ae52426203f4485b11071fbe4b6e
+5174a139c92c1238f9700d06e362dc628d81a0a9
+9dae9f5f1e2bf29f58d3f49b0c612063d883b8b3
+e282764e049523439bc8adaadc002a1420122830
+d8ae5044488248d5eb134aa7c0a15c813a2f8715
+06ea2783a2c11e7b171e2809c3211bb3091d894d
+00ce8543f16f4357926eb6dc701ac6229142be80
+1f63b460a8506675ccacb4647941f07d391735e3
+a100c42a136da5ddfd09aa442543ec2190f24faf
+636991d0c0f969968c790d490c82c1d2fa4e8047
+dd52f79a73eca18301db1569d517197160018dbb
+e157b98640c7cfb94cae7e0faca3bcffc2dde990
+ad9e5eaf77bf7e19a926a43407c88386a8a1e58f
+c5e67be03bb06a5d7885c55db1f016fbf2333fe3
+48eec32347494da781f26478fa488b28336afbd2
+c324b07a541a04698954ece94e5879ae7131c1c7
+4901631dac6a883c6ddd0d4e5e3edd08b10d7609
+cacbdbaa95317b45cf2100702bca92410fb43b9a
+b4f686952a60bbadc7ed2250651d0d6af0959f4d
+90e49c1ececd6296c6ec6109cea525a208c0626e
+700808754884919916a5518e7ecfdabadef956d8
+0cd1a2eff9e0020ec1052a931f3863794d1a95d9
+51527ec1ec4264f7e24ef548bb049db07a89fc7f
+ed4eeafbb6e2e73ff9fb9c03bd66bbb049b8aacd
+d4475ea7ae70ad1a1f9374b88c68f01590a88d54
+5e1aacab576b8d8918da129097a9ac0816b6ead2
+fe6a299fc0020cd62156d4b7dd9c8dac358c69c5
+0047d9b89b9fa6be660c363961cf0af72fa62ecf
+037c5e511fe2185d244049cae25a98f99b878787
+8730bd3fc87c8a7d44347267e1ff3c7a8674201b
+47b8256da872722953693c4037d1b9e07caadcb1
+85aea18ae660b5edf7b6c1415f033cfcb15307f9
+132d5f8c2f2397a4600a42203f413dafdb6bcc37
+23ebd7a8027f12e722834d214113892fe8561fe1
+a19f641a80cb2841ca9f593e9be589dbc4b4ae7c
+1e7db37e76c510d373c4404eea2b97508b367aca
+16fa967d3cca66eef0f17b41fd8aaee6a1420fbc
+9eedbe98c86ff2a9214c24c37f6524ce67fd129b
+0342ae1d395ca82614f6d3b8fabb6a44403baf2a
+777b89b3008e53374eb13fdee70db315cd61a703
+8b686776ef5cbd6ef9d5281c3136eded25ea35a4
+c90b42bcdb594638c5759ef5ef0773314d0a1379
+7134327be5c1bdcef7919ed735049a6bbfc457ec
+e88a52e9a2fda971d34425bb80e42ad2d6623d68
+173c79626867e9f89d49be7dcbb0c2042c480553
+2513499348fa955d0e4b0970b08ba9e715e6316e
+43bb10661360d9f35d921d493a1f94ac95df00e2
+6f55ab57cbfa414d57a8e9fb9a47f9bcd8c836d7
+6300b9556ec927a61371053fafe1a4045f5afb00
+f8b2e9bcfc76fede05f5e12f7b15f0d9c9d0add5
+b297b945f7610772434817181ad12067b2832565
+57a73d71a36ce212977607d3e94de6ef55521bfc
+5fdf37e14bb3b66264a7e6868250c2084ac39054
+3059d4dd72af73b654077d9f72019c47edd47674
+333a41882c5ccd5f0c7f884f97d25449bdeec07b
+7da4f65a00a8d96da2119de613ed7fbee2a28a0d
+e14f0fa6a346afecbb1d5470aef5226a8cc33e57
+cf0a8b9c4870cc88254a757286140d9632e7b70c
+b69fd5eaa99f84b62a49d7c7f48d8cee1227592a
+1e3ed01faa77215a7c36308237280aaa58895532
+6c9bc14a3f2cfa50144607c820ebab5288f9571c
+8e3c266a4f02093d57d563f32ba73d3ab4b5f208
+decde9bba6f9d3671bdf0af4fe6ff4bf28992d1d
+9b7eb584ade2ce73dbfcda080935172c3857b758
+3bbc46ddafb61f68785c7e581817db952f99d93a
+bbb83f0b2b2671980c06453fd243b1f2801a1cc4
+6c9460edaeb6c89692b71f51be7b7ee386f4f5c4
+b3072799248fae8fc16f910b642edb9c5acf8bac
+696d39410fc3372d120a6e89695c1543ac2fc052
+c5c4fb31828107a5ded88627632e19e05b2c7e83
+9ce1c506a3a5d20b1bf254235bfae48af592d86c
+fe66dad8a779ed928b1c2fc0c3accf594b042877
+f421de5be611f874a027392d5fee7e113dce4f54
+d492dc1cdaabdc52b0766bf4cba4bd73178325d0
+6348bc61b533705a229f2c2ddcff2bdd98849d07
+83b26cb97cb46516aa4fdee3bcbfa751d28c1233
+afac75f140a3e7d89877f03420e1bc64a8d8c6cd
+171f6f2699dc27e77843318be2fefdfcd9e589fb
+50c806f001d66e20f314777b9fa7fefa01dc6893
+bdbabc50ba6c87ded97ea2bbacd3605c59cd12d0
+9e32adbb5c543885b2c01a984bf1e4b80e8cec16
+7c08d81e119570792648fe95bbacddbb1d5f9ae2
+65e9ca22785f4a799cbcff6d95cbe1ce4b4a6bd2
+2948d6dea098bf722828b969838668f833c2cb00
+deb847b75710d600e5b0d3d5c77fa5166d80808a
+05e5af5a6c884d2ade3d7acc766ad5380cb85b64
+cba41db327a241f992f9329b214d9070888255b8
+f6d335e82822ed8f2da56c1bcaddf7f99acd4997
+30308cc380a8176a5ec0e0bd2beed8b9c482ccf7
+8b6cd42c6226dea28c182a48a214d1c091b9b5bb
+267917f5632a99bb51fc3fe516d8308e79d31ed1
+ba11eb354b9f3420ebb8608227062fb639a07496
+848b11615b67a3c49f76ebbcaa241a322d8014d8
+25290071c434638e2719a99784572deef44542ad
+159f89c118645c0f9e23e453f01cede84abac795
+37637bea3a9a48c0d52d68d3f78f154f8249a009
+0a76bf848c72211f986a6cc5b1c13de820b861dd
+358fe779cbb2681666ae5ab23a19662db21a2c46
+c44e734dca64a15fae92255a5d848c04adaad2fa
+8add59d77dd621be57059229f378822e4b707318
+922c49a1389531d9fba30168257c466bd413f625
+df0825046acc7cb496c47666e36af18118beb030
+c23bf06492dddacfc0eece3d4dd12cce81496dd0
+3eec29ed3aa1c8eb293a7a7a6be356fc014f8813
+a7e80449c0811b361cdaea39b6bab78ca5fbf668
+5e8e0b3d7f6055e326bda61e60712b530e8920f0
+a5edd191be93aff8f9c0f60f04e711e2e78ecc77
+515200298b555845696a07ae2bc0a84a5dd02ae4
+e8a3882e20f0ffeeb9b3964c2c09d8aa5eb53fd4
+c545a7aeb1d559377933c7b2e6edc2d4a37b33fb
+df669230cf2001dd869e897bb4f2d9c46f9accd9
+56a0fbf8365343d73cdff2b0a0e16542294d7577
+196b4599201dbce3e0317e9b98753fa6a244b82d
+cf5bb048e80d4cde8828787b266b7f5f2e3b6d7b
+b94d0c7af11bd91dad4f180ce2a2ffa09e4b5668
+792d0d8d512cf8ddca200317b74ce550c1a1a428
+767ee2e3a1082468b4e2248bac3ef8bd54bb2ddb
+31db3dd874dfbba88616c96a5767e2c9861d9a7a
+018fd9620293582f0ce43d344ac3110e19c4dedc
+801aaac2b39564aa14009785146ba26d2506fb53
+121d47afe3e67ff7f94d26e08a39573dccf652aa
+af7fba3af788e91a460582351d40f8f5e2118760
+8f1c28a609b203e0d0a844d9cc5ada9eb9160a5e
+8319c4e906e6df5f2048e7c048942fde285a93a2
+66be456d93a66526322b7f36fd734a8dbd5e5524
+c006ab29ceec9274dc85a0de7f7d0502021a4b87
+1220af5e6d1072ea306f6ecaaa7effe3d386c379
+14ba286556faad794f288ef38493c540382897cb
+784a21d35466736a7a372364498ed94482a12a2a
+4ad59042b359f473d5888ecee0c9288dcf98f1c9
+fee16b15fa3425871670239c25d4e61ae961e0c5
+216f4ca9e7ccb1f0fcb9bab0f9940992a87ae55f
+2d0bdb2089644f5904629413423cdc897911b081
+50c502f54abd9eb15c8ddca013f0fdfae3d132a9
+c840ab0231bc29057172179f005001c9ab299554
+aab5e48d422d396aec09bd6389de68613b19def5
diff --git a/contrib/verify-commits/trusted-git-root b/contrib/verify-commits/trusted-git-root
index 1c42195961..efb6b9f7b4 100644
--- a/contrib/verify-commits/trusted-git-root
+++ b/contrib/verify-commits/trusted-git-root
@@ -1 +1 @@
-577bd51a4b8de066466a445192c1c653872657e2
+8ef096d4f8e08ac691502e3fd34721a8bdfa9044
diff --git a/contrib/verify-commits/trusted-keys b/contrib/verify-commits/trusted-keys
index 5ca65e7b0d..eeafcdf205 100644
--- a/contrib/verify-commits/trusted-keys
+++ b/contrib/verify-commits/trusted-keys
@@ -1,4 +1,3 @@
-71A3B16735405025D447E8F274810B012346C9A6
 B8B3F1C0E58C15DB6A81D30C3648A882F4316B9B
 E777299FC265DD04793070EB944D35F9AC3DB76A
 D1DBF2C4B96F2DEBF4C16654410108112E7EA81F
diff --git a/contrib/verifybinaries/README.md b/contrib/verifybinaries/README.md
index c50d4bef71..ab831eea28 100644
--- a/contrib/verifybinaries/README.md
+++ b/contrib/verifybinaries/README.md
@@ -1,16 +1,5 @@
 ### Verify Binaries
 
-#### Preparation:
-
-Make sure you obtain the proper release signing key and verify the fingerprint with several independent sources.
-
-```sh
-$ gpg --fingerprint "Bitcoin Core binary release signing key"
-pub   4096R/36C2E964 2015-06-24 [expires: YYYY-MM-DD]
-      Key fingerprint = 01EA 5486 DE18 A882 D4C2  6845 90C8 019E 36C2 E964
-uid                  Wladimir J. van der Laan (Bitcoin Core binary release signing key) <laanwj@gmail.com>
-```
-
 #### Usage:
 
 This script attempts to download the signature file `SHA256SUMS.asc` from https://bitcoin.org.
diff --git a/doc/build-freebsd.md b/doc/build-freebsd.md
index d45e9c4d0d..aa10e4a891 100644
--- a/doc/build-freebsd.md
+++ b/doc/build-freebsd.md
@@ -36,13 +36,30 @@ pkg install sqlite3
 ```
 
 ###### Legacy Wallet Support
-`db5` is only required to support legacy wallets.
-Skip if you don't intend to use legacy wallets.
+BerkeleyDB is only required if legacy wallet support is required.
+
+It is required to use Berkeley DB 4.8. You **cannot** use the BerkeleyDB library
+from ports. However, you can build DB 4.8 yourself [using depends](/depends).
 
-```bash
-pkg install db5
 ```
----
+gmake -C depends NO_BOOST=1 NO_LIBEVENT=1 NO_QT=1 NO_SQLITE=1 NO_NATPMP=1 NO_UPNP=1 NO_ZMQ=1 NO_USDT=1
+```
+
+When the build is complete, the Berkeley DB installation location will be displayed:
+
+```
+to: /path/to/bitcoin/depends/x86_64-unknown-freebsd[release-number]
+```
+
+Finally, set `BDB_PREFIX` to this path according to your shell:
+
+```
+csh: setenv BDB_PREFIX [path displayed above]
+```
+
+```
+sh/bash: export BDB_PREFIX=[path displayed above]
+```
 
 #### GUI Dependencies
 ###### Qt5
@@ -91,12 +108,12 @@ This explicitly enables the GUI and disables legacy wallet support, assuming `sq
 
 ##### Descriptor & Legacy Wallet. No GUI:
 This enables support for both wallet types and disables the GUI, assuming
-`sqlite3` and `db5` are both installed.
+`sqlite3` and `db4` are both installed.
 ```bash
 ./autogen.sh
-./configure --with-gui=no --with-incompatible-bdb \
-    BDB_LIBS="-ldb_cxx-5" \
-    BDB_CFLAGS="-I/usr/local/include/db5" \
+./configure --with-gui=no \
+    BDB_LIBS="-L${BDB_PREFIX}/lib -ldb_cxx-4.8" \
+    BDB_CFLAGS="-I${BDB_PREFIX}/include" \
     MAKE=gmake
 ```
 
diff --git a/doc/dependencies.md b/doc/dependencies.md
index 3349c81c46..ec205e4b51 100644
--- a/doc/dependencies.md
+++ b/doc/dependencies.md
@@ -36,7 +36,7 @@ You can find installation instructions in the `build-*.md` file for your platfor
 | Dependency | Releases | Version used | Minimum required | Runtime |
 | --- | --- | --- | --- | --- |
 | [libnatpmp](../depends/packages/libnatpmp.mk) | [link](https://github.com/miniupnp/libnatpmp/) | commit [07004b9...](https://github.com/bitcoin/bitcoin/pull/25917) | | No |
-| [MiniUPnPc](../depends/packages/miniupnpc.mk) | [link](https://miniupnp.tuxfamily.org/) | [2.2.2](https://github.com/bitcoin/bitcoin/pull/20421) | 1.9 | No |
+| [MiniUPnPc](../depends/packages/miniupnpc.mk) | [link](https://miniupnp.tuxfamily.org/) | [2.2.2](https://github.com/bitcoin/bitcoin/pull/20421) | 2.1 | No |
 
 ### Notifications
 | Dependency | Releases | Version used | Minimum required | Runtime |
diff --git a/share/rpcauth/rpcauth.py b/share/rpcauth/rpcauth.py
index d441d5f21d..cc7bba1f8b 100755
--- a/share/rpcauth/rpcauth.py
+++ b/share/rpcauth/rpcauth.py
@@ -4,22 +4,20 @@
 # file COPYING or http://www.opensource.org/licenses/mit-license.php.
 
 from argparse import ArgumentParser
-from base64 import urlsafe_b64encode
 from getpass import getpass
-from os import urandom
-
+from secrets import token_hex, token_urlsafe
 import hmac
 
 def generate_salt(size):
     """Create size byte hex salt"""
-    return urandom(size).hex()
+    return token_hex(size)
 
 def generate_password():
     """Create 32 byte b64 password"""
-    return urlsafe_b64encode(urandom(32)).decode('utf-8')
+    return token_urlsafe(32)
 
 def password_to_hmac(salt, password):
-    m = hmac.new(bytearray(salt, 'utf-8'), bytearray(password, 'utf-8'), 'SHA256')
+    m = hmac.new(salt.encode('utf-8'), password.encode('utf-8'), 'SHA256')
     return m.hexdigest()
 
 def main():
@@ -38,8 +36,8 @@ def main():
     password_hmac = password_to_hmac(salt, args.password)
 
     print('String to be appended to bitcoin.conf:')
-    print('rpcauth={0}:{1}${2}'.format(args.username, salt, password_hmac))
-    print('Your password:\n{0}'.format(args.password))
+    print(f'rpcauth={args.username}:{salt}${password_hmac}')
+    print(f'Your password:\n{args.password}')
 
 if __name__ == '__main__':
     main()
diff --git a/src/Makefile.test.include b/src/Makefile.test.include
index 4d867fdc2f..fa77e28736 100644
--- a/src/Makefile.test.include
+++ b/src/Makefile.test.include
@@ -162,7 +162,8 @@ BITCOIN_TESTS =\
   test/validation_flush_tests.cpp \
   test/validation_tests.cpp \
   test/validationinterface_tests.cpp \
-  test/versionbits_tests.cpp
+  test/versionbits_tests.cpp \
+  test/xoroshiro128plusplus_tests.cpp
 
 if ENABLE_WALLET
 BITCOIN_TESTS += \
@@ -248,6 +249,7 @@ test_fuzz_fuzz_SOURCES = \
  test/fuzz/chain.cpp \
  test/fuzz/checkqueue.cpp \
  test/fuzz/coins_view.cpp \
+ test/fuzz/coinscache_sim.cpp \
  test/fuzz/connman.cpp \
  test/fuzz/crypto.cpp \
  test/fuzz/crypto_aes256.cpp \
diff --git a/src/Makefile.test_util.include b/src/Makefile.test_util.include
index 8496b3698a..ae77b79b8b 100644
--- a/src/Makefile.test_util.include
+++ b/src/Makefile.test_util.include
@@ -19,7 +19,8 @@ TEST_UTIL_H = \
   test/util/str.h \
   test/util/transaction_utils.h \
   test/util/txmempool.h \
-  test/util/validation.h
+  test/util/validation.h \
+  test/util/xoroshiro128plusplus.h
 
 if ENABLE_WALLET
 TEST_UTIL_H += wallet/test/util.h
diff --git a/src/bench/chacha20.cpp b/src/bench/chacha20.cpp
index 656fb833e7..115cd064bd 100644
--- a/src/bench/chacha20.cpp
+++ b/src/bench/chacha20.cpp
@@ -14,9 +14,9 @@ static const uint64_t BUFFER_SIZE_LARGE = 1024*1024;
 static void CHACHA20(benchmark::Bench& bench, size_t buffersize)
 {
     std::vector<uint8_t> key(32,0);
-    ChaCha20 ctx(key.data(), key.size());
+    ChaCha20 ctx(key.data());
     ctx.SetIV(0);
-    ctx.Seek(0);
+    ctx.Seek64(0);
     std::vector<uint8_t> in(buffersize,0);
     std::vector<uint8_t> out(buffersize,0);
     bench.batch(in.size()).unit("byte").run([&] {
diff --git a/src/bitcoin-cli.cpp b/src/bitcoin-cli.cpp
index e6e33007d5..df8fb7cece 100644
--- a/src/bitcoin-cli.cpp
+++ b/src/bitcoin-cli.cpp
@@ -55,7 +55,10 @@ static constexpr int DEFAULT_WAIT_CLIENT_TIMEOUT = 0;
 static const bool DEFAULT_NAMED=false;
 static const int CONTINUE_EXECUTION=-1;
 static constexpr int8_t UNKNOWN_NETWORK{-1};
-static constexpr std::array NETWORKS{"ipv4", "ipv6", "onion", "i2p", "cjdns"};
+// See GetNetworkName() in netbase.cpp
+static constexpr std::array NETWORKS{"not_publicly_routable", "ipv4", "ipv6", "onion", "i2p", "cjdns", "internal"};
+static constexpr std::array NETWORK_SHORT_NAMES{"npr", "ipv4", "ipv6", "onion", "i2p", "cjdns", "int"};
+static constexpr std::array UNREACHABLE_NETWORK_IDS{/*not_publicly_routable*/0, /*internal*/6};
 
 /** Default number of blocks to generate for RPC generatetoaddress. */
 static const std::string DEFAULT_NBLOCKS = "1";
@@ -289,7 +292,7 @@ class AddrinfoRequestHandler : public BaseRequestHandler
         // Prepare result to return to user.
         UniValue result{UniValue::VOBJ}, addresses{UniValue::VOBJ};
         uint64_t total{0}; // Total address count
-        for (size_t i = 0; i < NETWORKS.size(); ++i) {
+        for (size_t i = 1; i < NETWORKS.size() - 1; ++i) {
             addresses.pushKV(NETWORKS[i], counts.at(i));
             total += counts.at(i);
         }
@@ -506,7 +509,7 @@ class NetinfoRequestHandler : public BaseRequestHandler
                 const bool is_addr_relay_enabled{peer["addr_relay_enabled"].isNull() ? false : peer["addr_relay_enabled"].get_bool()};
                 const bool is_bip152_hb_from{peer["bip152_hb_from"].get_bool()};
                 const bool is_bip152_hb_to{peer["bip152_hb_to"].get_bool()};
-                m_peers.push_back({addr, sub_version, conn_type, network, age, min_ping, ping, addr_processed, addr_rate_limited, last_blck, last_recv, last_send, last_trxn, peer_id, mapped_as, version, is_addr_relay_enabled, is_bip152_hb_from, is_bip152_hb_to, is_outbound, is_tx_relay});
+                m_peers.push_back({addr, sub_version, conn_type, NETWORK_SHORT_NAMES[network_id], age, min_ping, ping, addr_processed, addr_rate_limited, last_blck, last_recv, last_send, last_trxn, peer_id, mapped_as, version, is_addr_relay_enabled, is_bip152_hb_from, is_bip152_hb_to, is_outbound, is_tx_relay});
                 m_max_addr_length = std::max(addr.length() + 1, m_max_addr_length);
                 m_max_addr_processed_length = std::max(ToString(addr_processed).length(), m_max_addr_processed_length);
                 m_max_addr_rate_limited_length = std::max(ToString(addr_rate_limited).length(), m_max_addr_rate_limited_length);
@@ -571,6 +574,13 @@ class NetinfoRequestHandler : public BaseRequestHandler
                 reachable_networks.push_back(network_id);
             }
         };
+
+        for (const size_t network_id : UNREACHABLE_NETWORK_IDS) {
+            if (m_counts.at(2).at(network_id) == 0) continue;
+            result += strprintf("%8s", NETWORK_SHORT_NAMES.at(network_id)); // column header
+            reachable_networks.push_back(network_id);
+        }
+
         result += "   total   block";
         if (m_manual_peers_count) result += "  manual";
 
@@ -636,7 +646,7 @@ class NetinfoRequestHandler : public BaseRequestHandler
         "           \"manual\" - peer we manually added using RPC addnode or the -addnode/-connect config options\n"
         "           \"feeler\" - short-lived connection for testing addresses\n"
         "           \"addr\"   - address fetch; short-lived connection for requesting addresses\n"
-        "  net      Network the peer connected through (\"ipv4\", \"ipv6\", \"onion\", \"i2p\", or \"cjdns\")\n"
+        "  net      Network the peer connected through (\"ipv4\", \"ipv6\", \"onion\", \"i2p\", \"cjdns\", or \"npr\" (not publicly routable))\n"
         "  mping    Minimum observed ping time, in milliseconds (ms)\n"
         "  ping     Last observed ping time, in milliseconds (ms)\n"
         "  send     Time since last message sent to the peer, in seconds\n"
diff --git a/src/coins.cpp b/src/coins.cpp
index e98bf816ab..5a6ae525a7 100644
--- a/src/coins.cpp
+++ b/src/coins.cpp
@@ -32,7 +32,10 @@ bool CCoinsViewBacked::BatchWrite(CCoinsMap &mapCoins, const uint256 &hashBlock,
 std::unique_ptr<CCoinsViewCursor> CCoinsViewBacked::Cursor() const { return base->Cursor(); }
 size_t CCoinsViewBacked::EstimateSize() const { return base->EstimateSize(); }
 
-CCoinsViewCache::CCoinsViewCache(CCoinsView* baseIn) : CCoinsViewBacked(baseIn) {}
+CCoinsViewCache::CCoinsViewCache(CCoinsView* baseIn, bool deterministic) :
+    CCoinsViewBacked(baseIn), m_deterministic(deterministic),
+    cacheCoins(0, SaltedOutpointHasher(/*deterministic=*/deterministic))
+{}
 
 size_t CCoinsViewCache::DynamicMemoryUsage() const {
     return memusage::DynamicUsage(cacheCoins) + cachedCoinsUsage;
@@ -311,7 +314,24 @@ void CCoinsViewCache::ReallocateCache()
     // Cache should be empty when we're calling this.
     assert(cacheCoins.size() == 0);
     cacheCoins.~CCoinsMap();
-    ::new (&cacheCoins) CCoinsMap();
+    ::new (&cacheCoins) CCoinsMap(0, SaltedOutpointHasher(/*deterministic=*/m_deterministic));
+}
+
+void CCoinsViewCache::SanityCheck() const
+{
+    size_t recomputed_usage = 0;
+    for (const auto& [_, entry] : cacheCoins) {
+        unsigned attr = 0;
+        if (entry.flags & CCoinsCacheEntry::DIRTY) attr |= 1;
+        if (entry.flags & CCoinsCacheEntry::FRESH) attr |= 2;
+        if (entry.coin.IsSpent()) attr |= 4;
+        // Only 5 combinations are possible.
+        assert(attr != 2 && attr != 4 && attr != 7);
+
+        // Recompute cachedCoinsUsage.
+        recomputed_usage += entry.coin.DynamicMemoryUsage();
+    }
+    assert(recomputed_usage == cachedCoinsUsage);
 }
 
 static const size_t MIN_TRANSACTION_OUTPUT_WEIGHT = WITNESS_SCALE_FACTOR * ::GetSerializeSize(CTxOut(), PROTOCOL_VERSION);
diff --git a/src/coins.h b/src/coins.h
index 710b8c7c83..dd336b210a 100644
--- a/src/coins.h
+++ b/src/coins.h
@@ -211,6 +211,9 @@ class CCoinsViewBacked : public CCoinsView
 /** CCoinsView that adds a memory cache for transactions to another CCoinsView */
 class CCoinsViewCache : public CCoinsViewBacked
 {
+private:
+    const bool m_deterministic;
+
 protected:
     /**
      * Make mutable so that we can "fill the cache" even from Get-methods
@@ -223,7 +226,7 @@ class CCoinsViewCache : public CCoinsViewBacked
     mutable size_t cachedCoinsUsage{0};
 
 public:
-    CCoinsViewCache(CCoinsView *baseIn);
+    CCoinsViewCache(CCoinsView *baseIn, bool deterministic = false);
 
     /**
      * By deleting the copy constructor, we prevent accidentally using it when one intends to create a cache on top of a base cache.
@@ -320,6 +323,9 @@ class CCoinsViewCache : public CCoinsViewBacked
     //! See: https://stackoverflow.com/questions/42114044/how-to-release-unordered-map-memory
     void ReallocateCache();
 
+    //! Run an internal sanity check on the cache data structure. */
+    void SanityCheck() const;
+
 private:
     /**
      * @note this is marked const, but may actually append to `cacheCoins`, increasing
diff --git a/src/crypto/chacha20.cpp b/src/crypto/chacha20.cpp
index 25d7baa8cc..6934cef163 100644
--- a/src/crypto/chacha20.cpp
+++ b/src/crypto/chacha20.cpp
@@ -8,6 +8,7 @@
 #include <crypto/common.h>
 #include <crypto/chacha20.h>
 
+#include <algorithm>
 #include <string.h>
 
 constexpr static inline uint32_t rotl32(uint32_t v, int c) { return (v << c) | (v >> (32 - c)); }
@@ -20,95 +21,69 @@ constexpr static inline uint32_t rotl32(uint32_t v, int c) { return (v << c) | (
 
 #define REPEAT10(a) do { {a}; {a}; {a}; {a}; {a}; {a}; {a}; {a}; {a}; {a}; } while(0)
 
-static const unsigned char sigma[] = "expand 32-byte k";
-static const unsigned char tau[] = "expand 16-byte k";
-
-void ChaCha20::SetKey(const unsigned char* k, size_t keylen)
+void ChaCha20Aligned::SetKey32(const unsigned char* k)
 {
-    const unsigned char *constants;
-
-    input[4] = ReadLE32(k + 0);
-    input[5] = ReadLE32(k + 4);
-    input[6] = ReadLE32(k + 8);
-    input[7] = ReadLE32(k + 12);
-    if (keylen == 32) { /* recommended */
-        k += 16;
-        constants = sigma;
-    } else { /* keylen == 16 */
-        constants = tau;
-    }
-    input[8] = ReadLE32(k + 0);
-    input[9] = ReadLE32(k + 4);
-    input[10] = ReadLE32(k + 8);
-    input[11] = ReadLE32(k + 12);
-    input[0] = ReadLE32(constants + 0);
-    input[1] = ReadLE32(constants + 4);
-    input[2] = ReadLE32(constants + 8);
-    input[3] = ReadLE32(constants + 12);
-    input[12] = 0;
-    input[13] = 0;
-    input[14] = 0;
-    input[15] = 0;
+    input[0] = ReadLE32(k + 0);
+    input[1] = ReadLE32(k + 4);
+    input[2] = ReadLE32(k + 8);
+    input[3] = ReadLE32(k + 12);
+    input[4] = ReadLE32(k + 16);
+    input[5] = ReadLE32(k + 20);
+    input[6] = ReadLE32(k + 24);
+    input[7] = ReadLE32(k + 28);
+    input[8] = 0;
+    input[9] = 0;
+    input[10] = 0;
+    input[11] = 0;
 }
 
-ChaCha20::ChaCha20()
+ChaCha20Aligned::ChaCha20Aligned()
 {
     memset(input, 0, sizeof(input));
 }
 
-ChaCha20::ChaCha20(const unsigned char* k, size_t keylen)
+ChaCha20Aligned::ChaCha20Aligned(const unsigned char* key32)
 {
-    SetKey(k, keylen);
+    SetKey32(key32);
 }
 
-void ChaCha20::SetIV(uint64_t iv)
+void ChaCha20Aligned::SetIV(uint64_t iv)
 {
-    input[14] = iv;
-    input[15] = iv >> 32;
+    input[10] = iv;
+    input[11] = iv >> 32;
 }
 
-void ChaCha20::Seek(uint64_t pos)
+void ChaCha20Aligned::Seek64(uint64_t pos)
 {
-    input[12] = pos;
-    input[13] = pos >> 32;
+    input[8] = pos;
+    input[9] = pos >> 32;
 }
 
-void ChaCha20::Keystream(unsigned char* c, size_t bytes)
+inline void ChaCha20Aligned::Keystream64(unsigned char* c, size_t blocks)
 {
     uint32_t x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15;
-    uint32_t j0, j1, j2, j3, j4, j5, j6, j7, j8, j9, j10, j11, j12, j13, j14, j15;
-    unsigned char *ctarget = nullptr;
-    unsigned char tmp[64];
-    unsigned int i;
-
-    if (!bytes) return;
-
-    j0 = input[0];
-    j1 = input[1];
-    j2 = input[2];
-    j3 = input[3];
-    j4 = input[4];
-    j5 = input[5];
-    j6 = input[6];
-    j7 = input[7];
-    j8 = input[8];
-    j9 = input[9];
-    j10 = input[10];
-    j11 = input[11];
-    j12 = input[12];
-    j13 = input[13];
-    j14 = input[14];
-    j15 = input[15];
+    uint32_t j4, j5, j6, j7, j8, j9, j10, j11, j12, j13, j14, j15;
+
+    if (!blocks) return;
+
+    j4 = input[0];
+    j5 = input[1];
+    j6 = input[2];
+    j7 = input[3];
+    j8 = input[4];
+    j9 = input[5];
+    j10 = input[6];
+    j11 = input[7];
+    j12 = input[8];
+    j13 = input[9];
+    j14 = input[10];
+    j15 = input[11];
 
     for (;;) {
-        if (bytes < 64) {
-            ctarget = c;
-            c = tmp;
-        }
-        x0 = j0;
-        x1 = j1;
-        x2 = j2;
-        x3 = j3;
+        x0 = 0x61707865;
+        x1 = 0x3320646e;
+        x2 = 0x79622d32;
+        x3 = 0x6b206574;
         x4 = j4;
         x5 = j5;
         x6 = j6;
@@ -134,10 +109,10 @@ void ChaCha20::Keystream(unsigned char* c, size_t bytes)
             QUARTERROUND( x3, x4, x9,x14);
         );
 
-        x0 += j0;
-        x1 += j1;
-        x2 += j2;
-        x3 += j3;
+        x0 += 0x61707865;
+        x1 += 0x3320646e;
+        x2 += 0x79622d32;
+        x3 += 0x6b206574;
         x4 += j4;
         x5 += j5;
         x6 += j6;
@@ -171,59 +146,41 @@ void ChaCha20::Keystream(unsigned char* c, size_t bytes)
         WriteLE32(c + 56, x14);
         WriteLE32(c + 60, x15);
 
-        if (bytes <= 64) {
-            if (bytes < 64) {
-                for (i = 0;i < bytes;++i) ctarget[i] = c[i];
-            }
-            input[12] = j12;
-            input[13] = j13;
+        if (blocks == 1) {
+            input[8] = j12;
+            input[9] = j13;
             return;
         }
-        bytes -= 64;
+        blocks -= 1;
         c += 64;
     }
 }
 
-void ChaCha20::Crypt(const unsigned char* m, unsigned char* c, size_t bytes)
+inline void ChaCha20Aligned::Crypt64(const unsigned char* m, unsigned char* c, size_t blocks)
 {
     uint32_t x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15;
-    uint32_t j0, j1, j2, j3, j4, j5, j6, j7, j8, j9, j10, j11, j12, j13, j14, j15;
-    unsigned char *ctarget = nullptr;
-    unsigned char tmp[64];
-    unsigned int i;
-
-    if (!bytes) return;
-
-    j0 = input[0];
-    j1 = input[1];
-    j2 = input[2];
-    j3 = input[3];
-    j4 = input[4];
-    j5 = input[5];
-    j6 = input[6];
-    j7 = input[7];
-    j8 = input[8];
-    j9 = input[9];
-    j10 = input[10];
-    j11 = input[11];
-    j12 = input[12];
-    j13 = input[13];
-    j14 = input[14];
-    j15 = input[15];
+    uint32_t j4, j5, j6, j7, j8, j9, j10, j11, j12, j13, j14, j15;
+
+    if (!blocks) return;
+
+    j4 = input[0];
+    j5 = input[1];
+    j6 = input[2];
+    j7 = input[3];
+    j8 = input[4];
+    j9 = input[5];
+    j10 = input[6];
+    j11 = input[7];
+    j12 = input[8];
+    j13 = input[9];
+    j14 = input[10];
+    j15 = input[11];
 
     for (;;) {
-        if (bytes < 64) {
-            // if m has fewer than 64 bytes available, copy m to tmp and
-            // read from tmp instead
-            for (i = 0;i < bytes;++i) tmp[i] = m[i];
-            m = tmp;
-            ctarget = c;
-            c = tmp;
-        }
-        x0 = j0;
-        x1 = j1;
-        x2 = j2;
-        x3 = j3;
+        x0 = 0x61707865;
+        x1 = 0x3320646e;
+        x2 = 0x79622d32;
+        x3 = 0x6b206574;
         x4 = j4;
         x5 = j5;
         x6 = j6;
@@ -249,10 +206,10 @@ void ChaCha20::Crypt(const unsigned char* m, unsigned char* c, size_t bytes)
             QUARTERROUND( x3, x4, x9,x14);
         );
 
-        x0 += j0;
-        x1 += j1;
-        x2 += j2;
-        x3 += j3;
+        x0 += 0x61707865;
+        x1 += 0x3320646e;
+        x2 += 0x79622d32;
+        x3 += 0x6b206574;
         x4 += j4;
         x5 += j5;
         x6 += j6;
@@ -303,16 +260,65 @@ void ChaCha20::Crypt(const unsigned char* m, unsigned char* c, size_t bytes)
         WriteLE32(c + 56, x14);
         WriteLE32(c + 60, x15);
 
-        if (bytes <= 64) {
-            if (bytes < 64) {
-                for (i = 0;i < bytes;++i) ctarget[i] = c[i];
-            }
-            input[12] = j12;
-            input[13] = j13;
+        if (blocks == 1) {
+            input[8] = j12;
+            input[9] = j13;
             return;
         }
-        bytes -= 64;
+        blocks -= 1;
         c += 64;
         m += 64;
     }
 }
+
+void ChaCha20::Keystream(unsigned char* c, size_t bytes)
+{
+    if (!bytes) return;
+    if (m_bufleft) {
+        unsigned reuse = std::min<size_t>(m_bufleft, bytes);
+        memcpy(c, m_buffer + 64 - m_bufleft, reuse);
+        m_bufleft -= reuse;
+        bytes -= reuse;
+        c += reuse;
+    }
+    if (bytes >= 64) {
+        size_t blocks = bytes / 64;
+        m_aligned.Keystream64(c, blocks);
+        c += blocks * 64;
+        bytes -= blocks * 64;
+    }
+    if (bytes) {
+        m_aligned.Keystream64(m_buffer, 1);
+        memcpy(c, m_buffer, bytes);
+        m_bufleft = 64 - bytes;
+    }
+}
+
+void ChaCha20::Crypt(const unsigned char* m, unsigned char* c, size_t bytes)
+{
+    if (!bytes) return;
+    if (m_bufleft) {
+        unsigned reuse = std::min<size_t>(m_bufleft, bytes);
+        for (unsigned i = 0; i < reuse; i++) {
+            c[i] = m[i] ^ m_buffer[64 - m_bufleft + i];
+        }
+        m_bufleft -= reuse;
+        bytes -= reuse;
+        c += reuse;
+        m += reuse;
+    }
+    if (bytes >= 64) {
+        size_t blocks = bytes / 64;
+        m_aligned.Crypt64(m, c, blocks);
+        c += blocks * 64;
+        m += blocks * 64;
+        bytes -= blocks * 64;
+    }
+    if (bytes) {
+        m_aligned.Keystream64(m_buffer, 1);
+        for (unsigned i = 0; i < bytes; i++) {
+            c[i] = m[i] ^ m_buffer[i];
+        }
+        m_bufleft = 64 - bytes;
+    }
+}
diff --git a/src/crypto/chacha20.h b/src/crypto/chacha20.h
index 624c083191..b286ef59fe 100644
--- a/src/crypto/chacha20.h
+++ b/src/crypto/chacha20.h
@@ -8,19 +8,69 @@
 #include <cstdlib>
 #include <stdint.h>
 
-/** A class for ChaCha20 256-bit stream cipher developed by Daniel J. Bernstein
-    https://cr.yp.to/chacha/chacha-20080128.pdf */
+// classes for ChaCha20 256-bit stream cipher developed by Daniel J. Bernstein
+// https://cr.yp.to/chacha/chacha-20080128.pdf */
+
+/** ChaCha20 cipher that only operates on multiples of 64 bytes. */
+class ChaCha20Aligned
+{
+private:
+    uint32_t input[12];
+
+public:
+    ChaCha20Aligned();
+
+    /** Initialize a cipher with specified 32-byte key. */
+    ChaCha20Aligned(const unsigned char* key32);
+
+    /** set 32-byte key. */
+    void SetKey32(const unsigned char* key32);
+
+    /** set the 64-bit nonce. */
+    void SetIV(uint64_t iv);
+
+    /** set the 64bit block counter (pos seeks to byte position 64*pos). */
+    void Seek64(uint64_t pos);
+
+    /** outputs the keystream of size <64*blocks> into <c> */
+    void Keystream64(unsigned char* c, size_t blocks);
+
+    /** enciphers the message <input> of length <64*blocks> and write the enciphered representation into <output>
+     *  Used for encryption and decryption (XOR)
+     */
+    void Crypt64(const unsigned char* input, unsigned char* output, size_t blocks);
+};
+
+/** Unrestricted ChaCha20 cipher. */
 class ChaCha20
 {
 private:
-    uint32_t input[16];
+    ChaCha20Aligned m_aligned;
+    unsigned char m_buffer[64] = {0};
+    unsigned m_bufleft{0};
 
 public:
-    ChaCha20();
-    ChaCha20(const unsigned char* key, size_t keylen);
-    void SetKey(const unsigned char* key, size_t keylen); //!< set key with flexible keylength; 256bit recommended */
-    void SetIV(uint64_t iv); // set the 64bit nonce
-    void Seek(uint64_t pos); // set the 64bit block counter
+    ChaCha20() = default;
+
+    /** Initialize a cipher with specified 32-byte key. */
+    ChaCha20(const unsigned char* key32) : m_aligned(key32) {}
+
+    /** set 32-byte key. */
+    void SetKey32(const unsigned char* key32)
+    {
+        m_aligned.SetKey32(key32);
+        m_bufleft = 0;
+    }
+
+    /** set the 64-bit nonce. */
+    void SetIV(uint64_t iv) { m_aligned.SetIV(iv); }
+
+    /** set the 64bit block counter (pos seeks to byte position 64*pos). */
+    void Seek64(uint64_t pos)
+    {
+        m_aligned.Seek64(pos);
+        m_bufleft = 0;
+    }
 
     /** outputs the keystream of size <bytes> into <c> */
     void Keystream(unsigned char* c, size_t bytes);
diff --git a/src/crypto/chacha_poly_aead.cpp b/src/crypto/chacha_poly_aead.cpp
index 6511f46adc..119ad6902f 100644
--- a/src/crypto/chacha_poly_aead.cpp
+++ b/src/crypto/chacha_poly_aead.cpp
@@ -36,8 +36,9 @@ ChaCha20Poly1305AEAD::ChaCha20Poly1305AEAD(const unsigned char* K_1, size_t K_1_
     assert(K_1_len == CHACHA20_POLY1305_AEAD_KEY_LEN);
     assert(K_2_len == CHACHA20_POLY1305_AEAD_KEY_LEN);
 
-    m_chacha_header.SetKey(K_1, CHACHA20_POLY1305_AEAD_KEY_LEN);
-    m_chacha_main.SetKey(K_2, CHACHA20_POLY1305_AEAD_KEY_LEN);
+    static_assert(CHACHA20_POLY1305_AEAD_KEY_LEN == 32);
+    m_chacha_header.SetKey32(K_1);
+    m_chacha_main.SetKey32(K_2);
 
     // set the cached sequence number to uint64 max which hints for an unset cache.
     // we can't hit uint64 max since the rekey rule (which resets the sequence number) is 1GB
@@ -62,7 +63,7 @@ bool ChaCha20Poly1305AEAD::Crypt(uint64_t seqnr_payload, uint64_t seqnr_aad, int
     // block counter 0 for the poly1305 key
     // use lower 32bytes for the poly1305 key
     // (throws away 32 unused bytes (upper 32) from this ChaCha20 round)
-    m_chacha_main.Seek(0);
+    m_chacha_main.Seek64(0);
     m_chacha_main.Crypt(poly_key, poly_key, sizeof(poly_key));
 
     // if decrypting, verify the tag prior to decryption
@@ -85,7 +86,7 @@ bool ChaCha20Poly1305AEAD::Crypt(uint64_t seqnr_payload, uint64_t seqnr_aad, int
     if (m_cached_aad_seqnr != seqnr_aad) {
         m_cached_aad_seqnr = seqnr_aad;
         m_chacha_header.SetIV(seqnr_aad);
-        m_chacha_header.Seek(0);
+        m_chacha_header.Seek64(0);
         m_chacha_header.Keystream(m_aad_keystream_buffer, CHACHA20_ROUND_OUTPUT);
     }
     // crypt the AAD (3 bytes message length) with given position in AAD cipher instance keystream
@@ -94,7 +95,7 @@ bool ChaCha20Poly1305AEAD::Crypt(uint64_t seqnr_payload, uint64_t seqnr_aad, int
     dest[2] = src[2] ^ m_aad_keystream_buffer[aad_pos + 2];
 
     // Set the playload ChaCha instance block counter to 1 and crypt the payload
-    m_chacha_main.Seek(1);
+    m_chacha_main.Seek64(1);
     m_chacha_main.Crypt(src + CHACHA20_POLY1305_AEAD_AAD_LEN, dest + CHACHA20_POLY1305_AEAD_AAD_LEN, src_len - CHACHA20_POLY1305_AEAD_AAD_LEN);
 
     // If encrypting, calculate and append tag
@@ -117,7 +118,7 @@ bool ChaCha20Poly1305AEAD::GetLength(uint32_t* len24_out, uint64_t seqnr_aad, in
         // we need to calculate the 64 keystream bytes since we reached a new aad sequence number
         m_cached_aad_seqnr = seqnr_aad;
         m_chacha_header.SetIV(seqnr_aad);                                         // use LE for the nonce
-        m_chacha_header.Seek(0);                                                  // block counter 0
+        m_chacha_header.Seek64(0);                                               // block counter 0
         m_chacha_header.Keystream(m_aad_keystream_buffer, CHACHA20_ROUND_OUTPUT); // write keystream to the cache
     }
 
diff --git a/src/crypto/muhash.cpp b/src/crypto/muhash.cpp
index 26f0248663..471ee6af97 100644
--- a/src/crypto/muhash.cpp
+++ b/src/crypto/muhash.cpp
@@ -299,7 +299,7 @@ Num3072 MuHash3072::ToNum3072(Span<const unsigned char> in) {
     unsigned char tmp[Num3072::BYTE_SIZE];
 
     uint256 hashed_in{(HashWriter{} << in).GetSHA256()};
-    ChaCha20(hashed_in.data(), hashed_in.size()).Keystream(tmp, Num3072::BYTE_SIZE);
+    ChaCha20Aligned(hashed_in.data()).Keystream64(tmp, Num3072::BYTE_SIZE / 64);
     Num3072 out{tmp};
 
     return out;
diff --git a/src/fs.cpp b/src/fs.cpp
index 0429b8cd0f..64411fe41f 100644
--- a/src/fs.cpp
+++ b/src/fs.cpp
@@ -60,36 +60,20 @@ FileLock::~FileLock()
     }
 }
 
-static bool IsWSL()
-{
-    struct utsname uname_data;
-    return uname(&uname_data) == 0 && std::string(uname_data.version).find("Microsoft") != std::string::npos;
-}
-
 bool FileLock::TryLock()
 {
     if (fd == -1) {
         return false;
     }
 
-    // Exclusive file locking is broken on WSL using fcntl (issue #18622)
-    // This workaround can be removed once the bug on WSL is fixed
-    static const bool is_wsl = IsWSL();
-    if (is_wsl) {
-        if (flock(fd, LOCK_EX | LOCK_NB) == -1) {
-            reason = GetErrorReason();
-            return false;
-        }
-    } else {
-        struct flock lock;
-        lock.l_type = F_WRLCK;
-        lock.l_whence = SEEK_SET;
-        lock.l_start = 0;
-        lock.l_len = 0;
-        if (fcntl(fd, F_SETLK, &lock) == -1) {
-            reason = GetErrorReason();
-            return false;
-        }
+    struct flock lock;
+    lock.l_type = F_WRLCK;
+    lock.l_whence = SEEK_SET;
+    lock.l_start = 0;
+    lock.l_len = 0;
+    if (fcntl(fd, F_SETLK, &lock) == -1) {
+        reason = GetErrorReason();
+        return false;
     }
 
     return true;
diff --git a/src/mapport.cpp b/src/mapport.cpp
index e6a473c185..84b889f22d 100644
--- a/src/mapport.cpp
+++ b/src/mapport.cpp
@@ -27,9 +27,9 @@
 #include <miniupnpc/miniupnpc.h>
 #include <miniupnpc/upnpcommands.h>
 #include <miniupnpc/upnperrors.h>
-// The minimum supported miniUPnPc API version is set to 10. This keeps compatibility
-// with Ubuntu 16.04 LTS and Debian 8 libminiupnpc-dev packages.
-static_assert(MINIUPNPC_API_VERSION >= 10, "miniUPnPc API version >= 10 assumed");
+// The minimum supported miniUPnPc API version is set to 17. This excludes
+// versions with known vulnerabilities.
+static_assert(MINIUPNPC_API_VERSION >= 17, "miniUPnPc API version >= 17 assumed");
 #endif // USE_UPNP
 
 #include <atomic>
@@ -159,11 +159,7 @@ static bool ProcessUpnp()
     char lanaddr[64];
 
     int error = 0;
-#if MINIUPNPC_API_VERSION < 14
-    devlist = upnpDiscover(2000, multicastif, minissdpdpath, 0, 0, &error);
-#else
     devlist = upnpDiscover(2000, multicastif, minissdpdpath, 0, 0, 2, &error);
-#endif
 
     struct UPNPUrls urls;
     struct IGDdatas data;
diff --git a/src/net.cpp b/src/net.cpp
index 6370833fa7..f585d88803 100644
--- a/src/net.cpp
+++ b/src/net.cpp
@@ -825,7 +825,13 @@ size_t CConnman::SocketSendData(CNode& node) const
             if (!node.m_sock) {
                 break;
             }
-            nBytes = node.m_sock->Send(reinterpret_cast<const char*>(data.data()) + node.nSendOffset, data.size() - node.nSendOffset, MSG_NOSIGNAL | MSG_DONTWAIT);
+            int flags = MSG_NOSIGNAL | MSG_DONTWAIT;
+#ifdef MSG_MORE
+            if (it + 1 != node.vSendMsg.end()) {
+                flags |= MSG_MORE;
+            }
+#endif
+            nBytes = node.m_sock->Send(reinterpret_cast<const char*>(data.data()) + node.nSendOffset, data.size() - node.nSendOffset, flags);
         }
         if (nBytes > 0) {
             node.m_last_send = GetTime<std::chrono::seconds>();
diff --git a/src/psbt.cpp b/src/psbt.cpp
index 50ccd9e2c0..fe45f2318c 100644
--- a/src/psbt.cpp
+++ b/src/psbt.cpp
@@ -132,6 +132,18 @@ void PSBTInput::FillSignatureData(SignatureData& sigdata) const
     for (const auto& [pubkey, leaf_origin] : m_tap_bip32_paths) {
         sigdata.taproot_misc_pubkeys.emplace(pubkey, leaf_origin);
     }
+    for (const auto& [hash, preimage] : ripemd160_preimages) {
+        sigdata.ripemd160_preimages.emplace(std::vector<unsigned char>(hash.begin(), hash.end()), preimage);
+    }
+    for (const auto& [hash, preimage] : sha256_preimages) {
+        sigdata.sha256_preimages.emplace(std::vector<unsigned char>(hash.begin(), hash.end()), preimage);
+    }
+    for (const auto& [hash, preimage] : hash160_preimages) {
+        sigdata.hash160_preimages.emplace(std::vector<unsigned char>(hash.begin(), hash.end()), preimage);
+    }
+    for (const auto& [hash, preimage] : hash256_preimages) {
+        sigdata.hash256_preimages.emplace(std::vector<unsigned char>(hash.begin(), hash.end()), preimage);
+    }
 }
 
 void PSBTInput::FromSignatureData(const SignatureData& sigdata)
diff --git a/src/psbt.h b/src/psbt.h
index d848c9dd49..c497584f36 100644
--- a/src/psbt.h
+++ b/src/psbt.h
@@ -1164,7 +1164,7 @@ struct PartiallySignedTransaction
 
         // Make sure that we got an unsigned tx
         if (!tx) {
-            throw std::ios_base::failure("No unsigned transcation was provided");
+            throw std::ios_base::failure("No unsigned transaction was provided");
         }
 
         // Read input data
diff --git a/src/qt/optionsmodel.cpp b/src/qt/optionsmodel.cpp
index 761351468e..7f95d527f0 100644
--- a/src/qt/optionsmodel.cpp
+++ b/src/qt/optionsmodel.cpp
@@ -59,7 +59,7 @@ static const char* SettingName(OptionsModel::OptionID option)
 }
 
 /** Call node.updateRwSetting() with Bitcoin 22.x workaround. */
-static void UpdateRwSetting(interfaces::Node& node, OptionsModel::OptionID option, const util::SettingsValue& value)
+static void UpdateRwSetting(interfaces::Node& node, OptionsModel::OptionID option, const std::string& suffix, const util::SettingsValue& value)
 {
     if (value.isNum() &&
         (option == OptionsModel::DatabaseCache ||
@@ -73,9 +73,9 @@ static void UpdateRwSetting(interfaces::Node& node, OptionsModel::OptionID optio
         // in later releases by https://github.com/bitcoin/bitcoin/pull/24498.
         // If new numeric settings are added, they can be written as numbers
         // instead of strings, because bitcoin 22.x will not try to read these.
-        node.updateRwSetting(SettingName(option), value.getValStr());
+        node.updateRwSetting(SettingName(option) + suffix, value.getValStr());
     } else {
-        node.updateRwSetting(SettingName(option), value);
+        node.updateRwSetting(SettingName(option) + suffix, value);
     }
 }
 
@@ -131,13 +131,6 @@ void OptionsModel::addOverriddenOption(const std::string &option)
 bool OptionsModel::Init(bilingual_str& error)
 {
     // Initialize display settings from stored settings.
-    m_prune_size_gb = PruneSizeGB(node().getPersistentSetting("prune"));
-    ProxySetting proxy = ParseProxyString(SettingToString(node().getPersistentSetting("proxy"), GetDefaultProxyAddress().toStdString()));
-    m_proxy_ip = proxy.ip;
-    m_proxy_port = proxy.port;
-    ProxySetting onion = ParseProxyString(SettingToString(node().getPersistentSetting("onion"), GetDefaultProxyAddress().toStdString()));
-    m_onion_ip = onion.ip;
-    m_onion_port = onion.port;
     language = QString::fromStdString(SettingToString(node().getPersistentSetting("lang"), ""));
 
     checkAndMigrate();
@@ -320,8 +313,6 @@ void OptionsModel::SetPruneTargetGB(int prune_target_gb)
     const util::SettingsValue cur_value = node().getPersistentSetting("prune");
     const util::SettingsValue new_value = PruneSetting(prune_target_gb > 0, prune_target_gb);
 
-    m_prune_size_gb = prune_target_gb;
-
     // Force setting to take effect. It is still safe to change the value at
     // this point because this function is only called after the intro screen is
     // shown, before the node starts.
@@ -334,7 +325,12 @@ void OptionsModel::SetPruneTargetGB(int prune_target_gb)
         PruneSizeGB(cur_value) != PruneSizeGB(new_value)) {
         // Call UpdateRwSetting() instead of setOption() to avoid setting
         // RestartRequired flag
-        UpdateRwSetting(node(), Prune, new_value);
+        UpdateRwSetting(node(), Prune, "", new_value);
+    }
+
+    // Keep previous pruning size, if pruning was disabled.
+    if (PruneEnabled(cur_value)) {
+        UpdateRwSetting(node(), Prune, "-prev", PruneEnabled(new_value) ? util::SettingsValue{} : cur_value);
     }
 }
 
@@ -362,9 +358,9 @@ bool OptionsModel::setData(const QModelIndex & index, const QVariant & value, in
     return successful;
 }
 
-QVariant OptionsModel::getOption(OptionID option) const
+QVariant OptionsModel::getOption(OptionID option, const std::string& suffix) const
 {
-    auto setting = [&]{ return node().getPersistentSetting(SettingName(option)); };
+    auto setting = [&]{ return node().getPersistentSetting(SettingName(option) + suffix); };
 
     QSettings settings;
     switch (option) {
@@ -391,19 +387,30 @@ QVariant OptionsModel::getOption(OptionID option) const
 
     // default proxy
     case ProxyUse:
+    case ProxyUseTor:
         return ParseProxyString(SettingToString(setting(), "")).is_set;
     case ProxyIP:
-        return m_proxy_ip;
+    case ProxyIPTor: {
+        ProxySetting proxy = ParseProxyString(SettingToString(setting(), ""));
+        if (proxy.is_set) {
+            return proxy.ip;
+        } else if (suffix.empty()) {
+            return getOption(option, "-prev");
+        } else {
+            return ParseProxyString(GetDefaultProxyAddress().toStdString()).ip;
+        }
+    }
     case ProxyPort:
-        return m_proxy_port;
-
-    // separate Tor proxy
-    case ProxyUseTor:
-        return ParseProxyString(SettingToString(setting(), "")).is_set;
-    case ProxyIPTor:
-        return m_onion_ip;
-    case ProxyPortTor:
-        return m_onion_port;
+    case ProxyPortTor: {
+        ProxySetting proxy = ParseProxyString(SettingToString(setting(), ""));
+        if (proxy.is_set) {
+            return proxy.port;
+        } else if (suffix.empty()) {
+            return getOption(option, "-prev");
+        } else {
+            return ParseProxyString(GetDefaultProxyAddress().toStdString()).port;
+        }
+    }
 
 #ifdef ENABLE_WALLET
     case SpendZeroConfChange:
@@ -428,7 +435,9 @@ QVariant OptionsModel::getOption(OptionID option) const
     case Prune:
         return PruneEnabled(setting());
     case PruneSize:
-        return m_prune_size_gb;
+        return PruneEnabled(setting()) ? PruneSizeGB(setting()) :
+               suffix.empty()          ? getOption(option, "-prev") :
+                                         DEFAULT_PRUNE_TARGET_GB;
     case DatabaseCache:
         return qlonglong(SettingToInt(setting(), nDefaultDbCache));
     case ThreadsScriptVerif:
@@ -444,10 +453,10 @@ QVariant OptionsModel::getOption(OptionID option) const
     }
 }
 
-bool OptionsModel::setOption(OptionID option, const QVariant& value)
+bool OptionsModel::setOption(OptionID option, const QVariant& value, const std::string& suffix)
 {
-    auto changed = [&] { return value.isValid() && value != getOption(option); };
-    auto update = [&](const util::SettingsValue& value) { return UpdateRwSetting(node(), option, value); };
+    auto changed = [&] { return value.isValid() && value != getOption(option, suffix); };
+    auto update = [&](const util::SettingsValue& value) { return UpdateRwSetting(node(), option, suffix, value); };
 
     bool successful = true; /* set to false on parse error */
     QSettings settings;
@@ -485,52 +494,60 @@ bool OptionsModel::setOption(OptionID option, const QVariant& value)
     // default proxy
     case ProxyUse:
         if (changed()) {
-            update(ProxyString(value.toBool(), m_proxy_ip, m_proxy_port));
-            setRestartRequired(true);
+            if (suffix.empty() && !value.toBool()) setOption(option, true, "-prev");
+            update(ProxyString(value.toBool(), getOption(ProxyIP).toString(), getOption(ProxyPort).toString()));
+            if (suffix.empty() && value.toBool()) UpdateRwSetting(node(), option, "-prev", {});
+            if (suffix.empty()) setRestartRequired(true);
         }
         break;
     case ProxyIP:
         if (changed()) {
-            m_proxy_ip = value.toString();
-            if (getOption(ProxyUse).toBool()) {
-                update(ProxyString(true, m_proxy_ip, m_proxy_port));
-                setRestartRequired(true);
+            if (suffix.empty() && !getOption(ProxyUse).toBool()) {
+                setOption(option, value, "-prev");
+            } else {
+                update(ProxyString(true, value.toString(), getOption(ProxyPort).toString()));
             }
+            if (suffix.empty() && getOption(ProxyUse).toBool()) setRestartRequired(true);
         }
         break;
     case ProxyPort:
         if (changed()) {
-            m_proxy_port = value.toString();
-            if (getOption(ProxyUse).toBool()) {
-                update(ProxyString(true, m_proxy_ip, m_proxy_port));
-                setRestartRequired(true);
+            if (suffix.empty() && !getOption(ProxyUse).toBool()) {
+                setOption(option, value, "-prev");
+            } else {
+                update(ProxyString(true, getOption(ProxyIP).toString(), value.toString()));
             }
+            if (suffix.empty() && getOption(ProxyUse).toBool()) setRestartRequired(true);
         }
         break;
 
     // separate Tor proxy
     case ProxyUseTor:
         if (changed()) {
-            update(ProxyString(value.toBool(), m_onion_ip, m_onion_port));
-            setRestartRequired(true);
+            if (suffix.empty() && !value.toBool()) setOption(option, true, "-prev");
+            update(ProxyString(value.toBool(), getOption(ProxyIPTor).toString(), getOption(ProxyPortTor).toString()));
+            if (suffix.empty() && value.toBool()) UpdateRwSetting(node(), option, "-prev", {});
+            if (suffix.empty()) setRestartRequired(true);
         }
         break;
     case ProxyIPTor:
         if (changed()) {
-            m_onion_ip = value.toString();
-            if (getOption(ProxyUseTor).toBool()) {
-                update(ProxyString(true, m_onion_ip, m_onion_port));
-                setRestartRequired(true);
+            if (suffix.empty() && !getOption(ProxyUseTor).toBool()) {
+                setOption(option, value, "-prev");
+            } else {
+                update(ProxyString(true, value.toString(), getOption(ProxyPortTor).toString()));
             }
+            if (suffix.empty() && getOption(ProxyUseTor).toBool()) setRestartRequired(true);
         }
         break;
     case ProxyPortTor:
         if (changed()) {
-            m_onion_port = value.toString();
-            if (getOption(ProxyUseTor).toBool()) {
-                update(ProxyString(true, m_onion_ip, m_onion_port));
-                setRestartRequired(true);
+            if (suffix.empty() && !getOption(ProxyUseTor).toBool()) {
+                setOption(option, value, "-prev");
+            } else {
+                update(ProxyString(true, getOption(ProxyIPTor).toString(), value.toString()));
             }
+            if (suffix.empty() && getOption(ProxyUseTor).toBool()) setRestartRequired(true);
         }
         break;
 
@@ -584,17 +601,20 @@ bool OptionsModel::setOption(OptionID option, const QVariant& value)
         break;
     case Prune:
         if (changed()) {
-            update(PruneSetting(value.toBool(), m_prune_size_gb));
-            setRestartRequired(true);
+            if (suffix.empty() && !value.toBool()) setOption(option, true, "-prev");
+            update(PruneSetting(value.toBool(), getOption(PruneSize).toInt()));
+            if (suffix.empty() && value.toBool()) UpdateRwSetting(node(), option, "-prev", {});
+            if (suffix.empty()) setRestartRequired(true);
         }
         break;
     case PruneSize:
         if (changed()) {
-            m_prune_size_gb = ParsePruneSizeGB(value);
-            if (getOption(Prune).toBool()) {
-                update(PruneSetting(true, m_prune_size_gb));
-                setRestartRequired(true);
+            if (suffix.empty() && !getOption(Prune).toBool()) {
+                setOption(option, value, "-prev");
+            } else {
+                update(PruneSetting(true, ParsePruneSizeGB(value)));
             }
+            if (suffix.empty() && getOption(Prune).toBool()) setRestartRequired(true);
         }
         break;
     case DatabaseCache:
diff --git a/src/qt/optionsmodel.h b/src/qt/optionsmodel.h
index 848966574d..a5da4dfaeb 100644
--- a/src/qt/optionsmodel.h
+++ b/src/qt/optionsmodel.h
@@ -82,8 +82,8 @@ class OptionsModel : public QAbstractListModel
     int rowCount(const QModelIndex & parent = QModelIndex()) const override;
     QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const override;
     bool setData(const QModelIndex & index, const QVariant & value, int role = Qt::EditRole) override;
-    QVariant getOption(OptionID option) const;
-    bool setOption(OptionID option, const QVariant& value);
+    QVariant getOption(OptionID option, const std::string& suffix="") const;
+    bool setOption(OptionID option, const QVariant& value, const std::string& suffix="");
     /** Updates current unit in memory, settings and emits displayUnitChanged(new_unit) signal */
     void setDisplayUnit(const QVariant& new_unit);
 
@@ -123,15 +123,6 @@ class OptionsModel : public QAbstractListModel
     bool m_enable_psbt_controls;
     bool m_mask_values;
 
-    //! In-memory settings for display. These are stored persistently by the
-    //! bitcoin node but it's also nice to store them in memory to prevent them
-    //! getting cleared when enable/disable toggles are used in the GUI.
-    int m_prune_size_gb;
-    QString m_proxy_ip;
-    QString m_proxy_port;
-    QString m_onion_ip;
-    QString m_onion_port;
-
     /* settings that were overridden by command-line */
     QString strOverriddenByCommandLine;
 
diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp
index cb8491e27a..3c69d46b7e 100644
--- a/src/qt/walletmodel.cpp
+++ b/src/qt/walletmodel.cpp
@@ -477,13 +477,6 @@ WalletModel::UnlockContext::~UnlockContext()
     }
 }
 
-void WalletModel::UnlockContext::CopyFrom(UnlockContext&& rhs)
-{
-    // Transfer context; old object no longer relocks wallet
-    *this = rhs;
-    rhs.relock = false;
-}
-
 bool WalletModel::bumpFee(uint256 hash, uint256& new_hash)
 {
     CCoinControl coin_control;
diff --git a/src/qt/walletmodel.h b/src/qt/walletmodel.h
index 604a9e03c8..17a39349f3 100644
--- a/src/qt/walletmodel.h
+++ b/src/qt/walletmodel.h
@@ -111,7 +111,7 @@ class WalletModel : public QObject
     bool setWalletLocked(bool locked, const SecureString &passPhrase=SecureString());
     bool changePassphrase(const SecureString &oldPass, const SecureString &newPass);
 
-    // RAI object for unlocking wallet, returned by requestUnlock()
+    // RAII object for unlocking wallet, returned by requestUnlock()
     class UnlockContext
     {
     public:
@@ -120,18 +120,16 @@ class WalletModel : public QObject
 
         bool isValid() const { return valid; }
 
-        // Copy constructor is disabled.
+        // Disable unused copy/move constructors/assignments explicitly.
         UnlockContext(const UnlockContext&) = delete;
-        // Move operator and constructor transfer the context
-        UnlockContext(UnlockContext&& obj) { CopyFrom(std::move(obj)); }
-        UnlockContext& operator=(UnlockContext&& rhs) { CopyFrom(std::move(rhs)); return *this; }
+        UnlockContext(UnlockContext&&) = delete;
+        UnlockContext& operator=(const UnlockContext&) = delete;
+        UnlockContext& operator=(UnlockContext&&) = delete;
+
     private:
         WalletModel *wallet;
-        bool valid;
-        mutable bool relock; // mutable, as it can be set to false by copying
-
-        UnlockContext& operator=(const UnlockContext&) = default;
-        void CopyFrom(UnlockContext&& rhs);
+        const bool valid;
+        const bool relock;
     };
 
     UnlockContext requestUnlock();
diff --git a/src/random.cpp b/src/random.cpp
index 23ea9ba6b7..5f50c001cd 100644
--- a/src/random.cpp
+++ b/src/random.cpp
@@ -599,18 +599,15 @@ uint256 GetRandHash() noexcept
 void FastRandomContext::RandomSeed()
 {
     uint256 seed = GetRandHash();
-    rng.SetKey(seed.begin(), 32);
+    rng.SetKey32(seed.begin());
     requires_seed = false;
 }
 
 uint256 FastRandomContext::rand256() noexcept
 {
-    if (bytebuf_size < 32) {
-        FillByteBuffer();
-    }
+    if (requires_seed) RandomSeed();
     uint256 ret;
-    memcpy(ret.begin(), bytebuf + 64 - bytebuf_size, 32);
-    bytebuf_size -= 32;
+    rng.Keystream(ret.data(), ret.size());
     return ret;
 }
 
@@ -624,9 +621,9 @@ std::vector<unsigned char> FastRandomContext::randbytes(size_t len)
     return ret;
 }
 
-FastRandomContext::FastRandomContext(const uint256& seed) noexcept : requires_seed(false), bytebuf_size(0), bitbuf_size(0)
+FastRandomContext::FastRandomContext(const uint256& seed) noexcept : requires_seed(false), bitbuf_size(0)
 {
-    rng.SetKey(seed.begin(), 32);
+    rng.SetKey32(seed.begin());
 }
 
 bool Random_SanityCheck()
@@ -675,25 +672,22 @@ bool Random_SanityCheck()
     return true;
 }
 
-FastRandomContext::FastRandomContext(bool fDeterministic) noexcept : requires_seed(!fDeterministic), bytebuf_size(0), bitbuf_size(0)
+FastRandomContext::FastRandomContext(bool fDeterministic) noexcept : requires_seed(!fDeterministic), bitbuf_size(0)
 {
     if (!fDeterministic) {
         return;
     }
     uint256 seed;
-    rng.SetKey(seed.begin(), 32);
+    rng.SetKey32(seed.begin());
 }
 
 FastRandomContext& FastRandomContext::operator=(FastRandomContext&& from) noexcept
 {
     requires_seed = from.requires_seed;
     rng = from.rng;
-    std::copy(std::begin(from.bytebuf), std::end(from.bytebuf), std::begin(bytebuf));
-    bytebuf_size = from.bytebuf_size;
     bitbuf = from.bitbuf;
     bitbuf_size = from.bitbuf_size;
     from.requires_seed = true;
-    from.bytebuf_size = 0;
     from.bitbuf_size = 0;
     return *this;
 }
diff --git a/src/random.h b/src/random.h
index bb8b5539a3..49c0dff5bf 100644
--- a/src/random.h
+++ b/src/random.h
@@ -146,23 +146,11 @@ class FastRandomContext
     bool requires_seed;
     ChaCha20 rng;
 
-    unsigned char bytebuf[64];
-    int bytebuf_size;
-
     uint64_t bitbuf;
     int bitbuf_size;
 
     void RandomSeed();
 
-    void FillByteBuffer()
-    {
-        if (requires_seed) {
-            RandomSeed();
-        }
-        rng.Keystream(bytebuf, sizeof(bytebuf));
-        bytebuf_size = sizeof(bytebuf);
-    }
-
     void FillBitBuffer()
     {
         bitbuf = rand64();
@@ -186,10 +174,10 @@ class FastRandomContext
     /** Generate a random 64-bit integer. */
     uint64_t rand64() noexcept
     {
-        if (bytebuf_size < 8) FillByteBuffer();
-        uint64_t ret = ReadLE64(bytebuf + 64 - bytebuf_size);
-        bytebuf_size -= 8;
-        return ret;
+        if (requires_seed) RandomSeed();
+        unsigned char buf[8];
+        rng.Keystream(buf, 8);
+        return ReadLE64(buf);
     }
 
     /** Generate a random (bits)-bit integer. */
diff --git a/src/rpc/rawtransaction_util.cpp b/src/rpc/rawtransaction_util.cpp
index 15b8e1dcd0..3ba930f84f 100644
--- a/src/rpc/rawtransaction_util.cpp
+++ b/src/rpc/rawtransaction_util.cpp
@@ -21,12 +21,8 @@
 #include <util/strencodings.h>
 #include <util/translation.h>
 
-CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniValue& outputs_in, const UniValue& locktime, std::optional<bool> rbf)
+void AddInputs(CMutableTransaction& rawTx, const UniValue& inputs_in, std::optional<bool> rbf)
 {
-    if (outputs_in.isNull()) {
-        throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, output argument must be non-null");
-    }
-
     UniValue inputs;
     if (inputs_in.isNull()) {
         inputs = UniValue::VARR;
@@ -34,18 +30,6 @@ CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniVal
         inputs = inputs_in.get_array();
     }
 
-    const bool outputs_is_obj = outputs_in.isObject();
-    UniValue outputs = outputs_is_obj ? outputs_in.get_obj() : outputs_in.get_array();
-
-    CMutableTransaction rawTx;
-
-    if (!locktime.isNull()) {
-        int64_t nLockTime = locktime.getInt<int64_t>();
-        if (nLockTime < 0 || nLockTime > LOCKTIME_MAX)
-            throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, locktime out of range");
-        rawTx.nLockTime = nLockTime;
-    }
-
     for (unsigned int idx = 0; idx < inputs.size(); idx++) {
         const UniValue& input = inputs[idx];
         const UniValue& o = input.get_obj();
@@ -84,6 +68,16 @@ CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniVal
 
         rawTx.vin.push_back(in);
     }
+}
+
+void AddOutputs(CMutableTransaction& rawTx, const UniValue& outputs_in)
+{
+    if (outputs_in.isNull()) {
+        throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, output argument must be non-null");
+    }
+
+    const bool outputs_is_obj = outputs_in.isObject();
+    UniValue outputs = outputs_is_obj ? outputs_in.get_obj() : outputs_in.get_array();
 
     if (!outputs_is_obj) {
         // Translate array of key-value pairs into dict
@@ -132,6 +126,21 @@ CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniVal
             rawTx.vout.push_back(out);
         }
     }
+}
+
+CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniValue& outputs_in, const UniValue& locktime, std::optional<bool> rbf)
+{
+    CMutableTransaction rawTx;
+
+    if (!locktime.isNull()) {
+        int64_t nLockTime = locktime.getInt<int64_t>();
+        if (nLockTime < 0 || nLockTime > LOCKTIME_MAX)
+            throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, locktime out of range");
+        rawTx.nLockTime = nLockTime;
+    }
+
+    AddInputs(rawTx, inputs_in, rbf);
+    AddOutputs(rawTx, outputs_in);
 
     if (rbf.has_value() && rbf.value() && rawTx.vin.size() > 0 && !SignalsOptInRBF(CTransaction(rawTx))) {
         throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter combination: Sequence number(s) contradict replaceable option");
diff --git a/src/rpc/rawtransaction_util.h b/src/rpc/rawtransaction_util.h
index 0c3823bc1e..a863432b7a 100644
--- a/src/rpc/rawtransaction_util.h
+++ b/src/rpc/rawtransaction_util.h
@@ -38,6 +38,13 @@ void SignTransactionResultToJSON(CMutableTransaction& mtx, bool complete, const
   */
 void ParsePrevouts(const UniValue& prevTxsUnival, FillableSigningProvider* keystore, std::map<COutPoint, Coin>& coins);
 
+
+/** Normalize univalue-represented inputs and add them to the transaction */
+void AddInputs(CMutableTransaction& rawTx, const UniValue& inputs_in, bool rbf);
+
+/** Normalize univalue-represented outputs and add them to the transaction */
+void AddOutputs(CMutableTransaction& rawTx, const UniValue& outputs_in);
+
 /** Create a transaction from univalue parameters */
 CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniValue& outputs_in, const UniValue& locktime, std::optional<bool> rbf);
 
diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp
index 8991bda340..857fee1818 100644
--- a/src/script/descriptor.cpp
+++ b/src/script/descriptor.cpp
@@ -1014,7 +1014,7 @@ class MiniscriptDescriptor final : public DescriptorImpl
         return false;
     }
 
-    bool IsSolvable() const override { return false; } // For now, mark these descriptors as non-solvable (as we don't have signing logic for them).
+    bool IsSolvable() const override { return true; }
     bool IsSingleType() const final { return true; }
 };
 
diff --git a/src/script/descriptor.h b/src/script/descriptor.h
index cb3b366acf..39b1a37f9a 100644
--- a/src/script/descriptor.h
+++ b/src/script/descriptor.h
@@ -35,7 +35,7 @@ class DescriptorCache {
     /** Retrieve a cached parent xpub
      *
      * @param[in] key_exp_pos Position of the key expression within the descriptor
-     * @param[in] xpub The CExtPubKey to get from cache
+     * @param[out] xpub The CExtPubKey to get from cache
      */
     bool GetCachedParentExtPubKey(uint32_t key_exp_pos, CExtPubKey& xpub) const;
     /** Cache an xpub derived at an index
@@ -49,7 +49,7 @@ class DescriptorCache {
      *
      * @param[in] key_exp_pos Position of the key expression within the descriptor
      * @param[in] der_index Derivation index of the xpub
-     * @param[in] xpub The CExtPubKey to get from cache
+     * @param[out] xpub The CExtPubKey to get from cache
      */
     bool GetCachedDerivedExtPubKey(uint32_t key_exp_pos, uint32_t der_index, CExtPubKey& xpub) const;
     /** Cache a last hardened xpub
@@ -61,7 +61,7 @@ class DescriptorCache {
     /** Retrieve a cached last hardened xpub
      *
      * @param[in] key_exp_pos Position of the key expression within the descriptor
-     * @param[in] xpub The CExtPubKey to get from cache
+     * @param[out] xpub The CExtPubKey to get from cache
      */
     bool GetCachedLastHardenedExtPubKey(uint32_t key_exp_pos, CExtPubKey& xpub) const;
 
diff --git a/src/script/miniscript.cpp b/src/script/miniscript.cpp
index 5e471cbe89..3937638cf8 100644
--- a/src/script/miniscript.cpp
+++ b/src/script/miniscript.cpp
@@ -172,8 +172,8 @@ Type ComputeType(Fragment fragment, Type x, Type y, Type z, const std::vector<Ty
             (y & "B"_mst).If(x << "Bdu"_mst) | // B=B_y*B_x*d_x*u_x
             (x & "o"_mst).If(y << "z"_mst) | // o=o_x*z_y
             (x & y & "m"_mst).If(x << "e"_mst && (x | y) << "s"_mst) | // m=m_x*m_y*e_x*(s_x+s_y)
-            (x & y & "zes"_mst) | // z=z_x*z_y, e=e_x*e_y, s=s_x*s_y
-            (y & "ufd"_mst) | // u=u_y, f=f_y, d=d_y
+            (x & y & "zs"_mst) | // z=z_x*z_y, s=s_x*s_y
+            (y & "ufde"_mst) | // u=u_y, f=f_y, d=d_y, e=e_y
             "x"_mst | // x
             ((x | y) & "ghij"_mst) | // g=g_x+g_y, h=h_x+h_y, i=i_x+i_y, j=j_x+j_y
             (x & y & "k"_mst); // k=k_x*k_y
@@ -201,7 +201,7 @@ Type ComputeType(Fragment fragment, Type x, Type y, Type z, const std::vector<Ty
             (y & z & "u"_mst) | // u=u_y*u_z
             (z & "f"_mst).If((x << "s"_mst) || (y << "f"_mst)) | // f=(s_x+f_y)*f_z
             (z & "d"_mst) | // d=d_z
-            (x & z & "e"_mst).If(x << "s"_mst || y << "f"_mst) | // e=e_x*e_z*(s_x+f_y)
+            (z & "e"_mst).If(x << "s"_mst || y << "f"_mst) | // e=e_z*(s_x+f_y)
             (x & y & z & "m"_mst).If(x << "e"_mst && (x | y | z) << "s"_mst) | // m=m_x*m_y*m_z*e_x*(s_x+s_y+s_z)
             (z & (x | y) & "s"_mst) | // s=s_z*(s_x+s_y)
             "x"_mst | // x
@@ -279,6 +279,76 @@ size_t ComputeScriptLen(Fragment fragment, Type sub0typ, size_t subsize, uint32_
     assert(false);
 }
 
+InputStack& InputStack::SetAvailable(Availability avail) {
+    available = avail;
+    if (avail == Availability::NO) {
+        stack.clear();
+        size = std::numeric_limits<size_t>::max();
+        has_sig = false;
+        malleable = false;
+        non_canon = false;
+    }
+    return *this;
+}
+
+InputStack& InputStack::SetWithSig() {
+    has_sig = true;
+    return *this;
+}
+
+InputStack& InputStack::SetNonCanon() {
+    non_canon = true;
+    return *this;
+}
+
+InputStack& InputStack::SetMalleable(bool x) {
+    malleable = x;
+    return *this;
+}
+
+InputStack operator+(InputStack a, InputStack b) {
+    a.stack = Cat(std::move(a.stack), std::move(b.stack));
+    if (a.available != Availability::NO && b.available != Availability::NO) a.size += b.size;
+    a.has_sig |= b.has_sig;
+    a.malleable |= b.malleable;
+    a.non_canon |= b.non_canon;
+    if (a.available == Availability::NO || b.available == Availability::NO) {
+        a.SetAvailable(Availability::NO);
+    } else if (a.available == Availability::MAYBE || b.available == Availability::MAYBE) {
+        a.SetAvailable(Availability::MAYBE);
+    }
+    return a;
+}
+
+InputStack operator|(InputStack a, InputStack b) {
+    // If only one is invalid, pick the other one. If both are invalid, pick an arbitrary one.
+    if (a.available == Availability::NO) return b;
+    if (b.available == Availability::NO) return a;
+    // If only one of the solutions has a signature, we must pick the other one.
+    if (!a.has_sig && b.has_sig) return a;
+    if (!b.has_sig && a.has_sig) return b;
+    if (!a.has_sig && !b.has_sig) {
+        // If neither solution requires a signature, the result is inevitably malleable.
+        a.malleable = true;
+        b.malleable = true;
+    } else {
+        // If both options require a signature, prefer the non-malleable one.
+        if (b.malleable && !a.malleable) return a;
+        if (a.malleable && !b.malleable) return b;
+    }
+    // Between two malleable or two non-malleable solutions, pick the smaller one between
+    // YESes, and the bigger ones between MAYBEs. Prefer YES over MAYBE.
+    if (a.available == Availability::YES && b.available == Availability::YES) {
+        return std::move(a.size <= b.size ? a : b);
+    } else if (a.available == Availability::MAYBE && b.available == Availability::MAYBE) {
+        return std::move(a.size >= b.size ? a : b);
+    } else if (a.available == Availability::YES) {
+        return a;
+    } else {
+        return b;
+    }
+}
+
 std::optional<std::vector<Opcode>> DecomposeScript(const CScript& script)
 {
     std::vector<Opcode> out;
diff --git a/src/script/miniscript.h b/src/script/miniscript.h
index 3a3f724f03..c42b530c4d 100644
--- a/src/script/miniscript.h
+++ b/src/script/miniscript.h
@@ -223,6 +223,11 @@ enum class Fragment {
     // WRAP_U(X) is represented as OR_I(X,0)
 };
 
+enum class Availability {
+    NO,
+    YES,
+    MAYBE,
+};
 
 namespace internal {
 
@@ -235,6 +240,62 @@ size_t ComputeScriptLen(Fragment fragment, Type sub0typ, size_t subsize, uint32_
 //! A helper sanitizer/checker for the output of CalcType.
 Type SanitizeType(Type x);
 
+//! An object representing a sequence of witness stack elements.
+struct InputStack {
+    /** Whether this stack is valid for its intended purpose (satisfaction or dissatisfaction of a Node).
+     *  The MAYBE value is used for size estimation, when keys/preimages may actually be unavailable,
+     *  but may be available at signing time. This makes the InputStack structure and signing logic,
+     *  filled with dummy signatures/preimages usable for witness size estimation.
+     */
+    Availability available = Availability::YES;
+    //! Whether this stack contains a digital signature.
+    bool has_sig = false;
+    //! Whether this stack is malleable (can be turned into an equally valid other stack by a third party).
+    bool malleable = false;
+    //! Whether this stack is non-canonical (using a construction known to be unnecessary for satisfaction).
+    //! Note that this flag does not affect the satisfaction algorithm; it is only used for sanity checking.
+    bool non_canon = false;
+    //! Serialized witness size.
+    size_t size = 0;
+    //! Data elements.
+    std::vector<std::vector<unsigned char>> stack;
+    //! Construct an empty stack (valid).
+    InputStack() {}
+    //! Construct a valid single-element stack (with an element up to 75 bytes).
+    InputStack(std::vector<unsigned char> in) : size(in.size() + 1), stack(Vector(std::move(in))) {}
+    //! Change availability
+    InputStack& SetAvailable(Availability avail);
+    //! Mark this input stack as having a signature.
+    InputStack& SetWithSig();
+    //! Mark this input stack as non-canonical (known to not be necessary in non-malleable satisfactions).
+    InputStack& SetNonCanon();
+    //! Mark this input stack as malleable.
+    InputStack& SetMalleable(bool x = true);
+    //! Concatenate two input stacks.
+    friend InputStack operator+(InputStack a, InputStack b);
+    //! Choose between two potential input stacks.
+    friend InputStack operator|(InputStack a, InputStack b);
+};
+
+/** A stack consisting of a single zero-length element (interpreted as 0 by the script interpreter in numeric context). */
+static const auto ZERO = InputStack(std::vector<unsigned char>());
+/** A stack consisting of a single malleable 32-byte 0x0000...0000 element (for dissatisfying hash challenges). */
+static const auto ZERO32 = InputStack(std::vector<unsigned char>(32, 0)).SetMalleable();
+/** A stack consisting of a single 0x01 element (interpreted as 1 by the script interpreted in numeric context). */
+static const auto ONE = InputStack(Vector((unsigned char)1));
+/** The empty stack. */
+static const auto EMPTY = InputStack();
+/** A stack representing the lack of any (dis)satisfactions. */
+static const auto INVALID = InputStack().SetAvailable(Availability::NO);
+
+//! A pair of a satisfaction and a dissatisfaction InputStack.
+struct InputResult {
+    InputStack nsat, sat;
+
+    template<typename A, typename B>
+    InputResult(A&& in_nsat, B&& in_sat) : nsat(std::forward<A>(in_nsat)), sat(std::forward<B>(in_sat)) {}
+};
+
 //! Class whose objects represent the maximum of a list of integers.
 template<typename I>
 struct MaxInt {
@@ -785,6 +846,226 @@ struct Node {
         assert(false);
     }
 
+    template<typename Ctx>
+    internal::InputResult ProduceInput(const Ctx& ctx) const {
+        using namespace internal;
+
+        // Internal function which is invoked for every tree node, constructing satisfaction/dissatisfactions
+        // given those of its subnodes.
+        auto helper = [&ctx](const Node& node, Span<InputResult> subres) -> InputResult {
+            switch (node.fragment) {
+                case Fragment::PK_K: {
+                    std::vector<unsigned char> sig;
+                    Availability avail = ctx.Sign(node.keys[0], sig);
+                    return {ZERO, InputStack(std::move(sig)).SetWithSig().SetAvailable(avail)};
+                }
+                case Fragment::PK_H: {
+                    std::vector<unsigned char> key = ctx.ToPKBytes(node.keys[0]), sig;
+                    Availability avail = ctx.Sign(node.keys[0], sig);
+                    return {ZERO + InputStack(key), (InputStack(std::move(sig)).SetWithSig() + InputStack(key)).SetAvailable(avail)};
+                }
+                case Fragment::MULTI: {
+                    // sats[j] represents the best stack containing j valid signatures (out of the first i keys).
+                    // In the loop below, these stacks are built up using a dynamic programming approach.
+                    // sats[0] starts off being {0}, due to the CHECKMULTISIG bug that pops off one element too many.
+                    std::vector<InputStack> sats = Vector(ZERO);
+                    for (size_t i = 0; i < node.keys.size(); ++i) {
+                        std::vector<unsigned char> sig;
+                        Availability avail = ctx.Sign(node.keys[i], sig);
+                        // Compute signature stack for just the i'th key.
+                        auto sat = InputStack(std::move(sig)).SetWithSig().SetAvailable(avail);
+                        // Compute the next sats vector: next_sats[0] is a copy of sats[0] (no signatures). All further
+                        // next_sats[j] are equal to either the existing sats[j], or sats[j-1] plus a signature for the
+                        // current (i'th) key. The very last element needs all signatures filled.
+                        std::vector<InputStack> next_sats;
+                        next_sats.push_back(sats[0]);
+                        for (size_t j = 1; j < sats.size(); ++j) next_sats.push_back(sats[j] | (std::move(sats[j - 1]) + sat));
+                        next_sats.push_back(std::move(sats[sats.size() - 1]) + std::move(sat));
+                        // Switch over.
+                        sats = std::move(next_sats);
+                    }
+                    // The dissatisfaction consists of k+1 stack elements all equal to 0.
+                    InputStack nsat = ZERO;
+                    for (size_t i = 0; i < node.k; ++i) nsat = std::move(nsat) + ZERO;
+                    assert(node.k <= sats.size());
+                    return {std::move(nsat), std::move(sats[node.k])};
+                }
+                case Fragment::THRESH: {
+                    // sats[k] represents the best stack that satisfies k out of the *last* i subexpressions.
+                    // In the loop below, these stacks are built up using a dynamic programming approach.
+                    // sats[0] starts off empty.
+                    std::vector<InputStack> sats = Vector(EMPTY);
+                    for (size_t i = 0; i < subres.size(); ++i) {
+                        // Introduce an alias for the i'th last satisfaction/dissatisfaction.
+                        auto& res = subres[subres.size() - i - 1];
+                        // Compute the next sats vector: next_sats[0] is sats[0] plus res.nsat (thus containing all dissatisfactions
+                        // so far. next_sats[j] is either sats[j] + res.nsat (reusing j earlier satisfactions) or sats[j-1] + res.sat
+                        // (reusing j-1 earlier satisfactions plus a new one). The very last next_sats[j] is all satisfactions.
+                        std::vector<InputStack> next_sats;
+                        next_sats.push_back(sats[0] + res.nsat);
+                        for (size_t j = 1; j < sats.size(); ++j) next_sats.push_back((sats[j] + res.nsat) | (std::move(sats[j - 1]) + res.sat));
+                        next_sats.push_back(std::move(sats[sats.size() - 1]) + std::move(res.sat));
+                        // Switch over.
+                        sats = std::move(next_sats);
+                    }
+                    // At this point, sats[k].sat is the best satisfaction for the overall thresh() node. The best dissatisfaction
+                    // is computed by gathering all sats[i].nsat for i != k.
+                    InputStack nsat = INVALID;
+                    for (size_t i = 0; i < sats.size(); ++i) {
+                        // i==k is the satisfaction; i==0 is the canonical dissatisfaction;
+                        // the rest are non-canonical (a no-signature dissatisfaction - the i=0
+                        // form - is always available) and malleable (due to overcompleteness).
+                        // Marking the solutions malleable here is not strictly necessary, as they
+                        // should already never be picked in non-malleable solutions due to the
+                        // availability of the i=0 form.
+                        if (i != 0 && i != node.k) sats[i].SetMalleable().SetNonCanon();
+                        // Include all dissatisfactions (even these non-canonical ones) in nsat.
+                        if (i != node.k) nsat = std::move(nsat) | std::move(sats[i]);
+                    }
+                    assert(node.k <= sats.size());
+                    return {std::move(nsat), std::move(sats[node.k])};
+                }
+                case Fragment::OLDER: {
+                    return {INVALID, ctx.CheckOlder(node.k) ? EMPTY : INVALID};
+                }
+                case Fragment::AFTER: {
+                    return {INVALID, ctx.CheckAfter(node.k) ? EMPTY : INVALID};
+                }
+                case Fragment::SHA256: {
+                    std::vector<unsigned char> preimage;
+                    Availability avail = ctx.SatSHA256(node.data, preimage);
+                    return {ZERO32, InputStack(std::move(preimage)).SetAvailable(avail)};
+                }
+                case Fragment::RIPEMD160: {
+                    std::vector<unsigned char> preimage;
+                    Availability avail = ctx.SatRIPEMD160(node.data, preimage);
+                    return {ZERO32, InputStack(std::move(preimage)).SetAvailable(avail)};
+                }
+                case Fragment::HASH256: {
+                    std::vector<unsigned char> preimage;
+                    Availability avail = ctx.SatHASH256(node.data, preimage);
+                    return {ZERO32, InputStack(std::move(preimage)).SetAvailable(avail)};
+                }
+                case Fragment::HASH160: {
+                    std::vector<unsigned char> preimage;
+                    Availability avail = ctx.SatHASH160(node.data, preimage);
+                    return {ZERO32, InputStack(std::move(preimage)).SetAvailable(avail)};
+                }
+                case Fragment::AND_V: {
+                    auto& x = subres[0], &y = subres[1];
+                    // As the dissatisfaction here only consist of a single option, it doesn't
+                    // actually need to be listed (it's not required for reasoning about malleability of
+                    // other options), and is never required (no valid miniscript relies on the ability
+                    // to satisfy the type V left subexpression). It's still listed here for
+                    // completeness, as a hypothetical (not currently implemented) satisfier that doesn't
+                    // care about malleability might in some cases prefer it still.
+                    return {(y.nsat + x.sat).SetNonCanon(), y.sat + x.sat};
+                }
+                case Fragment::AND_B: {
+                    auto& x = subres[0], &y = subres[1];
+                    // Note that it is not strictly necessary to mark the 2nd and 3rd dissatisfaction here
+                    // as malleable. While they are definitely malleable, they are also non-canonical due
+                    // to the guaranteed existence of a no-signature other dissatisfaction (the 1st)
+                    // option. Because of that, the 2nd and 3rd option will never be chosen, even if they
+                    // weren't marked as malleable.
+                    return {(y.nsat + x.nsat) | (y.sat + x.nsat).SetMalleable().SetNonCanon() | (y.nsat + x.sat).SetMalleable().SetNonCanon(), y.sat + x.sat};
+                }
+                case Fragment::OR_B: {
+                    auto& x = subres[0], &z = subres[1];
+                    // The (sat(Z) sat(X)) solution is overcomplete (attacker can change either into dsat).
+                    return {z.nsat + x.nsat, (z.nsat + x.sat) | (z.sat + x.nsat) | (z.sat + x.sat).SetMalleable().SetNonCanon()};
+                }
+                case Fragment::OR_C: {
+                    auto& x = subres[0], &z = subres[1];
+                    return {INVALID, std::move(x.sat) | (z.sat + x.nsat)};
+                }
+                case Fragment::OR_D: {
+                    auto& x = subres[0], &z = subres[1];
+                    return {z.nsat + x.nsat, std::move(x.sat) | (z.sat + x.nsat)};
+                }
+                case Fragment::OR_I: {
+                    auto& x = subres[0], &z = subres[1];
+                    return {(x.nsat + ONE) | (z.nsat + ZERO), (x.sat + ONE) | (z.sat + ZERO)};
+                }
+                case Fragment::ANDOR: {
+                    auto& x = subres[0], &y = subres[1], &z = subres[2];
+                    return {(y.nsat + x.sat).SetNonCanon() | (z.nsat + x.nsat), (y.sat + x.sat) | (z.sat + x.nsat)};
+                }
+                case Fragment::WRAP_A:
+                case Fragment::WRAP_S:
+                case Fragment::WRAP_C:
+                case Fragment::WRAP_N:
+                    return std::move(subres[0]);
+                case Fragment::WRAP_D: {
+                    auto &x = subres[0];
+                    return {ZERO, x.sat + ONE};
+                }
+                case Fragment::WRAP_J: {
+                    auto &x = subres[0];
+                    // If a dissatisfaction with a nonzero top stack element exists, an alternative dissatisfaction exists.
+                    // As the dissatisfaction logic currently doesn't keep track of this nonzeroness property, and thus even
+                    // if a dissatisfaction with a top zero element is found, we don't know whether another one with a
+                    // nonzero top stack element exists. Make the conservative assumption that whenever the subexpression is weakly
+                    // dissatisfiable, this alternative dissatisfaction exists and leads to malleability.
+                    return {InputStack(ZERO).SetMalleable(x.nsat.available != Availability::NO && !x.nsat.has_sig), std::move(x.sat)};
+                }
+                case Fragment::WRAP_V: {
+                    auto &x = subres[0];
+                    return {INVALID, std::move(x.sat)};
+                }
+                case Fragment::JUST_0: return {EMPTY, INVALID};
+                case Fragment::JUST_1: return {INVALID, EMPTY};
+            }
+            assert(false);
+            return {INVALID, INVALID};
+        };
+
+        auto tester = [&helper](const Node& node, Span<InputResult> subres) -> InputResult {
+            auto ret = helper(node, subres);
+
+            // Do a consistency check between the satisfaction code and the type checker
+            // (the actual satisfaction code in ProduceInputHelper does not use GetType)
+
+            // For 'z' nodes, available satisfactions/dissatisfactions must have stack size 0.
+            if (node.GetType() << "z"_mst && ret.nsat.available != Availability::NO) assert(ret.nsat.stack.size() == 0);
+            if (node.GetType() << "z"_mst && ret.sat.available != Availability::NO) assert(ret.sat.stack.size() == 0);
+
+            // For 'o' nodes, available satisfactions/dissatisfactions must have stack size 1.
+            if (node.GetType() << "o"_mst && ret.nsat.available != Availability::NO) assert(ret.nsat.stack.size() == 1);
+            if (node.GetType() << "o"_mst && ret.sat.available != Availability::NO) assert(ret.sat.stack.size() == 1);
+
+            // For 'n' nodes, available satisfactions/dissatisfactions must have stack size 1 or larger. For satisfactions,
+            // the top element cannot be 0.
+            if (node.GetType() << "n"_mst && ret.sat.available != Availability::NO) assert(ret.sat.stack.size() >= 1);
+            if (node.GetType() << "n"_mst && ret.nsat.available != Availability::NO) assert(ret.nsat.stack.size() >= 1);
+            if (node.GetType() << "n"_mst && ret.sat.available != Availability::NO) assert(!ret.sat.stack.back().empty());
+
+            // For 'd' nodes, a dissatisfaction must exist, and they must not need a signature. If it is non-malleable,
+            // it must be canonical.
+            if (node.GetType() << "d"_mst) assert(ret.nsat.available != Availability::NO);
+            if (node.GetType() << "d"_mst) assert(!ret.nsat.has_sig);
+            if (node.GetType() << "d"_mst && !ret.nsat.malleable) assert(!ret.nsat.non_canon);
+
+            // For 'f'/'s' nodes, dissatisfactions/satisfactions must have a signature.
+            if (node.GetType() << "f"_mst && ret.nsat.available != Availability::NO) assert(ret.nsat.has_sig);
+            if (node.GetType() << "s"_mst && ret.sat.available != Availability::NO) assert(ret.sat.has_sig);
+
+            // For non-malleable 'e' nodes, a non-malleable dissatisfaction must exist.
+            if (node.GetType() << "me"_mst) assert(ret.nsat.available != Availability::NO);
+            if (node.GetType() << "me"_mst) assert(!ret.nsat.malleable);
+
+            // For 'm' nodes, if a satisfaction exists, it must be non-malleable.
+            if (node.GetType() << "m"_mst && ret.sat.available != Availability::NO) assert(!ret.sat.malleable);
+
+            // If a non-malleable satisfaction exists, it must be canonical.
+            if (ret.sat.available != Availability::NO && !ret.sat.malleable) assert(!ret.sat.non_canon);
+
+            return ret;
+        };
+
+        return TreeEval<InputResult>(tester);
+    }
+
 public:
     /** Update duplicate key information in this Node.
      *
@@ -877,6 +1158,47 @@ struct Node {
         });
     }
 
+    //! Determine whether a Miniscript node is satisfiable. fn(node) will be invoked for all
+    //! key, time, and hashing nodes, and should return their satisfiability.
+    template<typename F>
+    bool IsSatisfiable(F fn) const
+    {
+        // TreeEval() doesn't support bool as NodeType, so use int instead.
+        return TreeEval<int>([&fn](const Node& node, Span<int> subs) -> bool {
+            switch (node.fragment) {
+                case Fragment::JUST_0:
+                    return false;
+                case Fragment::JUST_1:
+                    return true;
+                case Fragment::PK_K:
+                case Fragment::PK_H:
+                case Fragment::MULTI:
+                case Fragment::AFTER:
+                case Fragment::OLDER:
+                case Fragment::HASH256:
+                case Fragment::HASH160:
+                case Fragment::SHA256:
+                case Fragment::RIPEMD160:
+                    return bool{fn(node)};
+                case Fragment::ANDOR:
+                    return (subs[0] && subs[1]) || subs[2];
+                case Fragment::AND_V:
+                case Fragment::AND_B:
+                    return subs[0] && subs[1];
+                case Fragment::OR_B:
+                case Fragment::OR_C:
+                case Fragment::OR_D:
+                case Fragment::OR_I:
+                    return subs[0] || subs[1];
+                case Fragment::THRESH:
+                    return std::count(subs.begin(), subs.end(), true) >= node.k;
+                default: // wrappers
+                    assert(subs.size() == 1);
+                    return subs[0];
+            }
+        });
+    }
+
     //! Check whether this node is valid at all.
     bool IsValid() const { return !(GetType() == ""_mst) && ScriptSize() <= MAX_STANDARD_P2WSH_SCRIPT_SIZE; }
 
@@ -904,6 +1226,18 @@ struct Node {
     //! Check whether this node is safe as a script on its own.
     bool IsSane() const { return IsValidTopLevel() && IsSaneSubexpression() && NeedsSignature(); }
 
+    //! Produce a witness for this script, if possible and given the information available in the context.
+    //! The non-malleable satisfaction is guaranteed to be valid if it exists, and ValidSatisfaction()
+    //! is true. If IsSane() holds, this satisfaction is guaranteed to succeed in case the node's
+    //! conditions are satisfied (private keys and hash preimages available, locktimes satsified).
+    template<typename Ctx>
+    Availability Satisfy(const Ctx& ctx, std::vector<std::vector<unsigned char>>& stack, bool nonmalleable = true) const {
+        auto ret = ProduceInput(ctx);
+        if (nonmalleable && (ret.sat.malleable || !ret.sat.has_sig)) return Availability::NO;
+        stack = std::move(ret.sat.stack);
+        return ret.sat.available;
+    }
+
     //! Equality testing.
     bool operator==(const Node<Key>& arg) const { return Compare(*this, arg) == 0; }
 
diff --git a/src/script/sign.cpp b/src/script/sign.cpp
index 70df9ee62c..85589fe86b 100644
--- a/src/script/sign.cpp
+++ b/src/script/sign.cpp
@@ -10,6 +10,7 @@
 #include <policy/policy.h>
 #include <primitives/transaction.h>
 #include <script/keyorigin.h>
+#include <script/miniscript.h>
 #include <script/signingprovider.h>
 #include <script/standard.h>
 #include <uint256.h>
@@ -380,6 +381,92 @@ static CScript PushAll(const std::vector<valtype>& values)
     return result;
 }
 
+template<typename M, typename K, typename V>
+miniscript::Availability MsLookupHelper(const M& map, const K& key, V& value)
+{
+    auto it = map.find(key);
+    if (it != map.end()) {
+        value = it->second;
+        return miniscript::Availability::YES;
+    }
+    return miniscript::Availability::NO;
+}
+
+/**
+ * Context for solving a Miniscript.
+ * If enough material (access to keys, hash preimages, ..) is given, produces a valid satisfaction.
+ */
+struct Satisfier {
+    typedef CPubKey Key;
+
+    const SigningProvider& m_provider;
+    SignatureData& m_sig_data;
+    const BaseSignatureCreator& m_creator;
+    const CScript& m_witness_script;
+
+    explicit Satisfier(const SigningProvider& provider LIFETIMEBOUND, SignatureData& sig_data LIFETIMEBOUND,
+                       const BaseSignatureCreator& creator LIFETIMEBOUND,
+                       const CScript& witscript LIFETIMEBOUND) : m_provider(provider),
+                                                                 m_sig_data(sig_data),
+                                                                 m_creator(creator),
+                                                                 m_witness_script(witscript) {}
+
+    static bool KeyCompare(const Key& a, const Key& b) {
+        return a < b;
+    }
+
+    //! Conversion from a raw public key.
+    template <typename I>
+    std::optional<Key> FromPKBytes(I first, I last) const
+    {
+        Key pubkey{first, last};
+        if (pubkey.IsValid()) return pubkey;
+        return {};
+    }
+
+    //! Conversion from a raw public key hash.
+    template<typename I>
+    std::optional<Key> FromPKHBytes(I first, I last) const {
+        assert(last - first == 20);
+        Key pubkey;
+        CKeyID key_id;
+        std::copy(first, last, key_id.begin());
+        if (GetPubKey(m_provider, m_sig_data, key_id, pubkey)) return pubkey;
+        m_sig_data.missing_pubkeys.push_back(key_id);
+        return {};
+    }
+
+    //! Conversion to raw public key.
+    std::vector<unsigned char> ToPKBytes(const CPubKey& key) const { return {key.begin(), key.end()}; }
+
+    //! Satisfy a signature check.
+    miniscript::Availability Sign(const CPubKey& key, std::vector<unsigned char>& sig) const {
+        if (CreateSig(m_creator, m_sig_data, m_provider, sig, key, m_witness_script, SigVersion::WITNESS_V0)) {
+            return miniscript::Availability::YES;
+        }
+        return miniscript::Availability::NO;
+    }
+
+    //! Time lock satisfactions.
+    bool CheckAfter(uint32_t value) const { return m_creator.Checker().CheckLockTime(CScriptNum(value)); }
+    bool CheckOlder(uint32_t value) const { return m_creator.Checker().CheckSequence(CScriptNum(value)); }
+
+
+    //! Hash preimage satisfactions.
+    miniscript::Availability SatSHA256(const std::vector<unsigned char>& hash, std::vector<unsigned char>& preimage) const {
+        return MsLookupHelper(m_sig_data.sha256_preimages, hash, preimage);
+    }
+    miniscript::Availability SatRIPEMD160(const std::vector<unsigned char>& hash, std::vector<unsigned char>& preimage) const {
+        return MsLookupHelper(m_sig_data.ripemd160_preimages, hash, preimage);
+    }
+    miniscript::Availability SatHASH256(const std::vector<unsigned char>& hash, std::vector<unsigned char>& preimage) const {
+        return MsLookupHelper(m_sig_data.hash256_preimages, hash, preimage);
+    }
+    miniscript::Availability SatHASH160(const std::vector<unsigned char>& hash, std::vector<unsigned char>& preimage) const {
+        return MsLookupHelper(m_sig_data.hash160_preimages, hash, preimage);
+    }
+};
+
 bool ProduceSignature(const SigningProvider& provider, const BaseSignatureCreator& creator, const CScript& fromPubKey, SignatureData& sigdata)
 {
     if (sigdata.complete) return true;
@@ -415,9 +502,21 @@ bool ProduceSignature(const SigningProvider& provider, const BaseSignatureCreato
     {
         CScript witnessscript(result[0].begin(), result[0].end());
         sigdata.witness_script = witnessscript;
-        TxoutType subType;
+
+        TxoutType subType{TxoutType::NONSTANDARD};
         solved = solved && SignStep(provider, creator, witnessscript, result, subType, SigVersion::WITNESS_V0, sigdata) && subType != TxoutType::SCRIPTHASH && subType != TxoutType::WITNESS_V0_SCRIPTHASH && subType != TxoutType::WITNESS_V0_KEYHASH;
+
+        // If we couldn't find a solution with the legacy satisfier, try satisfying the script using Miniscript.
+        // Note we need to check if the result stack is empty before, because it might be used even if the Script
+        // isn't fully solved. For instance the CHECKMULTISIG satisfaction in SignStep() pushes partial signatures
+        // and the extractor relies on this behaviour to combine witnesses.
+        if (!solved && result.empty()) {
+            Satisfier ms_satisfier{provider, sigdata, creator, witnessscript};
+            const auto ms = miniscript::FromScript(witnessscript, ms_satisfier);
+            solved = ms && ms->Satisfy(ms_satisfier, result) == miniscript::Availability::YES;
+        }
         result.push_back(std::vector<unsigned char>(witnessscript.begin(), witnessscript.end()));
+
         sigdata.scriptWitness.stack = result;
         sigdata.witness = true;
         result.clear();
@@ -563,26 +662,25 @@ void SignatureData::MergeSignatureData(SignatureData sigdata)
     signatures.insert(std::make_move_iterator(sigdata.signatures.begin()), std::make_move_iterator(sigdata.signatures.end()));
 }
 
-bool SignSignature(const SigningProvider &provider, const CScript& fromPubKey, CMutableTransaction& txTo, unsigned int nIn, const CAmount& amount, int nHashType)
+bool SignSignature(const SigningProvider &provider, const CScript& fromPubKey, CMutableTransaction& txTo, unsigned int nIn, const CAmount& amount, int nHashType, SignatureData& sig_data)
 {
     assert(nIn < txTo.vin.size());
 
     MutableTransactionSignatureCreator creator(txTo, nIn, amount, nHashType);
 
-    SignatureData sigdata;
-    bool ret = ProduceSignature(provider, creator, fromPubKey, sigdata);
-    UpdateInput(txTo.vin.at(nIn), sigdata);
+    bool ret = ProduceSignature(provider, creator, fromPubKey, sig_data);
+    UpdateInput(txTo.vin.at(nIn), sig_data);
     return ret;
 }
 
-bool SignSignature(const SigningProvider &provider, const CTransaction& txFrom, CMutableTransaction& txTo, unsigned int nIn, int nHashType)
+bool SignSignature(const SigningProvider &provider, const CTransaction& txFrom, CMutableTransaction& txTo, unsigned int nIn, int nHashType, SignatureData& sig_data)
 {
     assert(nIn < txTo.vin.size());
     const CTxIn& txin = txTo.vin[nIn];
     assert(txin.prevout.n < txFrom.vout.size());
     const CTxOut& txout = txFrom.vout[txin.prevout.n];
 
-    return SignSignature(provider, txout.scriptPubKey, txTo, nIn, txout.nValue, nHashType);
+    return SignSignature(provider, txout.scriptPubKey, txTo, nIn, txout.nValue, nHashType, sig_data);
 }
 
 namespace {
@@ -591,8 +689,10 @@ class DummySignatureChecker final : public BaseSignatureChecker
 {
 public:
     DummySignatureChecker() = default;
-    bool CheckECDSASignature(const std::vector<unsigned char>& scriptSig, const std::vector<unsigned char>& vchPubKey, const CScript& scriptCode, SigVersion sigversion) const override { return true; }
-    bool CheckSchnorrSignature(Span<const unsigned char> sig, Span<const unsigned char> pubkey, SigVersion sigversion, ScriptExecutionData& execdata, ScriptError* serror) const override { return true; }
+    bool CheckECDSASignature(const std::vector<unsigned char>& sig, const std::vector<unsigned char>& vchPubKey, const CScript& scriptCode, SigVersion sigversion) const override { return sig.size() != 0; }
+    bool CheckSchnorrSignature(Span<const unsigned char> sig, Span<const unsigned char> pubkey, SigVersion sigversion, ScriptExecutionData& execdata, ScriptError* serror) const override { return sig.size() != 0; }
+    bool CheckLockTime(const CScriptNum& nLockTime) const override { return true; }
+    bool CheckSequence(const CScriptNum& nSequence) const override { return true; }
 };
 }
 
diff --git a/src/script/sign.h b/src/script/sign.h
index 263fb61fc5..fe2354cad7 100644
--- a/src/script/sign.h
+++ b/src/script/sign.h
@@ -83,6 +83,10 @@ struct SignatureData {
     std::vector<CKeyID> missing_sigs; ///< KeyIDs of pubkeys for signatures which could not be found
     uint160 missing_redeem_script; ///< ScriptID of the missing redeemScript (if any)
     uint256 missing_witness_script; ///< SHA256 of the missing witnessScript (if any)
+    std::map<std::vector<uint8_t>, std::vector<uint8_t>> sha256_preimages; ///< Mapping from a SHA256 hash to its preimage provided to solve a Script
+    std::map<std::vector<uint8_t>, std::vector<uint8_t>> hash256_preimages; ///< Mapping from a HASH256 hash to its preimage provided to solve a Script
+    std::map<std::vector<uint8_t>, std::vector<uint8_t>> ripemd160_preimages; ///< Mapping from a RIPEMD160 hash to its preimage provided to solve a Script
+    std::map<std::vector<uint8_t>, std::vector<uint8_t>> hash160_preimages; ///< Mapping from a HASH160 hash to its preimage provided to solve a Script
 
     SignatureData() {}
     explicit SignatureData(const CScript& script) : scriptSig(script) {}
@@ -92,9 +96,24 @@ struct SignatureData {
 /** Produce a script signature using a generic signature creator. */
 bool ProduceSignature(const SigningProvider& provider, const BaseSignatureCreator& creator, const CScript& scriptPubKey, SignatureData& sigdata);
 
-/** Produce a script signature for a transaction. */
-bool SignSignature(const SigningProvider &provider, const CScript& fromPubKey, CMutableTransaction& txTo, unsigned int nIn, const CAmount& amount, int nHashType);
-bool SignSignature(const SigningProvider &provider, const CTransaction& txFrom, CMutableTransaction& txTo, unsigned int nIn, int nHashType);
+/**
+ * Produce a satisfying script (scriptSig or witness).
+ *
+ * @param provider   Utility containing the information necessary to solve a script.
+ * @param fromPubKey The script to produce a satisfaction for.
+ * @param txTo       The spending transaction.
+ * @param nIn        The index of the input in `txTo` refering the output being spent.
+ * @param amount     The value of the output being spent.
+ * @param nHashType  Signature hash type.
+ * @param sig_data   Additional data provided to solve a script. Filled with the resulting satisfying
+ *                   script and whether the satisfaction is complete.
+ *
+ * @return           True if the produced script is entirely satisfying `fromPubKey`.
+ **/
+bool SignSignature(const SigningProvider &provider, const CScript& fromPubKey, CMutableTransaction& txTo,
+                   unsigned int nIn, const CAmount& amount, int nHashType, SignatureData& sig_data);
+bool SignSignature(const SigningProvider &provider, const CTransaction& txFrom, CMutableTransaction& txTo,
+                   unsigned int nIn, int nHashType, SignatureData& sig_data);
 
 /** Extract signature data from a transaction input, and insert it. */
 SignatureData DataFromTransaction(const CMutableTransaction& tx, unsigned int nIn, const CTxOut& txout);
diff --git a/src/test/crypto_tests.cpp b/src/test/crypto_tests.cpp
index d3eef7beb7..ed851b5266 100644
--- a/src/test/crypto_tests.cpp
+++ b/src/test/crypto_tests.cpp
@@ -133,14 +133,14 @@ static void TestAES256CBC(const std::string &hexkey, const std::string &hexiv, b
 static void TestChaCha20(const std::string &hex_message, const std::string &hexkey, uint64_t nonce, uint64_t seek, const std::string& hexout)
 {
     std::vector<unsigned char> key = ParseHex(hexkey);
+    assert(key.size() == 32);
     std::vector<unsigned char> m = ParseHex(hex_message);
-    ChaCha20 rng(key.data(), key.size());
+    ChaCha20 rng(key.data());
     rng.SetIV(nonce);
-    rng.Seek(seek);
-    std::vector<unsigned char> out = ParseHex(hexout);
+    rng.Seek64(seek);
     std::vector<unsigned char> outres;
-    outres.resize(out.size());
-    assert(hex_message.empty() || m.size() == out.size());
+    outres.resize(hexout.size() / 2);
+    assert(hex_message.empty() || m.size() * 2 == hexout.size());
 
     // perform the ChaCha20 round(s), if message is provided it will output the encrypted ciphertext otherwise the keystream
     if (!hex_message.empty()) {
@@ -148,17 +148,38 @@ static void TestChaCha20(const std::string &hex_message, const std::string &hexk
     } else {
         rng.Keystream(outres.data(), outres.size());
     }
-    BOOST_CHECK(out == outres);
+    BOOST_CHECK_EQUAL(hexout, HexStr(outres));
     if (!hex_message.empty()) {
         // Manually XOR with the keystream and compare the output
         rng.SetIV(nonce);
-        rng.Seek(seek);
+        rng.Seek64(seek);
         std::vector<unsigned char> only_keystream(outres.size());
         rng.Keystream(only_keystream.data(), only_keystream.size());
         for (size_t i = 0; i != m.size(); i++) {
             outres[i] = m[i] ^ only_keystream[i];
         }
-        BOOST_CHECK(out == outres);
+        BOOST_CHECK_EQUAL(hexout, HexStr(outres));
+    }
+
+    // Repeat 10x, but fragmented into 3 chunks, to exercise the ChaCha20 class's caching.
+    for (int i = 0; i < 10; ++i) {
+        size_t lens[3];
+        lens[0] = InsecureRandRange(hexout.size() / 2U + 1U);
+        lens[1] = InsecureRandRange(hexout.size() / 2U + 1U - lens[0]);
+        lens[2] = hexout.size() / 2U - lens[0] - lens[1];
+
+        rng.Seek64(seek);
+        outres.assign(hexout.size() / 2U, 0);
+        size_t pos = 0;
+        for (int j = 0; j < 3; ++j) {
+            if (!hex_message.empty()) {
+                rng.Crypt(m.data() + pos, outres.data() + pos, lens[j]);
+            } else {
+                rng.Keystream(outres.data() + pos, lens[j]);
+            }
+            pos += lens[j];
+        }
+        BOOST_CHECK_EQUAL(hexout, HexStr(outres));
     }
 }
 
@@ -460,7 +481,88 @@ BOOST_AUTO_TEST_CASE(aes_cbc_testvectors) {
 
 BOOST_AUTO_TEST_CASE(chacha20_testvector)
 {
-    // Test vector from RFC 7539
+    // RFC 7539/8439 A.1 Test Vector #1:
+    TestChaCha20("",
+                 "0000000000000000000000000000000000000000000000000000000000000000",
+                 0, 0,
+                 "76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7"
+                 "da41597c5157488d7724e03fb8d84a376a43b8f41518a11cc387b669b2ee6586");
+
+    // RFC 7539/8439 A.1 Test Vector #2:
+    TestChaCha20("",
+                 "0000000000000000000000000000000000000000000000000000000000000000",
+                 0, 1,
+                 "9f07e7be5551387a98ba977c732d080dcb0f29a048e3656912c6533e32ee7aed"
+                 "29b721769ce64e43d57133b074d839d531ed1f28510afb45ace10a1f4b794d6f");
+
+    // RFC 7539/8439 A.1 Test Vector #3:
+    TestChaCha20("",
+                 "0000000000000000000000000000000000000000000000000000000000000001",
+                 0, 1,
+                 "3aeb5224ecf849929b9d828db1ced4dd832025e8018b8160b82284f3c949aa5a"
+                 "8eca00bbb4a73bdad192b5c42f73f2fd4e273644c8b36125a64addeb006c13a0");
+
+    // RFC 7539/8439 A.1 Test Vector #4:
+    TestChaCha20("",
+                 "00ff000000000000000000000000000000000000000000000000000000000000",
+                 0, 2,
+                 "72d54dfbf12ec44b362692df94137f328fea8da73990265ec1bbbea1ae9af0ca"
+                 "13b25aa26cb4a648cb9b9d1be65b2c0924a66c54d545ec1b7374f4872e99f096");
+
+    // RFC 7539/8439 A.1 Test Vector #5:
+    TestChaCha20("",
+                 "0000000000000000000000000000000000000000000000000000000000000000",
+                 0x200000000000000, 0,
+                 "c2c64d378cd536374ae204b9ef933fcd1a8b2288b3dfa49672ab765b54ee27c7"
+                 "8a970e0e955c14f3a88e741b97c286f75f8fc299e8148362fa198a39531bed6d");
+
+    // RFC 7539/8439 A.2 Test Vector #1:
+    TestChaCha20("0000000000000000000000000000000000000000000000000000000000000000"
+                 "0000000000000000000000000000000000000000000000000000000000000000",
+                 "0000000000000000000000000000000000000000000000000000000000000000",
+                 0, 0,
+                 "76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7"
+                 "da41597c5157488d7724e03fb8d84a376a43b8f41518a11cc387b669b2ee6586");
+
+    // RFC 7539/8439 A.2 Test Vector #2:
+    TestChaCha20("416e79207375626d697373696f6e20746f20746865204945544620696e74656e"
+                 "6465642062792074686520436f6e7472696275746f7220666f72207075626c69"
+                 "636174696f6e20617320616c6c206f722070617274206f6620616e2049455446"
+                 "20496e7465726e65742d4472616674206f722052464320616e6420616e792073"
+                 "746174656d656e74206d6164652077697468696e2074686520636f6e74657874"
+                 "206f6620616e204945544620616374697669747920697320636f6e7369646572"
+                 "656420616e20224945544620436f6e747269627574696f6e222e205375636820"
+                 "73746174656d656e747320696e636c756465206f72616c2073746174656d656e"
+                 "747320696e20494554462073657373696f6e732c2061732077656c6c20617320"
+                 "7772697474656e20616e6420656c656374726f6e696320636f6d6d756e696361"
+                 "74696f6e73206d61646520617420616e792074696d65206f7220706c6163652c"
+                 "207768696368206172652061646472657373656420746f",
+                 "0000000000000000000000000000000000000000000000000000000000000001",
+                 0x200000000000000, 1,
+                 "a3fbf07df3fa2fde4f376ca23e82737041605d9f4f4f57bd8cff2c1d4b7955ec"
+                 "2a97948bd3722915c8f3d337f7d370050e9e96d647b7c39f56e031ca5eb6250d"
+                 "4042e02785ececfa4b4bb5e8ead0440e20b6e8db09d881a7c6132f420e527950"
+                 "42bdfa7773d8a9051447b3291ce1411c680465552aa6c405b7764d5e87bea85a"
+                 "d00f8449ed8f72d0d662ab052691ca66424bc86d2df80ea41f43abf937d3259d"
+                 "c4b2d0dfb48a6c9139ddd7f76966e928e635553ba76c5c879d7b35d49eb2e62b"
+                 "0871cdac638939e25e8a1e0ef9d5280fa8ca328b351c3c765989cbcf3daa8b6c"
+                 "cc3aaf9f3979c92b3720fc88dc95ed84a1be059c6499b9fda236e7e818b04b0b"
+                 "c39c1e876b193bfe5569753f88128cc08aaa9b63d1a16f80ef2554d7189c411f"
+                 "5869ca52c5b83fa36ff216b9c1d30062bebcfd2dc5bce0911934fda79a86f6e6"
+                 "98ced759c3ff9b6477338f3da4f9cd8514ea9982ccafb341b2384dd902f3d1ab"
+                 "7ac61dd29c6f21ba5b862f3730e37cfdc4fd806c22f221");
+
+    // RFC 7539/8439 A.2 Test Vector #3:
+    TestChaCha20("2754776173206272696c6c69672c20616e642074686520736c6974687920746f"
+                 "7665730a446964206779726520616e642067696d626c6520696e207468652077"
+                 "6162653a0a416c6c206d696d737920776572652074686520626f726f676f7665"
+                 "732c0a416e6420746865206d6f6d65207261746873206f757467726162652e",
+                 "1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0",
+                 0x200000000000000, 42,
+                 "62e6347f95ed87a45ffae7426f27a1df5fb69110044c0d73118effa95b01e5cf"
+                 "166d3df2d721caf9b21e5fb14c616871fd84c54f9d65b283196c7fe4f60553eb"
+                 "f39c6402c42234e32a356b3e764312a61a5532055716ead6962568f87d3f3f77"
+                 "04c6a8d1bcd1bf4d50d6154b6da731b187b58dfd728afa36757a797ac188d1");
 
     // test encryption
     TestChaCha20("4c616469657320616e642047656e746c656d656e206f662074686520636c617373206f66202739393a204966204920636f756"
@@ -477,27 +579,24 @@ BOOST_AUTO_TEST_CASE(chacha20_testvector)
                  "224f51f3401bd9e12fde276fb8631ded8c131f823d2c06e27e4fcaec9ef3cf788a3b0aa372600a92b57974cded2b9334794cb"
                  "a40c63e34cdea212c4cf07d41b769a6749f3f630f4122cafe28ec4dc47e26d4346d70b98c73f3e9c53ac40c5945398b6eda1a"
                  "832c89c167eacd901d7e2bf363");
+}
 
-    // Test vectors from https://tools.ietf.org/html/draft-agl-tls-chacha20poly1305-04#section-7
-    TestChaCha20("", "0000000000000000000000000000000000000000000000000000000000000000", 0, 0,
-                 "76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7da41597c5157488d7724e03fb8d84a376a43b"
-                 "8f41518a11cc387b669b2ee6586");
-    TestChaCha20("", "0000000000000000000000000000000000000000000000000000000000000001", 0, 0,
-                 "4540f05a9f1fb296d7736e7b208e3c96eb4fe1834688d2604f450952ed432d41bbe2a0b6ea7566d2a5d1e7e20d42af2c53d79"
-                 "2b1c43fea817e9ad275ae546963");
-    TestChaCha20("", "0000000000000000000000000000000000000000000000000000000000000000", 0x0100000000000000ULL, 0,
-                 "de9cba7bf3d69ef5e786dc63973f653a0b49e015adbff7134fcb7df137821031e85a050278a7084527214f73efc7fa5b52770"
-                 "62eb7a0433e445f41e3");
-    TestChaCha20("", "0000000000000000000000000000000000000000000000000000000000000000", 1, 0,
-                 "ef3fdfd6c61578fbf5cf35bd3dd33b8009631634d21e42ac33960bd138e50d32111e4caf237ee53ca8ad6426194a88545ddc4"
-                 "97a0b466e7d6bbdb0041b2f586b");
-    TestChaCha20("", "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", 0x0706050403020100ULL, 0,
-                 "f798a189f195e66982105ffb640bb7757f579da31602fc93ec01ac56f85ac3c134a4547b733b46413042c9440049176905d3b"
-                 "e59ea1c53f15916155c2be8241a38008b9a26bc35941e2444177c8ade6689de95264986d95889fb60e84629c9bd9a5acb1cc1"
-                 "18be563eb9b3a4a472f82e09a7e778492b562ef7130e88dfe031c79db9d4f7c7a899151b9a475032b63fc385245fe054e3dd5"
-                 "a97a5f576fe064025d3ce042c566ab2c507b138db853e3d6959660996546cc9c4a6eafdc777c040d70eaf46f76dad3979e5c5"
-                 "360c3317166a1c894c94a371876a94df7628fe4eaaf2ccb27d5aaae0ad7ad0f9d4b6ad3b54098746d4524d38407a6deb3ab78"
-                 "fab78c9");
+BOOST_AUTO_TEST_CASE(chacha20_midblock)
+{
+    auto key = ParseHex("0000000000000000000000000000000000000000000000000000000000000000");
+    ChaCha20 c20{key.data()};
+    // get one block of keystream
+    unsigned char block[64];
+    c20.Keystream(block, CHACHA20_ROUND_OUTPUT);
+    unsigned char b1[5], b2[7], b3[52];
+    c20 = ChaCha20{key.data()};
+    c20.Keystream(b1, 5);
+    c20.Keystream(b2, 7);
+    c20.Keystream(b3, 52);
+
+    BOOST_CHECK_EQUAL(0, memcmp(b1, block, 5));
+    BOOST_CHECK_EQUAL(0, memcmp(b2, block + 5, 7));
+    BOOST_CHECK_EQUAL(0, memcmp(b3, block + 12, 52));
 }
 
 BOOST_AUTO_TEST_CASE(poly1305_testvector)
@@ -617,7 +716,7 @@ static void TestChaCha20Poly1305AEAD(bool must_succeed, unsigned int expected_aa
     ChaCha20Poly1305AEAD aead(aead_K_1.data(), aead_K_1.size(), aead_K_2.data(), aead_K_2.size());
 
     // create a chacha20 instance to compare against
-    ChaCha20 cmp_ctx(aead_K_1.data(), 32);
+    ChaCha20 cmp_ctx(aead_K_1.data());
 
     // encipher
     bool res = aead.Crypt(seqnr_payload, seqnr_aad, aad_pos, ciphertext_buf.data(), ciphertext_buf.size(), plaintext_buf.data(), plaintext_buf.size(), true);
@@ -631,7 +730,7 @@ static void TestChaCha20Poly1305AEAD(bool must_succeed, unsigned int expected_aa
 
     // manually construct the AAD keystream
     cmp_ctx.SetIV(seqnr_aad);
-    cmp_ctx.Seek(0);
+    cmp_ctx.Seek64(0);
     cmp_ctx.Keystream(cmp_ctx_buffer.data(), 64);
     BOOST_CHECK(memcmp(expected_aad_keystream.data(), cmp_ctx_buffer.data(), expected_aad_keystream.size()) == 0);
     // crypt the 3 length bytes and compare the length
@@ -659,7 +758,7 @@ static void TestChaCha20Poly1305AEAD(bool must_succeed, unsigned int expected_aa
         }
         // set nonce and block counter, output the keystream
         cmp_ctx.SetIV(seqnr_aad);
-        cmp_ctx.Seek(0);
+        cmp_ctx.Seek64(0);
         cmp_ctx.Keystream(cmp_ctx_buffer.data(), 64);
 
         // crypt the 3 length bytes and compare the length
diff --git a/src/test/descriptor_tests.cpp b/src/test/descriptor_tests.cpp
index 6e4f6cdbab..c4b2b4c63b 100644
--- a/src/test/descriptor_tests.cpp
+++ b/src/test/descriptor_tests.cpp
@@ -45,6 +45,7 @@ constexpr int DERIVE_HARDENED = 16; // The final derivation is hardened, i.e. en
 constexpr int MIXED_PUBKEYS = 32;
 constexpr int XONLY_KEYS = 64; // X-only pubkeys are in use (and thus inferring/caching may swap parity of pubkeys/keyids)
 constexpr int MISSING_PRIVKEYS = 128; // Not all private keys are available, so ToPrivateString will fail.
+constexpr int SIGNABLE_FAILS = 256; // We can sign with this descriptor, but actually trying to sign will fail
 
 /** Compare two descriptors. If only one of them has a checksum, the checksum is ignored. */
 bool EqualDescriptor(std::string a, std::string b)
@@ -126,8 +127,11 @@ std::set<std::pair<CPubKey, KeyOriginInfo>> GetKeyOriginData(const FlatSigningPr
     return ret;
 }
 
-void DoCheck(const std::string& prv, const std::string& pub, const std::string& norm_pub, int flags, const std::vector<std::vector<std::string>>& scripts, const std::optional<OutputType>& type, const std::set<std::vector<uint32_t>>& paths = ONLY_EMPTY,
-    bool replace_apostrophe_with_h_in_prv=false, bool replace_apostrophe_with_h_in_pub=false)
+void DoCheck(const std::string& prv, const std::string& pub, const std::string& norm_pub, int flags,
+             const std::vector<std::vector<std::string>>& scripts, const std::optional<OutputType>& type,
+             const std::set<std::vector<uint32_t>>& paths = ONLY_EMPTY, bool replace_apostrophe_with_h_in_prv=false,
+             bool replace_apostrophe_with_h_in_pub=false, uint32_t spender_nlocktime=0, uint32_t spender_nsequence=CTxIn::SEQUENCE_FINAL,
+             std::map<std::vector<uint8_t>, std::vector<uint8_t>> preimages={})
 {
     FlatSigningProvider keys_priv, keys_pub;
     std::set<std::vector<uint32_t>> left_paths = paths;
@@ -303,16 +307,24 @@ void DoCheck(const std::string& prv, const std::string& pub, const std::string&
             for (size_t n = 0; n < spks.size(); ++n) {
                 BOOST_CHECK_EQUAL(ref[n], HexStr(spks[n]));
 
-                if (flags & SIGNABLE) {
+                if (flags & (SIGNABLE | SIGNABLE_FAILS)) {
                     CMutableTransaction spend;
+                    spend.nLockTime = spender_nlocktime;
                     spend.vin.resize(1);
+                    spend.vin[0].nSequence = spender_nsequence;
                     spend.vout.resize(1);
                     std::vector<CTxOut> utxos(1);
                     PrecomputedTransactionData txdata;
                     txdata.Init(spend, std::move(utxos), /*force=*/true);
                     MutableTransactionSignatureCreator creator{spend, 0, CAmount{0}, &txdata, SIGHASH_DEFAULT};
                     SignatureData sigdata;
-                    BOOST_CHECK_MESSAGE(ProduceSignature(FlatSigningProvider{keys_priv}.Merge(FlatSigningProvider{script_provider}), creator, spks[n], sigdata), prv);
+                    // We assume there is no collision between the hashes (eg h1=SHA256(SHA256(x)) and h2=SHA256(x))
+                    sigdata.sha256_preimages = preimages;
+                    sigdata.hash256_preimages = preimages;
+                    sigdata.ripemd160_preimages = preimages;
+                    sigdata.hash160_preimages = preimages;
+                    const auto prod_sig_res = ProduceSignature(FlatSigningProvider{keys_priv}.Merge(FlatSigningProvider{script_provider}), creator, spks[n], sigdata);
+                    BOOST_CHECK_MESSAGE(prod_sig_res == !(flags & SIGNABLE_FAILS), prv);
                 }
 
                 /* Infer a descriptor from the generated script, and verify its solvability and that it roundtrips. */
@@ -340,29 +352,40 @@ void DoCheck(const std::string& prv, const std::string& pub, const std::string&
     BOOST_CHECK_MESSAGE(left_paths.empty(), "Not all expected key paths found: " + prv);
 }
 
-void Check(const std::string& prv, const std::string& pub, const std::string& norm_pub, int flags, const std::vector<std::vector<std::string>>& scripts, const std::optional<OutputType>& type, const std::set<std::vector<uint32_t>>& paths = ONLY_EMPTY)
+void Check(const std::string& prv, const std::string& pub, const std::string& norm_pub, int flags,
+           const std::vector<std::vector<std::string>>& scripts, const std::optional<OutputType>& type,
+           const std::set<std::vector<uint32_t>>& paths = ONLY_EMPTY, uint32_t spender_nlocktime=0,
+           uint32_t spender_nsequence=CTxIn::SEQUENCE_FINAL, std::map<std::vector<uint8_t>, std::vector<uint8_t>> preimages={})
 {
     bool found_apostrophes_in_prv = false;
     bool found_apostrophes_in_pub = false;
 
     // Do not replace apostrophes with 'h' in prv and pub
-    DoCheck(prv, pub, norm_pub, flags, scripts, type, paths);
+    DoCheck(prv, pub, norm_pub, flags, scripts, type, paths, /*replace_apostrophe_with_h_in_prv=*/false,
+            /*replace_apostrophe_with_h_in_pub=*/false, /*spender_nlocktime=*/spender_nlocktime,
+            /*spender_nsequence=*/spender_nsequence, /*preimages=*/preimages);
 
     // Replace apostrophes with 'h' in prv but not in pub, if apostrophes are found in prv
     if (prv.find('\'') != std::string::npos) {
         found_apostrophes_in_prv = true;
-        DoCheck(prv, pub, norm_pub, flags, scripts, type, paths, /* replace_apostrophe_with_h_in_prv = */true, /*replace_apostrophe_with_h_in_pub = */false);
+        DoCheck(prv, pub, norm_pub, flags, scripts, type, paths, /*replace_apostrophe_with_h_in_prv=*/true,
+                /*replace_apostrophe_with_h_in_pub=*/false, /*spender_nlocktime=*/spender_nlocktime,
+                /*spender_nsequence=*/spender_nsequence, /*preimages=*/preimages);
     }
 
     // Replace apostrophes with 'h' in pub but not in prv, if apostrophes are found in pub
     if (pub.find('\'') != std::string::npos) {
         found_apostrophes_in_pub = true;
-        DoCheck(prv, pub, norm_pub, flags, scripts, type, paths, /* replace_apostrophe_with_h_in_prv = */false, /*replace_apostrophe_with_h_in_pub = */true);
+        DoCheck(prv, pub, norm_pub, flags, scripts, type, paths, /*replace_apostrophe_with_h_in_prv=*/false,
+                /*replace_apostrophe_with_h_in_pub=*/true,  /*spender_nlocktime=*/spender_nlocktime,
+                /*spender_nsequence=*/spender_nsequence, /*preimages=*/preimages);
     }
 
     // Replace apostrophes with 'h' both in prv and in pub, if apostrophes are found in both
     if (found_apostrophes_in_prv && found_apostrophes_in_pub) {
-        DoCheck(prv, pub, norm_pub, flags, scripts, type, paths, /* replace_apostrophe_with_h_in_prv = */true, /*replace_apostrophe_with_h_in_pub = */true);
+        DoCheck(prv, pub, norm_pub, flags, scripts, type, paths, /*replace_apostrophe_with_h_in_prv=*/true,
+                /*replace_apostrophe_with_h_in_pub=*/true, /*spender_nlocktime=*/spender_nlocktime,
+                /*spender_nsequence=*/spender_nsequence, /*preimages=*/preimages);
     }
 }
 
@@ -528,9 +551,31 @@ BOOST_AUTO_TEST_CASE(descriptor_test)
     CheckUnparsable("wsh(and_b(and_b(older(1),a:older(100000000)),s:pk(L4gM1FBdyHNpkzsFh9ipnofLhpZRp2mwobpeULy1a6dBTvw8Ywtd)))", "wsh(and_b(and_b(older(1),a:older(100000000)),s:pk(03cdabb7f2dce7bfbd8a0b9570c6fd1e712e5d64045e9d6b517b3d5072251dc204)))", "and_b(older(1),a:older(100000000)) is not sane: contains mixes of timelocks expressed in blocks and seconds");
     CheckUnparsable("wsh(and_b(or_b(pkh(L4gM1FBdyHNpkzsFh9ipnofLhpZRp2mwobpeULy1a6dBTvw8Ywtd),s:pk(Kx9HCDjGiwFcgVNhTrS5z5NeZdD6veeam61eDxLDCkGWujvL4Gnn)),s:pk(L4gM1FBdyHNpkzsFh9ipnofLhpZRp2mwobpeULy1a6dBTvw8Ywtd)))", "wsh(and_b(or_b(pkh(03cdabb7f2dce7bfbd8a0b9570c6fd1e712e5d64045e9d6b517b3d5072251dc204),s:pk(032707170c71d8f75e4ca4e3fce870b9409dcaf12b051d3bcadff74747fa7619c0)),s:pk(03cdabb7f2dce7bfbd8a0b9570c6fd1e712e5d64045e9d6b517b3d5072251dc204)))", "and_b(or_b(pkh(03cdabb7f2dce7bfbd8a0b9570c6fd1e712e5d64045e9d6b517b3d5072251dc204),s:pk(032707170c71d8f75e4ca4e3fce870b9409dcaf12b051d3bcadff74747fa7619c0)),s:pk(03cdabb7f2dce7bfbd8a0b9570c6fd1e712e5d64045e9d6b517b3d5072251dc204)) is not sane: contains duplicate public keys");
     // Valid with extended keys.
-    Check("wsh(and_v(v:ripemd160(095ff41131e5946f3c85f79e44adbcf8e27e080e),multi(1,xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0)))", "wsh(and_v(v:ripemd160(095ff41131e5946f3c85f79e44adbcf8e27e080e),multi(1,xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0)))", "wsh(and_v(v:ripemd160(095ff41131e5946f3c85f79e44adbcf8e27e080e),multi(1,xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0)))", UNSOLVABLE, {{"0020acf425291b98a1d7e0d4690139442abc289175be32ef1f75945e339924246d73"}}, OutputType::BECH32, {{},{0}});
-    // Valid under sh(wsh()) and with a mix of xpubs and raw keys
-    Check("sh(wsh(thresh(1,pkh(L4gM1FBdyHNpkzsFh9ipnofLhpZRp2mwobpeULy1a6dBTvw8Ywtd),a:and_n(multi(1,xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0),n:older(2)))))", "sh(wsh(thresh(1,pkh(03cdabb7f2dce7bfbd8a0b9570c6fd1e712e5d64045e9d6b517b3d5072251dc204),a:and_n(multi(1,xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0),n:older(2)))))", "sh(wsh(thresh(1,pkh(03cdabb7f2dce7bfbd8a0b9570c6fd1e712e5d64045e9d6b517b3d5072251dc204),a:and_n(multi(1,xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0),n:older(2)))))", UNSOLVABLE | MIXED_PUBKEYS, {{"a914767e9119ff3b3ac0cb6dcfe21de1842ccf85f1c487"}}, OutputType::P2SH_SEGWIT, {{},{0}});
+    Check("wsh(and_v(v:ripemd160(095ff41131e5946f3c85f79e44adbcf8e27e080e),multi(1,xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0)))", "wsh(and_v(v:ripemd160(095ff41131e5946f3c85f79e44adbcf8e27e080e),multi(1,xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0)))", "wsh(and_v(v:ripemd160(095ff41131e5946f3c85f79e44adbcf8e27e080e),multi(1,xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0)))", DEFAULT, {{"0020acf425291b98a1d7e0d4690139442abc289175be32ef1f75945e339924246d73"}}, OutputType::BECH32, {{},{0}});
+    // Valid under sh(wsh()) and with a mix of xpubs and raw keys.
+    Check("sh(wsh(thresh(1,pkh(L4gM1FBdyHNpkzsFh9ipnofLhpZRp2mwobpeULy1a6dBTvw8Ywtd),a:and_n(multi(1,xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0),n:older(2)))))", "sh(wsh(thresh(1,pkh(03cdabb7f2dce7bfbd8a0b9570c6fd1e712e5d64045e9d6b517b3d5072251dc204),a:and_n(multi(1,xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0),n:older(2)))))", "sh(wsh(thresh(1,pkh(03cdabb7f2dce7bfbd8a0b9570c6fd1e712e5d64045e9d6b517b3d5072251dc204),a:and_n(multi(1,xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0),n:older(2)))))", SIGNABLE | MIXED_PUBKEYS, {{"a914767e9119ff3b3ac0cb6dcfe21de1842ccf85f1c487"}}, OutputType::P2SH_SEGWIT, {{},{0}});
+    // An exotic multisig, we can sign for both branches
+    Check("wsh(thresh(1,pk(xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc),a:pkh(xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0)))", "wsh(thresh(1,pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL),a:pkh(xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0)))", "wsh(thresh(1,pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL),a:pkh(xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0)))", SIGNABLE, {{"00204a4528fbc0947e02e921b54bd476fc8cc2ebb5c6ae2ccf10ed29fe2937fb6892"}}, OutputType::BECH32, {{},{0}});
+    // We can sign for a script requiring the two kinds of timelock.
+    // But if we don't set a sequence high enough, we'll fail.
+    Check("sh(wsh(thresh(2,ndv:after(1000),a:and_n(multi(1,xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0),n:older(2)))))", "sh(wsh(thresh(2,ndv:after(1000),a:and_n(multi(1,xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0),n:older(2)))))", "sh(wsh(thresh(2,ndv:after(1000),a:and_n(multi(1,xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0),n:older(2)))))", SIGNABLE_FAILS, {{"a914099f400961f930d4c16c3b33c0e2a58ef53ac38f87"}}, OutputType::P2SH_SEGWIT, {{},{0}}, /*spender_nlocktime=*/1000, /*spender_nsequence=*/1);
+    // And same for the nLockTime.
+    Check("sh(wsh(thresh(2,ndv:after(1000),a:and_n(multi(1,xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0),n:older(2)))))", "sh(wsh(thresh(2,ndv:after(1000),a:and_n(multi(1,xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0),n:older(2)))))", "sh(wsh(thresh(2,ndv:after(1000),a:and_n(multi(1,xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0),n:older(2)))))", SIGNABLE_FAILS, {{"a914099f400961f930d4c16c3b33c0e2a58ef53ac38f87"}}, OutputType::P2SH_SEGWIT, {{},{0}}, /*spender_nlocktime=*/999, /*spender_nsequence=*/2);
+    // But if both are set to (at least) the required value, we'll succeed.
+    Check("sh(wsh(thresh(2,ndv:after(1000),a:and_n(multi(1,xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0),n:older(2)))))", "sh(wsh(thresh(2,ndv:after(1000),a:and_n(multi(1,xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0),n:older(2)))))", "sh(wsh(thresh(2,ndv:after(1000),a:and_n(multi(1,xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0),n:older(2)))))", SIGNABLE, {{"a914099f400961f930d4c16c3b33c0e2a58ef53ac38f87"}}, OutputType::P2SH_SEGWIT, {{},{0}}, /*spender_nlocktime=*/1000, /*spender_nsequence=*/2);
+    // We can't sign for a script requiring a ripemd160 preimage without providing it.
+    Check("wsh(and_v(v:ripemd160(ff9aa1829c90d26e73301383f549e1497b7d6325),pk(xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc)))", "wsh(and_v(v:ripemd160(ff9aa1829c90d26e73301383f549e1497b7d6325),pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL)))", "wsh(and_v(v:ripemd160(ff9aa1829c90d26e73301383f549e1497b7d6325),pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL)))", SIGNABLE_FAILS, {{"002001549deda34cbc4a5982263191380f522695a2ddc2f99fc3a65c736264bd6cab"}}, OutputType::BECH32, {{}}, /*spender_nlocktime=*/0, /*spender_nsequence=*/CTxIn::SEQUENCE_FINAL, {});
+    // But if we provide it, we can.
+    Check("wsh(and_v(v:ripemd160(ff9aa1829c90d26e73301383f549e1497b7d6325),pk(xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc)))", "wsh(and_v(v:ripemd160(ff9aa1829c90d26e73301383f549e1497b7d6325),pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL)))", "wsh(and_v(v:ripemd160(ff9aa1829c90d26e73301383f549e1497b7d6325),pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL)))", SIGNABLE, {{"002001549deda34cbc4a5982263191380f522695a2ddc2f99fc3a65c736264bd6cab"}}, OutputType::BECH32, {{}}, /*spender_nlocktime=*/0, /*spender_nsequence=*/CTxIn::SEQUENCE_FINAL, {{ParseHex("ff9aa1829c90d26e73301383f549e1497b7d6325"), ParseHex("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f")}});
+    // Same for sha256
+    Check("wsh(and_v(v:sha256(7426ba0604c3f8682c7016b44673f85c5bd9da2fa6c1080810cf53ae320c9863),pk(xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc)))", "wsh(and_v(v:sha256(7426ba0604c3f8682c7016b44673f85c5bd9da2fa6c1080810cf53ae320c9863),pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL)))", "wsh(and_v(v:sha256(7426ba0604c3f8682c7016b44673f85c5bd9da2fa6c1080810cf53ae320c9863),pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL)))", SIGNABLE_FAILS, {{"002071f7283dbbb9a55ed43a54cda16ba0efd0f16dc48fe200f299e57bb5d7be8dd4"}}, OutputType::BECH32, {{}}, /*spender_nlocktime=*/0, /*spender_nsequence=*/CTxIn::SEQUENCE_FINAL, {});
+    Check("wsh(and_v(v:sha256(7426ba0604c3f8682c7016b44673f85c5bd9da2fa6c1080810cf53ae320c9863),pk(xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc)))", "wsh(and_v(v:sha256(7426ba0604c3f8682c7016b44673f85c5bd9da2fa6c1080810cf53ae320c9863),pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL)))", "wsh(and_v(v:sha256(7426ba0604c3f8682c7016b44673f85c5bd9da2fa6c1080810cf53ae320c9863),pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL)))", SIGNABLE, {{"002071f7283dbbb9a55ed43a54cda16ba0efd0f16dc48fe200f299e57bb5d7be8dd4"}}, OutputType::BECH32, {{}}, /*spender_nlocktime=*/0, /*spender_nsequence=*/CTxIn::SEQUENCE_FINAL, {{ParseHex("7426ba0604c3f8682c7016b44673f85c5bd9da2fa6c1080810cf53ae320c9863"), ParseHex("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f")}});
+    // Same for hash160
+    Check("wsh(and_v(v:hash160(292e2df59e3a22109200beed0cdc84b12e66793e),pk(xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc)))", "wsh(and_v(v:hash160(292e2df59e3a22109200beed0cdc84b12e66793e),pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL)))", "wsh(and_v(v:hash160(292e2df59e3a22109200beed0cdc84b12e66793e),pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL)))", SIGNABLE_FAILS, {{"00209b9d5b45735d0e15df5b41d6594602d3de472262f7b75edc6cf5f3e3fa4e3ae4"}}, OutputType::BECH32, {{}}, /*spender_nlocktime=*/0, /*spender_nsequence=*/CTxIn::SEQUENCE_FINAL, {});
+    Check("wsh(and_v(v:hash160(292e2df59e3a22109200beed0cdc84b12e66793e),pk(xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc)))", "wsh(and_v(v:hash160(292e2df59e3a22109200beed0cdc84b12e66793e),pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL)))", "wsh(and_v(v:hash160(292e2df59e3a22109200beed0cdc84b12e66793e),pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL)))", SIGNABLE, {{"00209b9d5b45735d0e15df5b41d6594602d3de472262f7b75edc6cf5f3e3fa4e3ae4"}}, OutputType::BECH32, {{}}, /*spender_nlocktime=*/0, /*spender_nsequence=*/CTxIn::SEQUENCE_FINAL, {{ParseHex("292e2df59e3a22109200beed0cdc84b12e66793e"), ParseHex("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f")}});
+    // Same for hash256
+    Check("wsh(and_v(v:hash256(ae253ca2a54debcac7ecf414f6734f48c56421a08bb59182ff9f39a6fffdb588),pk(xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc)))", "wsh(and_v(v:hash256(ae253ca2a54debcac7ecf414f6734f48c56421a08bb59182ff9f39a6fffdb588),pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL)))", "wsh(and_v(v:hash256(ae253ca2a54debcac7ecf414f6734f48c56421a08bb59182ff9f39a6fffdb588),pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL)))", SIGNABLE_FAILS, {{"0020cf62bf97baf977aec69cbc290c372899f913337a9093e8f066ab59b8657a365c"}}, OutputType::BECH32, {{}}, /*spender_nlocktime=*/0, /*spender_nsequence=*/CTxIn::SEQUENCE_FINAL, {});
+    Check("wsh(and_v(v:hash256(ae253ca2a54debcac7ecf414f6734f48c56421a08bb59182ff9f39a6fffdb588),pk(xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc)))", "wsh(and_v(v:hash256(ae253ca2a54debcac7ecf414f6734f48c56421a08bb59182ff9f39a6fffdb588),pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL)))", "wsh(and_v(v:hash256(ae253ca2a54debcac7ecf414f6734f48c56421a08bb59182ff9f39a6fffdb588),pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL)))", SIGNABLE, {{"0020cf62bf97baf977aec69cbc290c372899f913337a9093e8f066ab59b8657a365c"}}, OutputType::BECH32, {{}}, /*spender_nlocktime=*/0, /*spender_nsequence=*/CTxIn::SEQUENCE_FINAL, {{ParseHex("ae253ca2a54debcac7ecf414f6734f48c56421a08bb59182ff9f39a6fffdb588"), ParseHex("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f")}});
 }
 
 BOOST_AUTO_TEST_SUITE_END()
diff --git a/src/test/fuzz/coins_view.cpp b/src/test/fuzz/coins_view.cpp
index e75dc3ce91..e80c772aa4 100644
--- a/src/test/fuzz/coins_view.cpp
+++ b/src/test/fuzz/coins_view.cpp
@@ -46,7 +46,7 @@ FUZZ_TARGET_INIT(coins_view, initialize_coins_view)
 {
     FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()};
     CCoinsView backend_coins_view;
-    CCoinsViewCache coins_view_cache{&backend_coins_view};
+    CCoinsViewCache coins_view_cache{&backend_coins_view, /*deterministic=*/true};
     COutPoint random_out_point;
     Coin random_coin;
     CMutableTransaction random_mutable_transaction;
diff --git a/src/test/fuzz/coinscache_sim.cpp b/src/test/fuzz/coinscache_sim.cpp
new file mode 100644
index 0000000000..f350c9d032
--- /dev/null
+++ b/src/test/fuzz/coinscache_sim.cpp
@@ -0,0 +1,478 @@
+// Copyright (c) 2023 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 <coins.h>
+#include <crypto/sha256.h>
+#include <primitives/transaction.h>
+#include <test/fuzz/fuzz.h>
+#include <test/fuzz/FuzzedDataProvider.h>
+#include <test/fuzz/util.h>
+
+#include <assert.h>
+#include <optional>
+#include <memory>
+#include <stdint.h>
+#include <vector>
+
+namespace {
+
+/** Number of distinct COutPoint values used in this test. */
+constexpr uint32_t NUM_OUTPOINTS = 256;
+/** Number of distinct Coin values used in this test (ignoring nHeight). */
+constexpr uint32_t NUM_COINS = 256;
+/** Maximum number CCoinsViewCache objects used in this test. */
+constexpr uint32_t MAX_CACHES = 4;
+/** Data type large enough to hold NUM_COINS-1. */
+using coinidx_type = uint8_t;
+
+struct PrecomputedData
+{
+    //! Randomly generated COutPoint values.
+    COutPoint outpoints[NUM_OUTPOINTS];
+
+    //! Randomly generated Coin values.
+    Coin coins[NUM_COINS];
+
+    PrecomputedData()
+    {
+        static const uint8_t PREFIX_O[1] = {'o'}; /** Hash prefix for outpoint hashes. */
+        static const uint8_t PREFIX_S[1] = {'s'}; /** Hash prefix for coins scriptPubKeys. */
+        static const uint8_t PREFIX_M[1] = {'m'}; /** Hash prefix for coins nValue/fCoinBase. */
+
+        for (uint32_t i = 0; i < NUM_OUTPOINTS; ++i) {
+            uint32_t idx = (i * 1200U) >> 12; /* Map 3 or 4 entries to same txid. */
+            const uint8_t ser[4] = {uint8_t(idx), uint8_t(idx >> 8), uint8_t(idx >> 16), uint8_t(idx >> 24)};
+            CSHA256().Write(PREFIX_O, 1).Write(ser, sizeof(ser)).Finalize(outpoints[i].hash.begin());
+            outpoints[i].n = i;
+        }
+
+        for (uint32_t i = 0; i < NUM_COINS; ++i) {
+            const uint8_t ser[4] = {uint8_t(i), uint8_t(i >> 8), uint8_t(i >> 16), uint8_t(i >> 24)};
+            uint256 hash;
+            CSHA256().Write(PREFIX_S, 1).Write(ser, sizeof(ser)).Finalize(hash.begin());
+            /* Convert hash to scriptPubkeys (of different lengths, so SanityCheck's cached memory
+             * usage check has a chance to detect mismatches). */
+            switch (i % 5U) {
+            case 0: /* P2PKH */
+                coins[i].out.scriptPubKey.resize(25);
+                coins[i].out.scriptPubKey[0] = OP_DUP;
+                coins[i].out.scriptPubKey[1] = OP_HASH160;
+                coins[i].out.scriptPubKey[2] = 20;
+                std::copy(hash.begin(), hash.begin() + 20, coins[i].out.scriptPubKey.begin() + 3);
+                coins[i].out.scriptPubKey[23] = OP_EQUALVERIFY;
+                coins[i].out.scriptPubKey[24] = OP_CHECKSIG;
+                break;
+            case 1: /* P2SH */
+                coins[i].out.scriptPubKey.resize(23);
+                coins[i].out.scriptPubKey[0] = OP_HASH160;
+                coins[i].out.scriptPubKey[1] = 20;
+                std::copy(hash.begin(), hash.begin() + 20, coins[i].out.scriptPubKey.begin() + 2);
+                coins[i].out.scriptPubKey[12] = OP_EQUAL;
+                break;
+            case 2: /* P2WPKH */
+                coins[i].out.scriptPubKey.resize(22);
+                coins[i].out.scriptPubKey[0] = OP_0;
+                coins[i].out.scriptPubKey[1] = 20;
+                std::copy(hash.begin(), hash.begin() + 20, coins[i].out.scriptPubKey.begin() + 2);
+                break;
+            case 3: /* P2WSH */
+                coins[i].out.scriptPubKey.resize(34);
+                coins[i].out.scriptPubKey[0] = OP_0;
+                coins[i].out.scriptPubKey[1] = 32;
+                std::copy(hash.begin(), hash.begin() + 32, coins[i].out.scriptPubKey.begin() + 2);
+                break;
+            case 4: /* P2TR */
+                coins[i].out.scriptPubKey.resize(34);
+                coins[i].out.scriptPubKey[0] = OP_1;
+                coins[i].out.scriptPubKey[1] = 32;
+                std::copy(hash.begin(), hash.begin() + 32, coins[i].out.scriptPubKey.begin() + 2);
+                break;
+            }
+            /* Hash again to construct nValue and fCoinBase. */
+            CSHA256().Write(PREFIX_M, 1).Write(ser, sizeof(ser)).Finalize(hash.begin());
+            coins[i].out.nValue = CAmount(hash.GetUint64(0) % MAX_MONEY);
+            coins[i].fCoinBase = (hash.GetUint64(1) & 7) == 0;
+            coins[i].nHeight = 0; /* Real nHeight used in simulation is set dynamically. */
+        }
+    }
+};
+
+enum class EntryType : uint8_t
+{
+    /* This entry in the cache does not exist (so we'd have to look in the parent cache). */
+    NONE,
+
+    /* This entry in the cache corresponds to an unspent coin. */
+    UNSPENT,
+
+    /* This entry in the cache corresponds to a spent coin. */
+    SPENT,
+};
+
+struct CacheEntry
+{
+    /* Type of entry. */
+    EntryType entrytype;
+
+    /* Index in the coins array this entry corresponds to (only if entrytype == UNSPENT). */
+    coinidx_type coinidx;
+
+    /* nHeight value for this entry (so the coins[coinidx].nHeight value is ignored; only if entrytype == UNSPENT). */
+    uint32_t height;
+};
+
+struct CacheLevel
+{
+    CacheEntry entry[NUM_OUTPOINTS];
+
+    void Wipe() {
+        for (uint32_t i = 0; i < NUM_OUTPOINTS; ++i) {
+            entry[i].entrytype = EntryType::NONE;
+        }
+    }
+};
+
+/** Class for the base of the hierarchy (roughly simulating a memory-backed CCoinsViewDB).
+ *
+ * The initial state consists of the empty UTXO set, though coins whose output index
+ * is 3 (mod 5) always have GetCoin() succeed (but returning an IsSpent() coin unless a UTXO
+ * exists). Coins whose output index is 4 (mod 5) have GetCoin() always succeed after being spent.
+ * This exercises code paths with spent, non-DIRTY cache entries.
+ */
+class CoinsViewBottom final : public CCoinsView
+{
+    std::map<COutPoint, Coin> m_data;
+
+public:
+    bool GetCoin(const COutPoint& outpoint, Coin& coin) const final
+    {
+        auto it = m_data.find(outpoint);
+        if (it == m_data.end()) {
+            if ((outpoint.n % 5) == 3) {
+                coin.Clear();
+                return true;
+            }
+            return false;
+        } else {
+            coin = it->second;
+            return true;
+        }
+    }
+
+    bool HaveCoin(const COutPoint& outpoint) const final
+    {
+        return m_data.count(outpoint);
+    }
+
+    uint256 GetBestBlock() const final { return {}; }
+    std::vector<uint256> GetHeadBlocks() const final { return {}; }
+    std::unique_ptr<CCoinsViewCursor> Cursor() const final { return {}; }
+    size_t EstimateSize() const final { return m_data.size(); }
+
+    bool BatchWrite(CCoinsMap& data, const uint256&, bool erase) final
+    {
+        for (auto it = data.begin(); it != data.end(); it = erase ? data.erase(it) : std::next(it)) {
+            if (it->second.flags & CCoinsCacheEntry::DIRTY) {
+                if (it->second.coin.IsSpent() && (it->first.n % 5) != 4) {
+                    m_data.erase(it->first);
+                } else if (erase) {
+                    m_data[it->first] = std::move(it->second.coin);
+                } else {
+                    m_data[it->first] = it->second.coin;
+                }
+            } else {
+                /* For non-dirty entries being written, compare them with what we have. */
+                auto it2 = m_data.find(it->first);
+                if (it->second.coin.IsSpent()) {
+                    assert(it2 == m_data.end() || it2->second.IsSpent());
+                } else {
+                    assert(it2 != m_data.end());
+                    assert(it->second.coin.out == it2->second.out);
+                    assert(it->second.coin.fCoinBase == it2->second.fCoinBase);
+                    assert(it->second.coin.nHeight == it2->second.nHeight);
+                }
+            }
+        }
+        return true;
+    }
+};
+
+} // namespace
+
+FUZZ_TARGET(coinscache_sim)
+{
+    /** Precomputed COutPoint and CCoins values. */
+    static const PrecomputedData data;
+
+    /** Dummy coinsview instance (base of the hierarchy). */
+    CoinsViewBottom bottom;
+    /** Real CCoinsViewCache objects. */
+    std::vector<std::unique_ptr<CCoinsViewCache>> caches;
+    /** Simulated cache data (sim_caches[0] matches bottom, sim_caches[i+1] matches caches[i]). */
+    CacheLevel sim_caches[MAX_CACHES + 1];
+    /** Current height in the simulation. */
+    uint32_t current_height = 1U;
+
+    // Initialize bottom simulated cache.
+    sim_caches[0].Wipe();
+
+    /** Helper lookup function in the simulated cache stack. */
+    auto lookup = [&](uint32_t outpointidx, int sim_idx = -1) -> std::optional<std::pair<coinidx_type, uint32_t>> {
+        uint32_t cache_idx = sim_idx == -1 ? caches.size() : sim_idx;
+        while (true) {
+            const auto& entry = sim_caches[cache_idx].entry[outpointidx];
+            if (entry.entrytype == EntryType::UNSPENT) {
+                return {{entry.coinidx, entry.height}};
+            } else if (entry.entrytype == EntryType::SPENT) {
+                return std::nullopt;
+            };
+            if (cache_idx == 0) break;
+            --cache_idx;
+        }
+        return std::nullopt;
+    };
+
+    /** Flush changes in top cache to the one below. */
+    auto flush = [&]() {
+        assert(caches.size() >= 1);
+        auto& cache = sim_caches[caches.size()];
+        auto& prev_cache = sim_caches[caches.size() - 1];
+        for (uint32_t outpointidx = 0; outpointidx < NUM_OUTPOINTS; ++outpointidx) {
+            if (cache.entry[outpointidx].entrytype != EntryType::NONE) {
+                prev_cache.entry[outpointidx] = cache.entry[outpointidx];
+                cache.entry[outpointidx].entrytype = EntryType::NONE;
+            }
+        }
+    };
+
+    // Main simulation loop: read commands from the fuzzer input, and apply them
+    // to both the real cache stack and the simulation.
+    FuzzedDataProvider provider(buffer.data(), buffer.size());
+    LIMITED_WHILE(provider.remaining_bytes(), 10000) {
+        // Every operation (except "Change height") moves current height forward,
+        // so it functions as a kind of epoch, making ~all UTXOs unique.
+        ++current_height;
+        // Make sure there is always at least one CCoinsViewCache.
+        if (caches.empty()) {
+            caches.emplace_back(new CCoinsViewCache(&bottom, /*deterministic=*/true));
+            sim_caches[caches.size()].Wipe();
+        }
+
+        // Execute command.
+        CallOneOf(
+            provider,
+
+            [&]() { // GetCoin
+                uint32_t outpointidx = provider.ConsumeIntegralInRange<uint32_t>(0, NUM_OUTPOINTS - 1);
+                // Look up in simulation data.
+                auto sim = lookup(outpointidx);
+                // Look up in real caches.
+                Coin realcoin;
+                auto real = caches.back()->GetCoin(data.outpoints[outpointidx], realcoin);
+                // Compare results.
+                if (!sim.has_value()) {
+                    assert(!real || realcoin.IsSpent());
+                } else {
+                    assert(real && !realcoin.IsSpent());
+                    const auto& simcoin = data.coins[sim->first];
+                    assert(realcoin.out == simcoin.out);
+                    assert(realcoin.fCoinBase == simcoin.fCoinBase);
+                    assert(realcoin.nHeight == sim->second);
+                }
+            },
+
+            [&]() { // HaveCoin
+                uint32_t outpointidx = provider.ConsumeIntegralInRange<uint32_t>(0, NUM_OUTPOINTS - 1);
+                // Look up in simulation data.
+                auto sim = lookup(outpointidx);
+                // Look up in real caches.
+                auto real = caches.back()->HaveCoin(data.outpoints[outpointidx]);
+                // Compare results.
+                assert(sim.has_value() == real);
+            },
+
+            [&]() { // HaveCoinInCache
+                uint32_t outpointidx = provider.ConsumeIntegralInRange<uint32_t>(0, NUM_OUTPOINTS - 1);
+                // Invoke on real cache (there is no equivalent in simulation, so nothing to compare result with).
+                (void)caches.back()->HaveCoinInCache(data.outpoints[outpointidx]);
+            },
+
+            [&]() { // AccessCoin
+                uint32_t outpointidx = provider.ConsumeIntegralInRange<uint32_t>(0, NUM_OUTPOINTS - 1);
+                // Look up in simulation data.
+                auto sim = lookup(outpointidx);
+                // Look up in real caches.
+                const auto& realcoin = caches.back()->AccessCoin(data.outpoints[outpointidx]);
+                // Compare results.
+                if (!sim.has_value()) {
+                    assert(realcoin.IsSpent());
+                } else {
+                    assert(!realcoin.IsSpent());
+                    const auto& simcoin = data.coins[sim->first];
+                    assert(simcoin.out == realcoin.out);
+                    assert(simcoin.fCoinBase == realcoin.fCoinBase);
+                    assert(realcoin.nHeight == sim->second);
+                }
+            },
+
+            [&]() { // AddCoin (only possible_overwrite if necessary)
+                uint32_t outpointidx = provider.ConsumeIntegralInRange<uint32_t>(0, NUM_OUTPOINTS - 1);
+                uint32_t coinidx = provider.ConsumeIntegralInRange<uint32_t>(0, NUM_COINS - 1);
+                // Look up in simulation data (to know whether we must set possible_overwrite or not).
+                auto sim = lookup(outpointidx);
+                // Invoke on real caches.
+                Coin coin = data.coins[coinidx];
+                coin.nHeight = current_height;
+                caches.back()->AddCoin(data.outpoints[outpointidx], std::move(coin), sim.has_value());
+                // Apply to simulation data.
+                auto& entry = sim_caches[caches.size()].entry[outpointidx];
+                entry.entrytype = EntryType::UNSPENT;
+                entry.coinidx = coinidx;
+                entry.height = current_height;
+            },
+
+            [&]() { // AddCoin (always possible_overwrite)
+                uint32_t outpointidx = provider.ConsumeIntegralInRange<uint32_t>(0, NUM_OUTPOINTS - 1);
+                uint32_t coinidx = provider.ConsumeIntegralInRange<uint32_t>(0, NUM_COINS - 1);
+                // Invoke on real caches.
+                Coin coin = data.coins[coinidx];
+                coin.nHeight = current_height;
+                caches.back()->AddCoin(data.outpoints[outpointidx], std::move(coin), true);
+                // Apply to simulation data.
+                auto& entry = sim_caches[caches.size()].entry[outpointidx];
+                entry.entrytype = EntryType::UNSPENT;
+                entry.coinidx = coinidx;
+                entry.height = current_height;
+            },
+
+            [&]() { // SpendCoin (moveto = nullptr)
+                uint32_t outpointidx = provider.ConsumeIntegralInRange<uint32_t>(0, NUM_OUTPOINTS - 1);
+                // Invoke on real caches.
+                caches.back()->SpendCoin(data.outpoints[outpointidx], nullptr);
+                // Apply to simulation data.
+                sim_caches[caches.size()].entry[outpointidx].entrytype = EntryType::SPENT;
+            },
+
+            [&]() { // SpendCoin (with moveto)
+                uint32_t outpointidx = provider.ConsumeIntegralInRange<uint32_t>(0, NUM_OUTPOINTS - 1);
+                // Look up in simulation data (to compare the returned *moveto with).
+                auto sim = lookup(outpointidx);
+                // Invoke on real caches.
+                Coin realcoin;
+                caches.back()->SpendCoin(data.outpoints[outpointidx], &realcoin);
+                // Apply to simulation data.
+                sim_caches[caches.size()].entry[outpointidx].entrytype = EntryType::SPENT;
+                // Compare *moveto with the value expected based on simulation data.
+                if (!sim.has_value()) {
+                    assert(realcoin.IsSpent());
+                } else {
+                    assert(!realcoin.IsSpent());
+                    const auto& simcoin = data.coins[sim->first];
+                    assert(simcoin.out == realcoin.out);
+                    assert(simcoin.fCoinBase == realcoin.fCoinBase);
+                    assert(realcoin.nHeight == sim->second);
+                }
+            },
+
+            [&]() { // Uncache
+                uint32_t outpointidx = provider.ConsumeIntegralInRange<uint32_t>(0, NUM_OUTPOINTS - 1);
+                // Apply to real caches (there is no equivalent in our simulation).
+                caches.back()->Uncache(data.outpoints[outpointidx]);
+            },
+
+            [&]() { // Add a cache level (if not already at the max).
+                if (caches.size() != MAX_CACHES) {
+                    // Apply to real caches.
+                    caches.emplace_back(new CCoinsViewCache(&*caches.back(), /*deterministic=*/true));
+                    // Apply to simulation data.
+                    sim_caches[caches.size()].Wipe();
+                }
+            },
+
+            [&]() { // Remove a cache level.
+                // Apply to real caches (this reduces caches.size(), implicitly doing the same on the simulation data).
+                caches.back()->SanityCheck();
+                caches.pop_back();
+            },
+
+            [&]() { // Flush.
+                // Apply to simulation data.
+                flush();
+                // Apply to real caches.
+                caches.back()->Flush();
+            },
+
+            [&]() { // Sync.
+                // Apply to simulation data (note that in our simulation, syncing and flushing is the same thing).
+                flush();
+                // Apply to real caches.
+                caches.back()->Sync();
+            },
+
+            [&]() { // Flush + ReallocateCache.
+                // Apply to simulation data.
+                flush();
+                // Apply to real caches.
+                caches.back()->Flush();
+                caches.back()->ReallocateCache();
+            },
+
+            [&]() { // GetCacheSize
+                (void)caches.back()->GetCacheSize();
+            },
+
+            [&]() { // DynamicMemoryUsage
+                (void)caches.back()->DynamicMemoryUsage();
+            },
+
+            [&]() { // Change height
+                current_height = provider.ConsumeIntegralInRange<uint32_t>(1, current_height - 1);
+            }
+        );
+    }
+
+    // Sanity check all the remaining caches
+    for (const auto& cache : caches) {
+        cache->SanityCheck();
+    }
+
+    // Full comparison between caches and simulation data, from bottom to top,
+    // as AccessCoin on a higher cache may affect caches below it.
+    for (unsigned sim_idx = 1; sim_idx <= caches.size(); ++sim_idx) {
+        auto& cache = *caches[sim_idx - 1];
+        size_t cache_size = 0;
+
+        for (uint32_t outpointidx = 0; outpointidx < NUM_OUTPOINTS; ++outpointidx) {
+            cache_size += cache.HaveCoinInCache(data.outpoints[outpointidx]);
+            const auto& real = cache.AccessCoin(data.outpoints[outpointidx]);
+            auto sim = lookup(outpointidx, sim_idx);
+            if (!sim.has_value()) {
+                assert(real.IsSpent());
+            } else {
+                assert(!real.IsSpent());
+                assert(real.out == data.coins[sim->first].out);
+                assert(real.fCoinBase == data.coins[sim->first].fCoinBase);
+                assert(real.nHeight == sim->second);
+            }
+        }
+
+        // HaveCoinInCache ignores spent coins, so GetCacheSize() may exceed it. */
+        assert(cache.GetCacheSize() >= cache_size);
+    }
+
+    // Compare the bottom coinsview (not a CCoinsViewCache) with sim_cache[0].
+    for (uint32_t outpointidx = 0; outpointidx < NUM_OUTPOINTS; ++outpointidx) {
+        Coin realcoin;
+        bool real = bottom.GetCoin(data.outpoints[outpointidx], realcoin);
+        auto sim = lookup(outpointidx, 0);
+        if (!sim.has_value()) {
+            assert(!real || realcoin.IsSpent());
+        } else {
+            assert(real && !realcoin.IsSpent());
+            assert(realcoin.out == data.coins[sim->first].out);
+            assert(realcoin.fCoinBase == data.coins[sim->first].fCoinBase);
+            assert(realcoin.nHeight == sim->second);
+        }
+    }
+}
diff --git a/src/test/fuzz/crypto_chacha20.cpp b/src/test/fuzz/crypto_chacha20.cpp
index 3f552a8cda..3fa445096a 100644
--- a/src/test/fuzz/crypto_chacha20.cpp
+++ b/src/test/fuzz/crypto_chacha20.cpp
@@ -6,6 +6,7 @@
 #include <test/fuzz/FuzzedDataProvider.h>
 #include <test/fuzz/fuzz.h>
 #include <test/fuzz/util.h>
+#include <test/util/xoroshiro128plusplus.h>
 
 #include <cstdint>
 #include <vector>
@@ -16,21 +17,21 @@ FUZZ_TARGET(crypto_chacha20)
 
     ChaCha20 chacha20;
     if (fuzzed_data_provider.ConsumeBool()) {
-        const std::vector<unsigned char> key = ConsumeFixedLengthByteVector(fuzzed_data_provider, fuzzed_data_provider.ConsumeIntegralInRange<size_t>(16, 32));
-        chacha20 = ChaCha20{key.data(), key.size()};
+        const std::vector<unsigned char> key = ConsumeFixedLengthByteVector(fuzzed_data_provider, 32);
+        chacha20 = ChaCha20{key.data()};
     }
     LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 10000) {
         CallOneOf(
             fuzzed_data_provider,
             [&] {
-                const std::vector<unsigned char> key = ConsumeFixedLengthByteVector(fuzzed_data_provider, fuzzed_data_provider.ConsumeIntegralInRange<size_t>(16, 32));
-                chacha20.SetKey(key.data(), key.size());
+                std::vector<unsigned char> key = ConsumeFixedLengthByteVector(fuzzed_data_provider, 32);
+                chacha20.SetKey32(key.data());
             },
             [&] {
                 chacha20.SetIV(fuzzed_data_provider.ConsumeIntegral<uint64_t>());
             },
             [&] {
-                chacha20.Seek(fuzzed_data_provider.ConsumeIntegral<uint64_t>());
+                chacha20.Seek64(fuzzed_data_provider.ConsumeIntegral<uint64_t>());
             },
             [&] {
                 std::vector<uint8_t> output(fuzzed_data_provider.ConsumeIntegralInRange<size_t>(0, 4096));
@@ -43,3 +44,110 @@ FUZZ_TARGET(crypto_chacha20)
             });
     }
 }
+
+namespace
+{
+
+/** Fuzzer that invokes ChaCha20::Crypt() or ChaCha20::Keystream multiple times:
+    once for a large block at once, and then the same data in chunks, comparing
+    the outcome.
+
+    If UseCrypt, seeded Xoroshiro128++ output is used as input to Crypt().
+    If not, Keystream() is used directly, or sequences of 0x00 are encrypted.
+*/
+template<bool UseCrypt>
+void ChaCha20SplitFuzz(FuzzedDataProvider& provider)
+{
+    // Determine key, iv, start position, length.
+    unsigned char key[32] = {0};
+    auto key_bytes = provider.ConsumeBytes<unsigned char>(32);
+    std::copy(key_bytes.begin(), key_bytes.end(), key);
+    uint64_t iv = provider.ConsumeIntegral<uint64_t>();
+    uint64_t total_bytes = provider.ConsumeIntegralInRange<uint64_t>(0, 1000000);
+    /* ~x = 2^64 - 1 - x, so ~(total_bytes >> 6) is the maximal seek position. */
+    uint64_t seek = provider.ConsumeIntegralInRange<uint64_t>(0, ~(total_bytes >> 6));
+
+    // Initialize two ChaCha20 ciphers, with the same key/iv/position.
+    ChaCha20 crypt1(key);
+    ChaCha20 crypt2(key);
+    crypt1.SetIV(iv);
+    crypt1.Seek64(seek);
+    crypt2.SetIV(iv);
+    crypt2.Seek64(seek);
+
+    // Construct vectors with data.
+    std::vector<unsigned char> data1, data2;
+    data1.resize(total_bytes);
+    data2.resize(total_bytes);
+
+    // If using Crypt(), initialize data1 and data2 with the same Xoroshiro128++ based
+    // stream.
+    if constexpr (UseCrypt) {
+        uint64_t seed = provider.ConsumeIntegral<uint64_t>();
+        XoRoShiRo128PlusPlus rng(seed);
+        uint64_t bytes = 0;
+        while (bytes < (total_bytes & ~uint64_t{7})) {
+            uint64_t val = rng();
+            WriteLE64(data1.data() + bytes, val);
+            WriteLE64(data2.data() + bytes, val);
+            bytes += 8;
+        }
+        if (bytes < total_bytes) {
+            unsigned char valbytes[8];
+            uint64_t val = rng();
+            WriteLE64(valbytes, val);
+            std::copy(valbytes, valbytes + (total_bytes - bytes), data1.data() + bytes);
+            std::copy(valbytes, valbytes + (total_bytes - bytes), data2.data() + bytes);
+        }
+    }
+
+    // Whether UseCrypt is used or not, the two byte arrays must match.
+    assert(data1 == data2);
+
+    // Encrypt data1, the whole array at once.
+    if constexpr (UseCrypt) {
+        crypt1.Crypt(data1.data(), data1.data(), total_bytes);
+    } else {
+        crypt1.Keystream(data1.data(), total_bytes);
+    }
+
+    // Encrypt data2, in at most 256 chunks.
+    uint64_t bytes2 = 0;
+    int iter = 0;
+    while (true) {
+        bool is_last = (iter == 255) || (bytes2 == total_bytes) || provider.ConsumeBool();
+        ++iter;
+        // Determine how many bytes to encrypt in this chunk: a fuzzer-determined
+        // amount for all but the last chunk (which processes all remaining bytes).
+        uint64_t now = is_last ? total_bytes - bytes2 :
+            provider.ConsumeIntegralInRange<uint64_t>(0, total_bytes - bytes2);
+        // For each chunk, consider using Crypt() even when UseCrypt is false.
+        // This tests that Keystream() has the same behavior as Crypt() applied
+        // to 0x00 input bytes.
+        if (UseCrypt || provider.ConsumeBool()) {
+            crypt2.Crypt(data2.data() + bytes2, data2.data() + bytes2, now);
+        } else {
+            crypt2.Keystream(data2.data() + bytes2, now);
+        }
+        bytes2 += now;
+        if (is_last) break;
+    }
+    // We should have processed everything now.
+    assert(bytes2 == total_bytes);
+    // And the result should match.
+    assert(data1 == data2);
+}
+
+} // namespace
+
+FUZZ_TARGET(chacha20_split_crypt)
+{
+    FuzzedDataProvider provider{buffer.data(), buffer.size()};
+    ChaCha20SplitFuzz<true>(provider);
+}
+
+FUZZ_TARGET(chacha20_split_keystream)
+{
+    FuzzedDataProvider provider{buffer.data(), buffer.size()};
+    ChaCha20SplitFuzz<false>(provider);
+}
diff --git a/src/test/fuzz/crypto_diff_fuzz_chacha20.cpp b/src/test/fuzz/crypto_diff_fuzz_chacha20.cpp
index 1b89d55773..78fee48de6 100644
--- a/src/test/fuzz/crypto_diff_fuzz_chacha20.cpp
+++ b/src/test/fuzz/crypto_diff_fuzz_chacha20.cpp
@@ -267,32 +267,33 @@ void ECRYPT_keystream_bytes(ECRYPT_ctx* x, u8* stream, u32 bytes)
 
 FUZZ_TARGET(crypto_diff_fuzz_chacha20)
 {
+    static const unsigned char ZEROKEY[32] = {0};
     FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()};
 
     ChaCha20 chacha20;
     ECRYPT_ctx ctx;
-    // D. J. Bernstein doesn't initialise ctx to 0 while Bitcoin Core initialises chacha20 to 0 in the constructor
-    for (int i = 0; i < 16; i++) {
-        ctx.input[i] = 0;
-    }
 
     if (fuzzed_data_provider.ConsumeBool()) {
-        const std::vector<unsigned char> key = ConsumeFixedLengthByteVector(fuzzed_data_provider, fuzzed_data_provider.ConsumeIntegralInRange<size_t>(16, 32));
-        chacha20 = ChaCha20{key.data(), key.size()};
+        const std::vector<unsigned char> key = ConsumeFixedLengthByteVector(fuzzed_data_provider, 32);
+        chacha20 = ChaCha20{key.data()};
         ECRYPT_keysetup(&ctx, key.data(), key.size() * 8, 0);
-        // ECRYPT_keysetup() doesn't set the counter and nonce to 0 while SetKey() does
-        uint8_t iv[8] = {0, 0, 0, 0, 0, 0, 0, 0};
-        ECRYPT_ivsetup(&ctx, iv);
+    } else {
+        // The default ChaCha20 constructor is equivalent to using the all-0 key.
+        ECRYPT_keysetup(&ctx, ZEROKEY, 256, 0);
     }
 
+    // ECRYPT_keysetup() doesn't set the counter and nonce to 0 while SetKey32() does
+    static const uint8_t iv[8] = {0, 0, 0, 0, 0, 0, 0, 0};
+    ECRYPT_ivsetup(&ctx, iv);
+
     LIMITED_WHILE (fuzzed_data_provider.ConsumeBool(), 3000) {
         CallOneOf(
             fuzzed_data_provider,
             [&] {
-                const std::vector<unsigned char> key = ConsumeFixedLengthByteVector(fuzzed_data_provider, fuzzed_data_provider.ConsumeIntegralInRange<size_t>(16, 32));
-                chacha20.SetKey(key.data(), key.size());
+                const std::vector<unsigned char> key = ConsumeFixedLengthByteVector(fuzzed_data_provider, 32);
+                chacha20.SetKey32(key.data());
                 ECRYPT_keysetup(&ctx, key.data(), key.size() * 8, 0);
-                // ECRYPT_keysetup() doesn't set the counter and nonce to 0 while SetKey() does
+                // ECRYPT_keysetup() doesn't set the counter and nonce to 0 while SetKey32() does
                 uint8_t iv[8] = {0, 0, 0, 0, 0, 0, 0, 0};
                 ECRYPT_ivsetup(&ctx, iv);
             },
@@ -304,26 +305,32 @@ FUZZ_TARGET(crypto_diff_fuzz_chacha20)
             },
             [&] {
                 uint64_t counter = fuzzed_data_provider.ConsumeIntegral<uint64_t>();
-                chacha20.Seek(counter);
+                chacha20.Seek64(counter);
                 ctx.input[12] = counter;
                 ctx.input[13] = counter >> 32;
             },
             [&] {
                 uint32_t integralInRange = fuzzed_data_provider.ConsumeIntegralInRange<size_t>(0, 4096);
+                // DJB's version seeks forward to a multiple of 64 bytes after every operation. Correct for that.
+                uint64_t pos = ctx.input[12] + (((uint64_t)ctx.input[13]) << 32) + ((integralInRange + 63) >> 6);
                 std::vector<uint8_t> output(integralInRange);
                 chacha20.Keystream(output.data(), output.size());
                 std::vector<uint8_t> djb_output(integralInRange);
                 ECRYPT_keystream_bytes(&ctx, djb_output.data(), djb_output.size());
                 assert(output == djb_output);
+                chacha20.Seek64(pos);
             },
             [&] {
                 uint32_t integralInRange = fuzzed_data_provider.ConsumeIntegralInRange<size_t>(0, 4096);
+                // DJB's version seeks forward to a multiple of 64 bytes after every operation. Correct for that.
+                uint64_t pos = ctx.input[12] + (((uint64_t)ctx.input[13]) << 32) + ((integralInRange + 63) >> 6);
                 std::vector<uint8_t> output(integralInRange);
                 const std::vector<uint8_t> input = ConsumeFixedLengthByteVector(fuzzed_data_provider, output.size());
                 chacha20.Crypt(input.data(), output.data(), input.size());
                 std::vector<uint8_t> djb_output(integralInRange);
                 ECRYPT_encrypt_bytes(&ctx, input.data(), djb_output.data(), input.size());
                 assert(output == djb_output);
+                chacha20.Seek64(pos);
             });
     }
 }
diff --git a/src/test/fuzz/miniscript.cpp b/src/test/fuzz/miniscript.cpp
index d5667e0cf3..1b791fc19c 100644
--- a/src/test/fuzz/miniscript.cpp
+++ b/src/test/fuzz/miniscript.cpp
@@ -14,14 +14,25 @@
 
 namespace {
 
-//! Some pre-computed data for more efficient string roundtrips.
+//! Some pre-computed data for more efficient string roundtrips and to simulate challenges.
 struct TestData {
     typedef CPubKey Key;
 
-    // Precomputed public keys.
+    // Precomputed public keys, and a dummy signature for each of them.
     std::vector<Key> dummy_keys;
     std::map<Key, int> dummy_key_idx_map;
     std::map<CKeyID, Key> dummy_keys_map;
+    std::map<Key, std::pair<std::vector<unsigned char>, bool>> dummy_sigs;
+
+    // Precomputed hashes of each kind.
+    std::vector<std::vector<unsigned char>> sha256;
+    std::vector<std::vector<unsigned char>> ripemd160;
+    std::vector<std::vector<unsigned char>> hash256;
+    std::vector<std::vector<unsigned char>> hash160;
+    std::map<std::vector<unsigned char>, std::vector<unsigned char>> sha256_preimages;
+    std::map<std::vector<unsigned char>, std::vector<unsigned char>> ripemd160_preimages;
+    std::map<std::vector<unsigned char>, std::vector<unsigned char>> hash256_preimages;
+    std::map<std::vector<unsigned char>, std::vector<unsigned char>> hash160_preimages;
 
     //! Set the precomputed data.
     void Init() {
@@ -35,6 +46,28 @@ struct TestData {
             dummy_keys.push_back(pubkey);
             dummy_key_idx_map.emplace(pubkey, i);
             dummy_keys_map.insert({pubkey.GetID(), pubkey});
+
+            std::vector<unsigned char> sig;
+            privkey.Sign(uint256S(""), sig);
+            sig.push_back(1); // SIGHASH_ALL
+            dummy_sigs.insert({pubkey, {sig, i & 1}});
+
+            std::vector<unsigned char> hash;
+            hash.resize(32);
+            CSHA256().Write(keydata, 32).Finalize(hash.data());
+            sha256.push_back(hash);
+            if (i & 1) sha256_preimages[hash] = std::vector<unsigned char>(keydata, keydata + 32);
+            CHash256().Write(keydata).Finalize(hash);
+            hash256.push_back(hash);
+            if (i & 1) hash256_preimages[hash] = std::vector<unsigned char>(keydata, keydata + 32);
+            hash.resize(20);
+            CRIPEMD160().Write(keydata, 32).Finalize(hash.data());
+            assert(hash.size() == 20);
+            ripemd160.push_back(hash);
+            if (i & 1) ripemd160_preimages[hash] = std::vector<unsigned char>(keydata, keydata + 32);
+            CHash160().Write(keydata).Finalize(hash);
+            hash160.push_back(hash);
+            if (i & 1) hash160_preimages[hash] = std::vector<unsigned char>(keydata, keydata + 32);
         }
     }
 } TEST_DATA;
@@ -59,6 +92,17 @@ struct ParserContext {
         return HexStr(Span{&idx, 1});
     }
 
+    std::vector<unsigned char> ToPKBytes(const Key& key) const
+    {
+        return {key.begin(), key.end()};
+    }
+
+    std::vector<unsigned char> ToPKHBytes(const Key& key) const
+    {
+        const auto h = Hash160(key);
+        return {h.begin(), h.end()};
+    }
+
     template<typename I>
     std::optional<Key> FromString(I first, I last) const {
         if (last - first != 2) return {};
@@ -69,7 +113,7 @@ struct ParserContext {
 
     template<typename I>
     std::optional<Key> FromPKBytes(I first, I last) const {
-        Key key;
+        CPubKey key;
         key.Set(first, last);
         if (!key.IsValid()) return {};
         return key;
@@ -130,6 +174,732 @@ struct ScriptParserContext {
     }
 } SCRIPT_PARSER_CONTEXT;
 
+//! Context to produce a satisfaction for a Miniscript node using the pre-computed data.
+struct SatisfierContext: ParserContext {
+    // Timelock challenges satisfaction. Make the value (deterministically) vary to explore different
+    // paths.
+    bool CheckAfter(uint32_t value) const { return value % 2; }
+    bool CheckOlder(uint32_t value) const { return value % 2; }
+
+    // Signature challenges fulfilled with a dummy signature, if it was one of our dummy keys.
+    miniscript::Availability Sign(const CPubKey& key, std::vector<unsigned char>& sig) const {
+        const auto it = TEST_DATA.dummy_sigs.find(key);
+        if (it == TEST_DATA.dummy_sigs.end()) return miniscript::Availability::NO;
+        if (it->second.second) {
+            // Key is "available"
+            sig = it->second.first;
+            return miniscript::Availability::YES;
+        } else {
+            return miniscript::Availability::NO;
+        }
+    }
+
+    //! Lookup generalization for all the hash satisfactions below
+    miniscript::Availability LookupHash(const std::vector<unsigned char>& hash, std::vector<unsigned char>& preimage,
+                                        const std::map<std::vector<unsigned char>, std::vector<unsigned char>>& map) const
+    {
+        const auto it = map.find(hash);
+        if (it == map.end()) return miniscript::Availability::NO;
+        preimage = it->second;
+        return miniscript::Availability::YES;
+    }
+    miniscript::Availability SatSHA256(const std::vector<unsigned char>& hash, std::vector<unsigned char>& preimage) const {
+        return LookupHash(hash, preimage, TEST_DATA.sha256_preimages);
+    }
+    miniscript::Availability SatRIPEMD160(const std::vector<unsigned char>& hash, std::vector<unsigned char>& preimage) const {
+        return LookupHash(hash, preimage, TEST_DATA.ripemd160_preimages);
+    }
+    miniscript::Availability SatHASH256(const std::vector<unsigned char>& hash, std::vector<unsigned char>& preimage) const {
+        return LookupHash(hash, preimage, TEST_DATA.hash256_preimages);
+    }
+    miniscript::Availability SatHASH160(const std::vector<unsigned char>& hash, std::vector<unsigned char>& preimage) const {
+        return LookupHash(hash, preimage, TEST_DATA.hash160_preimages);
+    }
+} SATISFIER_CTX;
+
+//! Context to check a satisfaction against the pre-computed data.
+struct CheckerContext: BaseSignatureChecker {
+    TestData *test_data;
+
+    // Signature checker methods. Checks the right dummy signature is used.
+    bool CheckECDSASignature(const std::vector<unsigned char>& sig, const std::vector<unsigned char>& vchPubKey,
+                             const CScript& scriptCode, SigVersion sigversion) const override
+    {
+        const CPubKey key{vchPubKey};
+        const auto it = TEST_DATA.dummy_sigs.find(key);
+        if (it == TEST_DATA.dummy_sigs.end()) return false;
+        return it->second.first == sig;
+    }
+    bool CheckLockTime(const CScriptNum& nLockTime) const override { return nLockTime.GetInt64() & 1; }
+    bool CheckSequence(const CScriptNum& nSequence) const override { return nSequence.GetInt64() & 1; }
+} CHECKER_CTX;
+
+//! Context to check for duplicates when instancing a Node.
+struct KeyComparator {
+    bool KeyCompare(const CPubKey& a, const CPubKey& b) const {
+        return a < b;
+    }
+} KEY_COMP;
+
+// A dummy scriptsig to pass to VerifyScript (we always use Segwit v0).
+const CScript DUMMY_SCRIPTSIG;
+
+using Fragment = miniscript::Fragment;
+using NodeRef = miniscript::NodeRef<CPubKey>;
+using Node = miniscript::Node<CPubKey>;
+using Type = miniscript::Type;
+// https://github.com/llvm/llvm-project/issues/53444
+// NOLINTNEXTLINE(misc-unused-using-decls)
+using miniscript::operator"" _mst;
+
+//! Construct a miniscript node as a shared_ptr.
+template<typename... Args> NodeRef MakeNodeRef(Args&&... args) { return miniscript::MakeNodeRef<CPubKey>(KEY_COMP, std::forward<Args>(args)...); }
+
+/** Information about a yet to be constructed Miniscript node. */
+struct NodeInfo {
+    //! The type of this node
+    Fragment fragment;
+    //! Number of subs of this node
+    uint8_t n_subs;
+    //! The timelock value for older() and after(), the threshold value for multi() and thresh()
+    uint32_t k;
+    //! Keys for this node, if it has some
+    std::vector<CPubKey> keys;
+    //! The hash value for this node, if it has one
+    std::vector<unsigned char> hash;
+    //! The type requirements for the children of this node.
+    std::vector<Type> subtypes;
+
+    NodeInfo(Fragment frag): fragment(frag), n_subs(0), k(0) {}
+    NodeInfo(Fragment frag, CPubKey key): fragment(frag), n_subs(0), k(0), keys({key}) {}
+    NodeInfo(Fragment frag, uint32_t _k): fragment(frag), n_subs(0), k(_k) {}
+    NodeInfo(Fragment frag, std::vector<unsigned char> h): fragment(frag), n_subs(0), k(0), hash(std::move(h)) {}
+    NodeInfo(uint8_t subs, Fragment frag): fragment(frag), n_subs(subs), k(0), subtypes(subs, ""_mst) {}
+    NodeInfo(uint8_t subs, Fragment frag, uint32_t _k): fragment(frag), n_subs(subs), k(_k), subtypes(subs, ""_mst)  {}
+    NodeInfo(std::vector<Type> subt, Fragment frag): fragment(frag), n_subs(subt.size()), k(0), subtypes(std::move(subt)) {}
+    NodeInfo(std::vector<Type> subt, Fragment frag, uint32_t _k): fragment(frag), n_subs(subt.size()), k(_k), subtypes(std::move(subt))  {}
+    NodeInfo(Fragment frag, uint32_t _k, std::vector<CPubKey> _keys): fragment(frag), n_subs(0), k(_k), keys(std::move(_keys)) {}
+};
+
+/** Pick an index in a collection from a single byte in the fuzzer's output. */
+template<typename T, typename A>
+T ConsumeIndex(FuzzedDataProvider& provider, A& col) {
+    const uint8_t i = provider.ConsumeIntegral<uint8_t>();
+    return col[i];
+}
+
+CPubKey ConsumePubKey(FuzzedDataProvider& provider) {
+    return ConsumeIndex<CPubKey>(provider, TEST_DATA.dummy_keys);
+}
+
+std::vector<unsigned char> ConsumeSha256(FuzzedDataProvider& provider) {
+    return ConsumeIndex<std::vector<unsigned char>>(provider, TEST_DATA.sha256);
+}
+
+std::vector<unsigned char> ConsumeHash256(FuzzedDataProvider& provider) {
+    return ConsumeIndex<std::vector<unsigned char>>(provider, TEST_DATA.hash256);
+}
+
+std::vector<unsigned char> ConsumeRipemd160(FuzzedDataProvider& provider) {
+    return ConsumeIndex<std::vector<unsigned char>>(provider, TEST_DATA.ripemd160);
+}
+
+std::vector<unsigned char> ConsumeHash160(FuzzedDataProvider& provider) {
+    return ConsumeIndex<std::vector<unsigned char>>(provider, TEST_DATA.hash160);
+}
+
+std::optional<uint32_t> ConsumeTimeLock(FuzzedDataProvider& provider) {
+    const uint32_t k = provider.ConsumeIntegral<uint32_t>();
+    if (k == 0 || k >= 0x80000000) return {};
+    return k;
+}
+
+/**
+ * Consume a Miniscript node from the fuzzer's output.
+ *
+ * This version is intended to have a fixed, stable, encoding for Miniscript nodes:
+ *  - The first byte sets the type of the fragment. 0, 1 and all non-leaf fragments but thresh() are a
+ *    single byte.
+ *  - For the other leaf fragments, the following bytes depend on their type.
+ *    - For older() and after(), the next 4 bytes define the timelock value.
+ *    - For pk_k(), pk_h(), and all hashes, the next byte defines the index of the value in the test data.
+ *    - For multi(), the next 2 bytes define respectively the threshold and the number of keys. Then as many
+ *      bytes as the number of keys define the index of each key in the test data.
+ *    - For thresh(), the next byte defines the threshold value and the following one the number of subs.
+ */
+std::optional<NodeInfo> ConsumeNodeStable(FuzzedDataProvider& provider) {
+    switch (provider.ConsumeIntegral<uint8_t>()) {
+        case 0: return {{Fragment::JUST_0}};
+        case 1: return {{Fragment::JUST_1}};
+        case 2: return {{Fragment::PK_K, ConsumePubKey(provider)}};
+        case 3: return {{Fragment::PK_H, ConsumePubKey(provider)}};
+        case 4: {
+            const auto k = ConsumeTimeLock(provider);
+            if (!k) return {};
+            return {{Fragment::OLDER, *k}};
+        }
+        case 5: {
+            const auto k = ConsumeTimeLock(provider);
+            if (!k) return {};
+            return {{Fragment::AFTER, *k}};
+        }
+        case 6: return {{Fragment::SHA256, ConsumeSha256(provider)}};
+        case 7: return {{Fragment::HASH256, ConsumeHash256(provider)}};
+        case 8: return {{Fragment::RIPEMD160, ConsumeRipemd160(provider)}};
+        case 9: return {{Fragment::HASH160, ConsumeHash160(provider)}};
+        case 10: {
+            const auto k = provider.ConsumeIntegral<uint8_t>();
+            const auto n_keys = provider.ConsumeIntegral<uint8_t>();
+            if (n_keys > 20 || k == 0 || k > n_keys) return {};
+            std::vector<CPubKey> keys{n_keys};
+            for (auto& key: keys) key = ConsumePubKey(provider);
+            return {{Fragment::MULTI, k, std::move(keys)}};
+        }
+        case 11: return {{3, Fragment::ANDOR}};
+        case 12: return {{2, Fragment::AND_V}};
+        case 13: return {{2, Fragment::AND_B}};
+        case 15: return {{2, Fragment::OR_B}};
+        case 16: return {{2, Fragment::OR_C}};
+        case 17: return {{2, Fragment::OR_D}};
+        case 18: return {{2, Fragment::OR_I}};
+        case 19: {
+            auto k = provider.ConsumeIntegral<uint8_t>();
+            auto n_subs = provider.ConsumeIntegral<uint8_t>();
+            if (k == 0 || k > n_subs) return {};
+            return {{n_subs, Fragment::THRESH, k}};
+        }
+        case 20: return {{1, Fragment::WRAP_A}};
+        case 21: return {{1, Fragment::WRAP_S}};
+        case 22: return {{1, Fragment::WRAP_C}};
+        case 23: return {{1, Fragment::WRAP_D}};
+        case 24: return {{1, Fragment::WRAP_V}};
+        case 25: return {{1, Fragment::WRAP_J}};
+        case 26: return {{1, Fragment::WRAP_N}};
+        default:
+            break;
+    }
+    return {};
+}
+
+/* This structure contains a table which for each "target" Type a list of recipes
+ * to construct it, automatically inferred from the behavior of ComputeType.
+ * Note that the Types here are not the final types of the constructed Nodes, but
+ * just the subset that are required. For example, a recipe for the "Bo" type
+ * might construct a "Bondu" sha256() NodeInfo, but cannot construct a "Bz" older().
+ * Each recipe is a Fragment together with a list of required types for its subnodes.
+ */
+struct SmartInfo
+{
+    using recipe = std::pair<Fragment, std::vector<Type>>;
+    std::map<Type, std::vector<recipe>> table;
+
+    void Init()
+    {
+        /* Construct a set of interesting type requirements to reason with (sections of BKVWzondu). */
+        std::vector<Type> types;
+        for (int base = 0; base < 4; ++base) { /* select from B,K,V,W */
+            Type type_base = base == 0 ? "B"_mst : base == 1 ? "K"_mst : base == 2 ? "V"_mst : "W"_mst;
+            for (int zo = 0; zo < 3; ++zo) { /* select from z,o,(none) */
+                Type type_zo = zo == 0 ? "z"_mst : zo == 1 ? "o"_mst : ""_mst;
+                for (int n = 0; n < 2; ++n) { /* select from (none),n */
+                    if (zo == 0 && n == 1) continue; /* z conflicts with n */
+                    if (base == 3 && n == 1) continue; /* W conficts with n */
+                    Type type_n = n == 0 ? ""_mst : "n"_mst;
+                    for (int d = 0; d < 2; ++d) { /* select from (none),d */
+                        if (base == 2 && d == 1) continue; /* V conflicts with d */
+                        Type type_d = d == 0 ? ""_mst : "d"_mst;
+                        for (int u = 0; u < 2; ++u) { /* select from (none),u */
+                            if (base == 2 && u == 1) continue; /* V conflicts with u */
+                            Type type_u = u == 0 ? ""_mst : "u"_mst;
+                            Type type = type_base | type_zo | type_n | type_d | type_u;
+                            types.push_back(type);
+                        }
+                    }
+                }
+            }
+        }
+
+        /* We define a recipe a to be a super-recipe of recipe b if they use the same
+         * fragment, the same number of subexpressions, and each of a's subexpression
+         * types is a supertype of the corresponding subexpression type of b.
+         * Within the set of recipes for the construction of a given type requirement,
+         * no recipe should be a super-recipe of another (as the super-recipe is
+         * applicable in every place the sub-recipe is, the sub-recipe is redundant). */
+        auto is_super_of = [](const recipe& a, const recipe& b) {
+            if (a.first != b.first) return false;
+            if (a.second.size() != b.second.size()) return false;
+            for (size_t i = 0; i < a.second.size(); ++i) {
+                if (!(b.second[i] << a.second[i])) return false;
+            }
+            return true;
+        };
+
+        /* Sort the type requirements. Subtypes will always sort later (e.g. Bondu will
+         * sort after Bo or Bu). As we'll be constructing recipes using these types, in
+         * order, in what follows, we'll construct super-recipes before sub-recipes.
+         * That means we never need to go back and delete a sub-recipe because a
+         * super-recipe got added. */
+        std::sort(types.begin(), types.end());
+
+        // Iterate over all possible fragments.
+        for (int fragidx = 0; fragidx <= int(Fragment::MULTI); ++fragidx) {
+            int sub_count = 0; //!< The minimum number of child nodes this recipe has.
+            int sub_range = 1; //!< The maximum number of child nodes for this recipe is sub_count+sub_range-1.
+            size_t data_size = 0;
+            size_t n_keys = 0;
+            uint32_t k = 0;
+            Fragment frag{fragidx};
+
+            // Based on the fragment, determine #subs/data/k/keys to pass to ComputeType. */
+            switch (frag) {
+                case Fragment::PK_K:
+                case Fragment::PK_H:
+                    n_keys = 1;
+                    break;
+                case Fragment::MULTI:
+                    n_keys = 1;
+                    k = 1;
+                    break;
+                case Fragment::OLDER:
+                case Fragment::AFTER:
+                    k = 1;
+                    break;
+                case Fragment::SHA256:
+                case Fragment::HASH256:
+                    data_size = 32;
+                    break;
+                case Fragment::RIPEMD160:
+                case Fragment::HASH160:
+                    data_size = 20;
+                    break;
+                case Fragment::JUST_0:
+                case Fragment::JUST_1:
+                    break;
+                case Fragment::WRAP_A:
+                case Fragment::WRAP_S:
+                case Fragment::WRAP_C:
+                case Fragment::WRAP_D:
+                case Fragment::WRAP_V:
+                case Fragment::WRAP_J:
+                case Fragment::WRAP_N:
+                    sub_count = 1;
+                    break;
+                case Fragment::AND_V:
+                case Fragment::AND_B:
+                case Fragment::OR_B:
+                case Fragment::OR_C:
+                case Fragment::OR_D:
+                case Fragment::OR_I:
+                    sub_count = 2;
+                    break;
+                case Fragment::ANDOR:
+                    sub_count = 3;
+                    break;
+                case Fragment::THRESH:
+                    // Thresh logic is executed for 1 and 2 arguments. Larger numbers use ad-hoc code to extend.
+                    sub_count = 1;
+                    sub_range = 2;
+                    k = 1;
+                    break;
+            }
+
+            // Iterate over the number of subnodes (sub_count...sub_count+sub_range-1).
+            std::vector<Type> subt;
+            for (int subs = sub_count; subs < sub_count + sub_range; ++subs) {
+                // Iterate over the possible subnode types (at most 3).
+                for (Type x : types) {
+                    for (Type y : types) {
+                        for (Type z : types) {
+                            // Compute the resulting type of a node with the selected fragment / subnode types.
+                            subt.clear();
+                            if (subs > 0) subt.push_back(x);
+                            if (subs > 1) subt.push_back(y);
+                            if (subs > 2) subt.push_back(z);
+                            Type res = miniscript::internal::ComputeType(frag, x, y, z, subt, k, data_size, subs, n_keys);
+                            // Continue if the result is not a valid node.
+                            if ((res << "K"_mst) + (res << "V"_mst) + (res << "B"_mst) + (res << "W"_mst) != 1) continue;
+
+                            recipe entry{frag, subt};
+                            auto super_of_entry = [&](const recipe& rec) { return is_super_of(rec, entry); };
+                            // Iterate over all supertypes of res (because if e.g. our selected fragment/subnodes result
+                            // in a Bondu, they can form a recipe that is also applicable for constructing a B, Bou, Bdu, ...).
+                            for (Type s : types) {
+                                if ((res & "BKVWzondu"_mst) << s) {
+                                    auto& recipes = table[s];
+                                    // If we don't already have a super-recipe to the new one, add it.
+                                    if (!std::any_of(recipes.begin(), recipes.end(), super_of_entry)) {
+                                        recipes.push_back(entry);
+                                    }
+                                }
+                            }
+
+                            if (subs <= 2) break;
+                        }
+                        if (subs <= 1) break;
+                    }
+                    if (subs <= 0) break;
+                }
+            }
+        }
+
+        /* Find which types are useful. The fuzzer logic only cares about constructing
+         * B,V,K,W nodes, so any type that isn't needed in any recipe (directly or
+         * indirectly) for the construction of those is uninteresting. */
+        std::set<Type> useful_types{"B"_mst, "V"_mst, "K"_mst, "W"_mst};
+        // Find the transitive closure by adding types until the set of types does not change.
+        while (true) {
+            size_t set_size = useful_types.size();
+            for (const auto& [type, recipes] : table) {
+                if (useful_types.count(type) != 0) {
+                    for (const auto& [_, subtypes] : recipes) {
+                        for (auto subtype : subtypes) useful_types.insert(subtype);
+                    }
+                }
+            }
+            if (useful_types.size() == set_size) break;
+        }
+        // Remove all rules that construct uninteresting types.
+        for (auto type_it = table.begin(); type_it != table.end();) {
+            if (useful_types.count(type_it->first) == 0) {
+                type_it = table.erase(type_it);
+            } else {
+                ++type_it;
+            }
+        }
+
+        /* Find which types are constructible. A type is constructible if there is a leaf
+         * node recipe for constructing it, or a recipe whose subnodes are all constructible.
+         * Types can be non-constructible because they have no recipes to begin with,
+         * because they can only be constructed using recipes that involve otherwise
+         * non-constructible types, or because they require infinite recursion. */
+        std::set<Type> constructible_types{};
+        auto known_constructible = [&](Type type) { return constructible_types.count(type) != 0; };
+        // Find the transitive closure by adding types until the set of types does not change.
+        while (true) {
+            size_t set_size = constructible_types.size();
+            // Iterate over all types we have recipes for.
+            for (const auto& [type, recipes] : table) {
+                if (!known_constructible(type)) {
+                    // For not (yet known to be) constructible types, iterate over their recipes.
+                    for (const auto& [_, subt] : recipes) {
+                        // If any recipe involves only (already known to be) constructible types,
+                        // add the recipe's type to the set.
+                        if (std::all_of(subt.begin(), subt.end(), known_constructible)) {
+                            constructible_types.insert(type);
+                            break;
+                        }
+                    }
+                }
+            }
+            if (constructible_types.size() == set_size) break;
+        }
+        for (auto type_it = table.begin(); type_it != table.end();) {
+            // Remove all recipes which involve non-constructible types.
+            type_it->second.erase(std::remove_if(type_it->second.begin(), type_it->second.end(),
+                [&](const recipe& rec) {
+                    return !std::all_of(rec.second.begin(), rec.second.end(), known_constructible);
+                }), type_it->second.end());
+            // Delete types entirely which have no recipes left.
+            if (type_it->second.empty()) {
+                type_it = table.erase(type_it);
+            } else {
+                ++type_it;
+            }
+        }
+
+        for (auto& [type, recipes] : table) {
+            // Sort recipes for determinism, and place those using fewer subnodes first.
+            // This avoids runaway expansion (when reaching the end of the fuzz input,
+            // all zeroes are read, resulting in the first available recipe being picked).
+            std::sort(recipes.begin(), recipes.end(),
+                [](const recipe& a, const recipe& b) {
+                    if (a.second.size() < b.second.size()) return true;
+                    if (a.second.size() > b.second.size()) return false;
+                    return a < b;
+                }
+            );
+        }
+    }
+} SMARTINFO;
+
+/**
+ * Consume a Miniscript node from the fuzzer's output.
+ *
+ * This is similar to ConsumeNodeStable, but uses a precomputed table with permitted
+ * fragments/subnode type for each required type. It is intended to more quickly explore
+ * interesting miniscripts, at the cost of higher implementation complexity (which could
+ * cause it miss things if incorrect), and with less regard for stability of the seeds
+ * (as improvements to the tables or changes to the typing rules could invalidate
+ * everything).
+ */
+std::optional<NodeInfo> ConsumeNodeSmart(FuzzedDataProvider& provider, Type type_needed) {
+    /** Table entry for the requested type. */
+    auto recipes_it = SMARTINFO.table.find(type_needed);
+    assert(recipes_it != SMARTINFO.table.end());
+    /** Pick one recipe from the available ones for that type. */
+    const auto& [frag, subt] = PickValue(provider, recipes_it->second);
+
+    // Based on the fragment the recipe uses, fill in other data (k, keys, data).
+    switch (frag) {
+        case Fragment::PK_K:
+        case Fragment::PK_H:
+            return {{frag, ConsumePubKey(provider)}};
+        case Fragment::MULTI: {
+            const auto n_keys = provider.ConsumeIntegralInRange<uint8_t>(1, 20);
+            const auto k = provider.ConsumeIntegralInRange<uint8_t>(1, n_keys);
+            std::vector<CPubKey> keys{n_keys};
+            for (auto& key: keys) key = ConsumePubKey(provider);
+            return {{frag, k, std::move(keys)}};
+        }
+        case Fragment::OLDER:
+        case Fragment::AFTER:
+            return {{frag, provider.ConsumeIntegralInRange<uint32_t>(1, 0x7FFFFFF)}};
+        case Fragment::SHA256:
+            return {{frag, PickValue(provider, TEST_DATA.sha256)}};
+        case Fragment::HASH256:
+            return {{frag, PickValue(provider, TEST_DATA.hash256)}};
+        case Fragment::RIPEMD160:
+            return {{frag, PickValue(provider, TEST_DATA.ripemd160)}};
+        case Fragment::HASH160:
+            return {{frag, PickValue(provider, TEST_DATA.hash160)}};
+        case Fragment::JUST_0:
+        case Fragment::JUST_1:
+        case Fragment::WRAP_A:
+        case Fragment::WRAP_S:
+        case Fragment::WRAP_C:
+        case Fragment::WRAP_D:
+        case Fragment::WRAP_V:
+        case Fragment::WRAP_J:
+        case Fragment::WRAP_N:
+        case Fragment::AND_V:
+        case Fragment::AND_B:
+        case Fragment::OR_B:
+        case Fragment::OR_C:
+        case Fragment::OR_D:
+        case Fragment::OR_I:
+        case Fragment::ANDOR:
+            return {{subt, frag}};
+        case Fragment::THRESH: {
+            uint32_t children;
+            if (subt.size() < 2) {
+                children = subt.size();
+            } else {
+                // If we hit a thresh with 2 subnodes, artificially extend it to any number
+                // (2 or larger) by replicating the type of the last subnode.
+                children = provider.ConsumeIntegralInRange<uint32_t>(2, MAX_OPS_PER_SCRIPT / 2);
+            }
+            auto k = provider.ConsumeIntegralInRange<uint32_t>(1, children);
+            std::vector<Type> subs = subt;
+            while (subs.size() < children) subs.push_back(subs.back());
+            return {{std::move(subs), frag, k}};
+        }
+    }
+
+    assert(false);
+}
+
+/**
+ * Generate a Miniscript node based on the fuzzer's input.
+ *
+ * - ConsumeNode is a function object taking a Type, and returning an std::optional<NodeInfo>.
+ * - root_type is the required type properties of the constructed NodeRef.
+ * - strict_valid sets whether ConsumeNode is expected to guarantee a NodeInfo that results in
+ *   a NodeRef whose Type() matches the type fed to ConsumeNode.
+ */
+template<typename F>
+NodeRef GenNode(F ConsumeNode, Type root_type = ""_mst, bool strict_valid = false) {
+    /** A stack of miniscript Nodes being built up. */
+    std::vector<NodeRef> stack;
+    /** The queue of instructions. */
+    std::vector<std::pair<Type, std::optional<NodeInfo>>> todo{{root_type, {}}};
+
+    while (!todo.empty()) {
+        // The expected type we have to construct.
+        auto type_needed = todo.back().first;
+        if (!todo.back().second) {
+            // Fragment/children have not been decided yet. Decide them.
+            auto node_info = ConsumeNode(type_needed);
+            if (!node_info) return {};
+            auto subtypes = node_info->subtypes;
+            todo.back().second = std::move(node_info);
+            todo.reserve(todo.size() + subtypes.size());
+            // As elements on the todo stack are processed back to front, construct
+            // them in reverse order (so that the first subnode is generated first).
+            for (size_t i = 0; i < subtypes.size(); ++i) {
+                todo.emplace_back(*(subtypes.rbegin() + i), std::nullopt);
+            }
+        } else {
+            // The back of todo has fragment and number of children decided, and
+            // those children have been constructed at the back of stack. Pop
+            // that entry off todo, and use it to construct a new NodeRef on
+            // stack.
+            NodeInfo& info = *todo.back().second;
+            // Gather children from the back of stack.
+            std::vector<NodeRef> sub;
+            sub.reserve(info.n_subs);
+            for (size_t i = 0; i < info.n_subs; ++i) {
+                sub.push_back(std::move(*(stack.end() - info.n_subs + i)));
+            }
+            stack.erase(stack.end() - info.n_subs, stack.end());
+            // Construct new NodeRef.
+            NodeRef node;
+            if (info.keys.empty()) {
+                node = MakeNodeRef(info.fragment, std::move(sub), std::move(info.hash), info.k);
+            } else {
+                assert(sub.empty());
+                assert(info.hash.empty());
+                node = MakeNodeRef(info.fragment, std::move(info.keys), info.k);
+            }
+            // Verify acceptability.
+            if (!node || !(node->GetType() << type_needed)) {
+                assert(!strict_valid);
+                return {};
+            }
+            if (!node->IsValid()) return {};
+            // Move it to the stack.
+            stack.push_back(std::move(node));
+            todo.pop_back();
+        }
+    }
+    assert(stack.size() == 1);
+    return std::move(stack[0]);
+}
+
+/** Perform various applicable tests on a miniscript Node. */
+void TestNode(const NodeRef& node, FuzzedDataProvider& provider)
+{
+    if (!node) return;
+
+    // Check that it roundtrips to text representation
+    std::optional<std::string> str{node->ToString(PARSER_CTX)};
+    assert(str);
+    auto parsed = miniscript::FromString(*str, PARSER_CTX);
+    assert(parsed);
+    assert(*parsed == *node);
+
+    // Check consistency between script size estimation and real size.
+    auto script = node->ToScript(PARSER_CTX);
+    assert(node->ScriptSize() == script.size());
+
+    // Check consistency of "x" property with the script (type K is excluded, because it can end
+    // with a push of a key, which could match these opcodes).
+    if (!(node->GetType() << "K"_mst)) {
+        bool ends_in_verify = !(node->GetType() << "x"_mst);
+        assert(ends_in_verify == (script.back() == OP_CHECKSIG || script.back() == OP_CHECKMULTISIG || script.back() == OP_EQUAL));
+    }
+
+    // The rest of the checks only apply when testing a valid top-level script.
+    if (!node->IsValidTopLevel()) return;
+
+    // Check roundtrip to script
+    auto decoded = miniscript::FromScript(script, PARSER_CTX);
+    assert(decoded);
+    // Note we can't use *decoded == *node because the miniscript representation may differ, so we check that:
+    // - The script corresponding to that decoded form matchs exactly
+    // - The type matches exactly
+    assert(decoded->ToScript(PARSER_CTX) == script);
+    assert(decoded->GetType() == node->GetType());
+
+    if (provider.ConsumeBool() && node->GetOps() < MAX_OPS_PER_SCRIPT && node->ScriptSize() < MAX_STANDARD_P2WSH_SCRIPT_SIZE) {
+        // Optionally pad the script with OP_NOPs to max op the ops limit of the constructed script.
+        // This makes the script obviously not actually miniscript-compatible anymore, but the
+        // signatures constructed in this test don't commit to the script anyway, so the same
+        // miniscript satisfier will work. This increases the sensitivity of the test to the ops
+        // counting logic being too low, especially for simple scripts.
+        // Do this optionally because we're not solely interested in cases where the number of ops is
+        // maximal.
+        // Do not pad more than what would cause MAX_STANDARD_P2WSH_SCRIPT_SIZE to be reached, however,
+        // as that also invalidates scripts.
+        int add = std::min<int>(
+            MAX_OPS_PER_SCRIPT - node->GetOps(),
+            MAX_STANDARD_P2WSH_SCRIPT_SIZE - node->ScriptSize());
+        for (int i = 0; i < add; ++i) script.push_back(OP_NOP);
+    }
+
+    // Run malleable satisfaction algorithm.
+    const CScript script_pubkey = CScript() << OP_0 << WitnessV0ScriptHash(script);
+    CScriptWitness witness_mal;
+    const bool mal_success = node->Satisfy(SATISFIER_CTX, witness_mal.stack, false) == miniscript::Availability::YES;
+    witness_mal.stack.push_back(std::vector<unsigned char>(script.begin(), script.end()));
+
+    // Run non-malleable satisfaction algorithm.
+    CScriptWitness witness_nonmal;
+    const bool nonmal_success = node->Satisfy(SATISFIER_CTX, witness_nonmal.stack, true) == miniscript::Availability::YES;
+    witness_nonmal.stack.push_back(std::vector<unsigned char>(script.begin(), script.end()));
+
+    if (nonmal_success) {
+        // Non-malleable satisfactions are bounded by GetStackSize().
+        assert(witness_nonmal.stack.size() <= node->GetStackSize());
+        // If a non-malleable satisfaction exists, the malleable one must also exist, and be identical to it.
+        assert(mal_success);
+        assert(witness_nonmal.stack == witness_mal.stack);
+
+        // Test non-malleable satisfaction.
+        ScriptError serror;
+        bool res = VerifyScript(DUMMY_SCRIPTSIG, script_pubkey, &witness_nonmal, STANDARD_SCRIPT_VERIFY_FLAGS, CHECKER_CTX, &serror);
+        // Non-malleable satisfactions are guaranteed to be valid if ValidSatisfactions().
+        if (node->ValidSatisfactions()) assert(res);
+        // More detailed: non-malleable satisfactions must be valid, or could fail with ops count error (if CheckOpsLimit failed),
+        // or with a stack size error (if CheckStackSize check failed).
+        assert(res ||
+               (!node->CheckOpsLimit() && serror == ScriptError::SCRIPT_ERR_OP_COUNT) ||
+               (!node->CheckStackSize() && serror == ScriptError::SCRIPT_ERR_STACK_SIZE));
+    }
+
+    if (mal_success && (!nonmal_success || witness_mal.stack != witness_nonmal.stack)) {
+        // Test malleable satisfaction only if it's different from the non-malleable one.
+        ScriptError serror;
+        bool res = VerifyScript(DUMMY_SCRIPTSIG, script_pubkey, &witness_mal, STANDARD_SCRIPT_VERIFY_FLAGS, CHECKER_CTX, &serror);
+        // Malleable satisfactions are not guaranteed to be valid under any conditions, but they can only
+        // fail due to stack or ops limits.
+        assert(res || serror == ScriptError::SCRIPT_ERR_OP_COUNT || serror == ScriptError::SCRIPT_ERR_STACK_SIZE);
+    }
+
+    if (node->IsSane()) {
+        // For sane nodes, the two algorithms behave identically.
+        assert(mal_success == nonmal_success);
+    }
+
+    // Verify that if a node is policy-satisfiable, the malleable satisfaction
+    // algorithm succeeds. Given that under IsSane() both satisfactions
+    // are identical, this implies that for such nodes, the non-malleable
+    // satisfaction will also match the expected policy.
+    bool satisfiable = node->IsSatisfiable([](const Node& node) -> bool {
+        switch (node.fragment) {
+        case Fragment::PK_K:
+        case Fragment::PK_H: {
+            auto it = TEST_DATA.dummy_sigs.find(node.keys[0]);
+            assert(it != TEST_DATA.dummy_sigs.end());
+            return it->second.second;
+        }
+        case Fragment::MULTI: {
+            size_t sats = 0;
+            for (const auto& key : node.keys) {
+                auto it = TEST_DATA.dummy_sigs.find(key);
+                assert(it != TEST_DATA.dummy_sigs.end());
+                sats += it->second.second;
+            }
+            return sats >= node.k;
+        }
+        case Fragment::OLDER:
+        case Fragment::AFTER:
+            return node.k & 1;
+        case Fragment::SHA256:
+            return TEST_DATA.sha256_preimages.count(node.data);
+        case Fragment::HASH256:
+            return TEST_DATA.hash256_preimages.count(node.data);
+        case Fragment::RIPEMD160:
+            return TEST_DATA.ripemd160_preimages.count(node.data);
+        case Fragment::HASH160:
+            return TEST_DATA.hash160_preimages.count(node.data);
+        default:
+            assert(false);
+        }
+        return false;
+    });
+    assert(mal_success == satisfiable);
+}
+
 } // namespace
 
 void FuzzInit()
@@ -138,6 +908,33 @@ void FuzzInit()
     TEST_DATA.Init();
 }
 
+void FuzzInitSmart()
+{
+    FuzzInit();
+    SMARTINFO.Init();
+}
+
+/** Fuzz target that runs TestNode on nodes generated using ConsumeNodeStable. */
+FUZZ_TARGET_INIT(miniscript_stable, FuzzInit)
+{
+    FuzzedDataProvider provider(buffer.data(), buffer.size());
+    TestNode(GenNode([&](Type) {
+        return ConsumeNodeStable(provider);
+    }), provider);
+}
+
+/** Fuzz target that runs TestNode on nodes generated using ConsumeNodeSmart. */
+FUZZ_TARGET_INIT(miniscript_smart, FuzzInitSmart)
+{
+    /** The set of types we aim to construct nodes for. Together they cover all. */
+    static constexpr std::array<Type, 4> BASE_TYPES{"B"_mst, "V"_mst, "K"_mst, "W"_mst};
+
+    FuzzedDataProvider provider(buffer.data(), buffer.size());
+    TestNode(GenNode([&](Type needed_type) {
+        return ConsumeNodeSmart(provider, needed_type);
+    }, PickValue(provider, BASE_TYPES), true), provider);
+}
+
 /* Fuzz tests that test parsing from a string, and roundtripping via string. */
 FUZZ_TARGET_INIT(miniscript_string, FuzzInit)
 {
diff --git a/src/test/fuzz/script_sign.cpp b/src/test/fuzz/script_sign.cpp
index 3cef81c251..c78c22e6cc 100644
--- a/src/test/fuzz/script_sign.cpp
+++ b/src/test/fuzz/script_sign.cpp
@@ -108,10 +108,12 @@ FUZZ_TARGET_INIT(script_sign, initialize_script_sign)
             CMutableTransaction script_tx_to = tx_to;
             CMutableTransaction sign_transaction_tx_to = tx_to;
             if (n_in < tx_to.vin.size() && tx_to.vin[n_in].prevout.n < tx_from.vout.size()) {
-                (void)SignSignature(provider, tx_from, tx_to, n_in, fuzzed_data_provider.ConsumeIntegral<int>());
+                SignatureData empty;
+                (void)SignSignature(provider, tx_from, tx_to, n_in, fuzzed_data_provider.ConsumeIntegral<int>(), empty);
             }
             if (n_in < script_tx_to.vin.size()) {
-                (void)SignSignature(provider, ConsumeScript(fuzzed_data_provider), script_tx_to, n_in, ConsumeMoney(fuzzed_data_provider), fuzzed_data_provider.ConsumeIntegral<int>());
+                SignatureData empty;
+                (void)SignSignature(provider, ConsumeScript(fuzzed_data_provider), script_tx_to, n_in, ConsumeMoney(fuzzed_data_provider), fuzzed_data_provider.ConsumeIntegral<int>(), empty);
                 MutableTransactionSignatureCreator signature_creator{tx_to, n_in, ConsumeMoney(fuzzed_data_provider), fuzzed_data_provider.ConsumeIntegral<int>()};
                 std::vector<unsigned char> vch_sig;
                 CKeyID address;
diff --git a/src/test/miniscript_tests.cpp b/src/test/miniscript_tests.cpp
index 3181c9cf28..655d6d7828 100644
--- a/src/test/miniscript_tests.cpp
+++ b/src/test/miniscript_tests.cpp
@@ -2,18 +2,23 @@
 // Distributed under the MIT software license, see the accompanying
 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
 
-
+#include <stdint.h>
 #include <string>
+#include <vector>
 
 #include <test/util/setup_common.h>
 #include <boost/test/unit_test.hpp>
 
+#include <core_io.h>
 #include <hash.h>
 #include <pubkey.h>
 #include <uint256.h>
 #include <crypto/ripemd160.h>
 #include <crypto/sha256.h>
+#include <script/interpreter.h>
 #include <script/miniscript.h>
+#include <script/standard.h>
+#include <script/script_error.h>
 
 namespace {
 
@@ -24,15 +29,22 @@ struct TestData {
     //! A map from the public keys to their CKeyIDs (faster than hashing every time).
     std::map<CPubKey, CKeyID> pkhashes;
     std::map<CKeyID, CPubKey> pkmap;
+    std::map<CPubKey, std::vector<unsigned char>> signatures;
 
     // Various precomputed hashes
     std::vector<std::vector<unsigned char>> sha256;
     std::vector<std::vector<unsigned char>> ripemd160;
     std::vector<std::vector<unsigned char>> hash256;
     std::vector<std::vector<unsigned char>> hash160;
+    std::map<std::vector<unsigned char>, std::vector<unsigned char>> sha256_preimages;
+    std::map<std::vector<unsigned char>, std::vector<unsigned char>> ripemd160_preimages;
+    std::map<std::vector<unsigned char>, std::vector<unsigned char>> hash256_preimages;
+    std::map<std::vector<unsigned char>, std::vector<unsigned char>> hash160_preimages;
 
     TestData()
     {
+        // All our signatures sign (and are required to sign) this constant message.
+        auto const MESSAGE_HASH = uint256S("f5cd94e18b6fe77dd7aca9e35c2b0c9cbd86356c80a71065");
         // We generate 255 public keys and 255 hashes of each type.
         for (int i = 1; i <= 255; ++i) {
             // This 32-byte array functions as both private key data and hash preimage (31 zero bytes plus any nonzero byte).
@@ -48,18 +60,28 @@ struct TestData {
             pkhashes.emplace(pubkey, keyid);
             pkmap.emplace(keyid, pubkey);
 
+            // Compute ECDSA signatures on MESSAGE_HASH with the private keys.
+            std::vector<unsigned char> sig;
+            BOOST_CHECK(key.Sign(MESSAGE_HASH, sig));
+            sig.push_back(1); // sighash byte
+            signatures.emplace(pubkey, sig);
+
             // Compute various hashes
             std::vector<unsigned char> hash;
             hash.resize(32);
             CSHA256().Write(keydata, 32).Finalize(hash.data());
             sha256.push_back(hash);
+            sha256_preimages[hash] = std::vector<unsigned char>(keydata, keydata + 32);
             CHash256().Write(keydata).Finalize(hash);
             hash256.push_back(hash);
+            hash256_preimages[hash] = std::vector<unsigned char>(keydata, keydata + 32);
             hash.resize(20);
             CRIPEMD160().Write(keydata, 32).Finalize(hash.data());
             ripemd160.push_back(hash);
+            ripemd160_preimages[hash] = std::vector<unsigned char>(keydata, keydata + 32);
             CHash160().Write(keydata).Finalize(hash);
             hash160.push_back(hash);
+            hash160_preimages[hash] = std::vector<unsigned char>(keydata, keydata + 32);
         }
     }
 };
@@ -67,7 +89,27 @@ struct TestData {
 //! Global TestData object
 std::unique_ptr<const TestData> g_testdata;
 
-/** A class encapsulating conversion routing for CPubKey. */
+//! A classification of leaf conditions in miniscripts (excluding true/false).
+enum class ChallengeType {
+    SHA256,
+    RIPEMD160,
+    HASH256,
+    HASH160,
+    OLDER,
+    AFTER,
+    PK
+};
+
+/* With each leaf condition we associate a challenge number.
+ * For hashes it's just the first 4 bytes of the hash. For pubkeys, it's the last 4 bytes.
+ */
+uint32_t ChallengeNumber(const CPubKey& pubkey) { return ReadLE32(pubkey.data() + 29); }
+uint32_t ChallengeNumber(const std::vector<unsigned char>& hash) { return ReadLE32(hash.data()); }
+
+//! A Challenge is a combination of type of leaf condition and its challenge number.
+typedef std::pair<ChallengeType, uint32_t> Challenge;
+
+/** A class encapulating conversion routing for CPubKey. */
 struct KeyConverter {
     typedef CPubKey Key;
 
@@ -117,12 +159,197 @@ struct KeyConverter {
     }
 };
 
+/** A class that encapsulates all signing/hash revealing operations. */
+struct Satisfier : public KeyConverter {
+    //! Which keys/timelocks/hash preimages are available.
+    std::set<Challenge> supported;
+
+    //! Implement simplified CLTV logic: stack value must exactly match an entry in `supported`.
+    bool CheckAfter(uint32_t value) const {
+        return supported.count(Challenge(ChallengeType::AFTER, value));
+    }
+
+    //! Implement simplified CSV logic: stack value must exactly match an entry in `supported`.
+    bool CheckOlder(uint32_t value) const {
+        return supported.count(Challenge(ChallengeType::OLDER, value));
+    }
+
+    //! Produce a signature for the given key.
+    miniscript::Availability Sign(const CPubKey& key, std::vector<unsigned char>& sig) const {
+        if (supported.count(Challenge(ChallengeType::PK, ChallengeNumber(key)))) {
+            auto it = g_testdata->signatures.find(key);
+            if (it == g_testdata->signatures.end()) return miniscript::Availability::NO;
+            sig = it->second;
+            return miniscript::Availability::YES;
+        }
+        return miniscript::Availability::NO;
+    }
+
+    //! Helper function for the various hash based satisfactions.
+    miniscript::Availability SatHash(const std::vector<unsigned char>& hash, std::vector<unsigned char>& preimage, ChallengeType chtype) const {
+        if (!supported.count(Challenge(chtype, ChallengeNumber(hash)))) return miniscript::Availability::NO;
+        const auto& m =
+            chtype == ChallengeType::SHA256 ? g_testdata->sha256_preimages :
+            chtype == ChallengeType::HASH256 ? g_testdata->hash256_preimages :
+            chtype == ChallengeType::RIPEMD160 ? g_testdata->ripemd160_preimages :
+            g_testdata->hash160_preimages;
+        auto it = m.find(hash);
+        if (it == m.end()) return miniscript::Availability::NO;
+        preimage = it->second;
+        return miniscript::Availability::YES;
+    }
+
+    // Functions that produce the preimage for hashes of various types.
+    miniscript::Availability SatSHA256(const std::vector<unsigned char>& hash, std::vector<unsigned char>& preimage) const { return SatHash(hash, preimage, ChallengeType::SHA256); }
+    miniscript::Availability SatRIPEMD160(const std::vector<unsigned char>& hash, std::vector<unsigned char>& preimage) const { return SatHash(hash, preimage, ChallengeType::RIPEMD160); }
+    miniscript::Availability SatHASH256(const std::vector<unsigned char>& hash, std::vector<unsigned char>& preimage) const { return SatHash(hash, preimage, ChallengeType::HASH256); }
+    miniscript::Availability SatHASH160(const std::vector<unsigned char>& hash, std::vector<unsigned char>& preimage) const { return SatHash(hash, preimage, ChallengeType::HASH160); }
+};
+
+/** Mocking signature/timelock checker.
+ *
+ * It holds a pointer to a Satisfier object, to determine which timelocks are supposed to be available.
+ */
+class TestSignatureChecker : public BaseSignatureChecker {
+    const Satisfier& ctx;
+
+public:
+    TestSignatureChecker(const Satisfier& in_ctx LIFETIMEBOUND) : ctx(in_ctx) {}
+
+    bool CheckECDSASignature(const std::vector<unsigned char>& sig, const std::vector<unsigned char>& pubkey, const CScript& scriptcode, SigVersion sigversion) const override {
+        CPubKey pk(pubkey);
+        if (!pk.IsValid()) return false;
+        // Instead of actually running signature validation, check if the signature matches the precomputed one for this key.
+        auto it = g_testdata->signatures.find(pk);
+        if (it == g_testdata->signatures.end()) return false;
+        return sig == it->second;
+    }
+
+    bool CheckLockTime(const CScriptNum& locktime) const override {
+        // Delegate to Satisfier.
+        return ctx.CheckAfter(locktime.GetInt64());
+    }
+
+    bool CheckSequence(const CScriptNum& sequence) const override {
+        // Delegate to Satisfier.
+        return ctx.CheckOlder(sequence.GetInt64());
+    }
+};
+
 //! Singleton instance of KeyConverter.
 const KeyConverter CONVERTER{};
 
+using Fragment = miniscript::Fragment;
+using NodeRef = miniscript::NodeRef<CPubKey>;
 // https://github.com/llvm/llvm-project/issues/53444
 // NOLINTNEXTLINE(misc-unused-using-decls)
 using miniscript::operator"" _mst;
+using Node = miniscript::Node<CPubKey>;
+
+/** Compute all challenges (pubkeys, hashes, timelocks) that occur in a given Miniscript. */
+std::set<Challenge> FindChallenges(const NodeRef& ref) {
+    std::set<Challenge> chal;
+    for (const auto& key : ref->keys) {
+        chal.emplace(ChallengeType::PK, ChallengeNumber(key));
+    }
+    if (ref->fragment == miniscript::Fragment::OLDER) {
+        chal.emplace(ChallengeType::OLDER, ref->k);
+    } else if (ref->fragment == miniscript::Fragment::AFTER) {
+        chal.emplace(ChallengeType::AFTER, ref->k);
+    } else if (ref->fragment == miniscript::Fragment::SHA256) {
+        chal.emplace(ChallengeType::SHA256, ChallengeNumber(ref->data));
+    } else if (ref->fragment == miniscript::Fragment::RIPEMD160) {
+        chal.emplace(ChallengeType::RIPEMD160, ChallengeNumber(ref->data));
+    } else if (ref->fragment == miniscript::Fragment::HASH256) {
+        chal.emplace(ChallengeType::HASH256, ChallengeNumber(ref->data));
+    } else if (ref->fragment == miniscript::Fragment::HASH160) {
+        chal.emplace(ChallengeType::HASH160, ChallengeNumber(ref->data));
+    }
+    for (const auto& sub : ref->subs) {
+        auto sub_chal = FindChallenges(sub);
+        chal.insert(sub_chal.begin(), sub_chal.end());
+    }
+    return chal;
+}
+
+/** Run random satisfaction tests. */
+void TestSatisfy(const std::string& testcase, const NodeRef& node) {
+    auto script = node->ToScript(CONVERTER);
+    auto challenges = FindChallenges(node); // Find all challenges in the generated miniscript.
+    std::vector<Challenge> challist(challenges.begin(), challenges.end());
+    for (int iter = 0; iter < 3; ++iter) {
+        Shuffle(challist.begin(), challist.end(), g_insecure_rand_ctx);
+        Satisfier satisfier;
+        TestSignatureChecker checker(satisfier);
+        bool prev_mal_success = false, prev_nonmal_success = false;
+        // Go over all challenges involved in this miniscript in random order.
+        for (int add = -1; add < (int)challist.size(); ++add) {
+            if (add >= 0) satisfier.supported.insert(challist[add]); // The first iteration does not add anything
+
+            // Run malleable satisfaction algorithm.
+            const CScript script_pubkey = CScript() << OP_0 << WitnessV0ScriptHash(script);
+            CScriptWitness witness_mal;
+            const bool mal_success = node->Satisfy(satisfier, witness_mal.stack, false) == miniscript::Availability::YES;
+            witness_mal.stack.push_back(std::vector<unsigned char>(script.begin(), script.end()));
+
+            // Run non-malleable satisfaction algorithm.
+            CScriptWitness witness_nonmal;
+            const bool nonmal_success = node->Satisfy(satisfier, witness_nonmal.stack, true) == miniscript::Availability::YES;
+            witness_nonmal.stack.push_back(std::vector<unsigned char>(script.begin(), script.end()));
+
+            if (nonmal_success) {
+                // Non-malleable satisfactions are bounded by GetStackSize().
+                BOOST_CHECK(witness_nonmal.stack.size() <= node->GetStackSize());
+                // If a non-malleable satisfaction exists, the malleable one must also exist, and be identical to it.
+                BOOST_CHECK(mal_success);
+                BOOST_CHECK(witness_nonmal.stack == witness_mal.stack);
+
+                // Test non-malleable satisfaction.
+                ScriptError serror;
+                bool res = VerifyScript(CScript(), script_pubkey, &witness_nonmal, STANDARD_SCRIPT_VERIFY_FLAGS, checker, &serror);
+                // Non-malleable satisfactions are guaranteed to be valid if ValidSatisfactions().
+                if (node->ValidSatisfactions()) BOOST_CHECK(res);
+                // More detailed: non-malleable satisfactions must be valid, or could fail with ops count error (if CheckOpsLimit failed),
+                // or with a stack size error (if CheckStackSize check fails).
+                BOOST_CHECK(res ||
+                            (!node->CheckOpsLimit() && serror == ScriptError::SCRIPT_ERR_OP_COUNT) ||
+                            (!node->CheckStackSize() && serror == ScriptError::SCRIPT_ERR_STACK_SIZE));
+            }
+
+            if (mal_success && (!nonmal_success || witness_mal.stack != witness_nonmal.stack)) {
+                // Test malleable satisfaction only if it's different from the non-malleable one.
+                ScriptError serror;
+                bool res = VerifyScript(CScript(), script_pubkey, &witness_mal, STANDARD_SCRIPT_VERIFY_FLAGS, checker, &serror);
+                // Malleable satisfactions are not guaranteed to be valid under any conditions, but they can only
+                // fail due to stack or ops limits.
+                BOOST_CHECK(res || serror == ScriptError::SCRIPT_ERR_OP_COUNT || serror == ScriptError::SCRIPT_ERR_STACK_SIZE);
+            }
+
+            if (node->IsSane()) {
+                // For sane nodes, the two algorithms behave identically.
+                BOOST_CHECK_EQUAL(mal_success, nonmal_success);
+            }
+
+            // Adding more satisfied conditions can never remove our ability to produce a satisfaction.
+            BOOST_CHECK(mal_success >= prev_mal_success);
+            // For nonmalleable solutions this is only true if the added condition is PK;
+            // for other conditions, adding one may make an valid satisfaction become malleable. If the script
+            // is sane, this cannot happen however.
+            if (node->IsSane() || add < 0 || challist[add].first == ChallengeType::PK) {
+                BOOST_CHECK(nonmal_success >= prev_nonmal_success);
+            }
+            // Remember results for the next added challenge.
+            prev_mal_success = mal_success;
+            prev_nonmal_success = nonmal_success;
+        }
+
+        bool satisfiable = node->IsSatisfiable([](const Node&) { return true; });
+        // If the miniscript was satisfiable at all, a satisfaction must be found after all conditions are added.
+        BOOST_CHECK_EQUAL(prev_mal_success, satisfiable);
+        // If the miniscript is sane and satisfiable, a nonmalleable satisfaction must eventually be found.
+        if (node->IsSane()) BOOST_CHECK_EQUAL(prev_nonmal_success, satisfiable);
+    }
+}
 
 enum TestMode : int {
     TESTMODE_INVALID = 0,
@@ -152,6 +379,7 @@ void Test(const std::string& ms, const std::string& hexscript, int mode, int ops
         BOOST_CHECK_MESSAGE(inferred_miniscript->ToScript(CONVERTER) == computed_script, "Roundtrip failure: miniscript->script != miniscript->script->miniscript->script: " + ms);
         if (opslimit != -1) BOOST_CHECK_MESSAGE((int)node->GetOps() == opslimit, "Ops limit mismatch: " << ms << " (" << node->GetOps() << " vs " << opslimit << ")");
         if (stacklimit != -1) BOOST_CHECK_MESSAGE((int)node->GetStackSize() == stacklimit, "Stack limit mismatch: " << ms << " (" << node->GetStackSize() << " vs " << stacklimit << ")");
+        TestSatisfy(ms, node);
     }
 }
 } // namespace
diff --git a/src/test/multisig_tests.cpp b/src/test/multisig_tests.cpp
index 1e1a9932ad..7a3e8e3a47 100644
--- a/src/test/multisig_tests.cpp
+++ b/src/test/multisig_tests.cpp
@@ -217,7 +217,8 @@ BOOST_AUTO_TEST_CASE(multisig_Sign)
 
     for (int i = 0; i < 3; i++)
     {
-        BOOST_CHECK_MESSAGE(SignSignature(keystore, CTransaction(txFrom), txTo[i], 0, SIGHASH_ALL), strprintf("SignSignature %d", i));
+        SignatureData empty;
+        BOOST_CHECK_MESSAGE(SignSignature(keystore, CTransaction(txFrom), txTo[i], 0, SIGHASH_ALL, empty), strprintf("SignSignature %d", i));
     }
 }
 
diff --git a/src/test/orphanage_tests.cpp b/src/test/orphanage_tests.cpp
index a55b0bbcd0..d95b9711d0 100644
--- a/src/test/orphanage_tests.cpp
+++ b/src/test/orphanage_tests.cpp
@@ -88,7 +88,8 @@ BOOST_AUTO_TEST_CASE(DoS_mapOrphans)
         tx.vout.resize(1);
         tx.vout[0].nValue = 1*CENT;
         tx.vout[0].scriptPubKey = GetScriptForDestination(PKHash(key.GetPubKey()));
-        BOOST_CHECK(SignSignature(keystore, *txPrev, tx, 0, SIGHASH_ALL));
+        SignatureData empty;
+        BOOST_CHECK(SignSignature(keystore, *txPrev, tx, 0, SIGHASH_ALL, empty));
 
         orphanage.AddTx(MakeTransactionRef(tx), i);
     }
@@ -108,7 +109,8 @@ BOOST_AUTO_TEST_CASE(DoS_mapOrphans)
             tx.vin[j].prevout.n = j;
             tx.vin[j].prevout.hash = txPrev->GetHash();
         }
-        BOOST_CHECK(SignSignature(keystore, *txPrev, tx, 0, SIGHASH_ALL));
+        SignatureData empty;
+        BOOST_CHECK(SignSignature(keystore, *txPrev, tx, 0, SIGHASH_ALL, empty));
         // Re-use same signature for other inputs
         // (they don't have to be valid for this test)
         for (unsigned int j = 1; j < tx.vin.size(); j++)
diff --git a/src/test/script_p2sh_tests.cpp b/src/test/script_p2sh_tests.cpp
index e439ff3519..c9f002b324 100644
--- a/src/test/script_p2sh_tests.cpp
+++ b/src/test/script_p2sh_tests.cpp
@@ -102,7 +102,8 @@ BOOST_AUTO_TEST_CASE(sign)
     }
     for (int i = 0; i < 8; i++)
     {
-        BOOST_CHECK_MESSAGE(SignSignature(keystore, CTransaction(txFrom), txTo[i], 0, SIGHASH_ALL), strprintf("SignSignature %d", i));
+        SignatureData empty;
+        BOOST_CHECK_MESSAGE(SignSignature(keystore, CTransaction(txFrom), txTo[i], 0, SIGHASH_ALL, empty), strprintf("SignSignature %d", i));
     }
     // All of the above should be OK, and the txTos have valid signatures
     // Check to make sure signature verification fails if we use the wrong ScriptSig:
@@ -197,7 +198,8 @@ BOOST_AUTO_TEST_CASE(set)
     }
     for (int i = 0; i < 4; i++)
     {
-        BOOST_CHECK_MESSAGE(SignSignature(keystore, CTransaction(txFrom), txTo[i], 0, SIGHASH_ALL), strprintf("SignSignature %d", i));
+        SignatureData empty;
+        BOOST_CHECK_MESSAGE(SignSignature(keystore, CTransaction(txFrom), txTo[i], 0, SIGHASH_ALL, empty), strprintf("SignSignature %d", i));
         BOOST_CHECK_MESSAGE(IsStandardTx(CTransaction(txTo[i]), reason), strprintf("txTo[%d].IsStandard", i));
     }
 }
@@ -334,9 +336,12 @@ BOOST_AUTO_TEST_CASE(AreInputsStandard)
         txTo.vin[i].prevout.n = i;
         txTo.vin[i].prevout.hash = txFrom.GetHash();
     }
-    BOOST_CHECK(SignSignature(keystore, CTransaction(txFrom), txTo, 0, SIGHASH_ALL));
-    BOOST_CHECK(SignSignature(keystore, CTransaction(txFrom), txTo, 1, SIGHASH_ALL));
-    BOOST_CHECK(SignSignature(keystore, CTransaction(txFrom), txTo, 2, SIGHASH_ALL));
+    SignatureData empty;
+    BOOST_CHECK(SignSignature(keystore, CTransaction(txFrom), txTo, 0, SIGHASH_ALL, empty));
+    SignatureData empty_b;
+    BOOST_CHECK(SignSignature(keystore, CTransaction(txFrom), txTo, 1, SIGHASH_ALL, empty_b));
+    SignatureData empty_c;
+    BOOST_CHECK(SignSignature(keystore, CTransaction(txFrom), txTo, 2, SIGHASH_ALL, empty_c));
     // SignSignature doesn't know how to sign these. We're
     // not testing validating signatures, so just create
     // dummy signatures that DO include the correct P2SH scripts:
diff --git a/src/test/script_tests.cpp b/src/test/script_tests.cpp
index 22f6cfd164..b16f63d685 100644
--- a/src/test/script_tests.cpp
+++ b/src/test/script_tests.cpp
@@ -1180,7 +1180,8 @@ BOOST_AUTO_TEST_CASE(script_combineSigs)
     BOOST_CHECK(combined.scriptSig.empty());
 
     // Single signature case:
-    BOOST_CHECK(SignSignature(keystore, CTransaction(txFrom), txTo, 0, SIGHASH_ALL)); // changes scriptSig
+    SignatureData dummy;
+    BOOST_CHECK(SignSignature(keystore, CTransaction(txFrom), txTo, 0, SIGHASH_ALL, dummy)); // changes scriptSig
     scriptSig = DataFromTransaction(txTo, 0, txFrom.vout[0]);
     combined = CombineSignatures(txFrom.vout[0], txTo, scriptSig, empty);
     BOOST_CHECK(combined.scriptSig == scriptSig.scriptSig);
@@ -1188,7 +1189,8 @@ BOOST_AUTO_TEST_CASE(script_combineSigs)
     BOOST_CHECK(combined.scriptSig == scriptSig.scriptSig);
     SignatureData scriptSigCopy = scriptSig;
     // Signing again will give a different, valid signature:
-    BOOST_CHECK(SignSignature(keystore, CTransaction(txFrom), txTo, 0, SIGHASH_ALL));
+    SignatureData dummy_b;
+    BOOST_CHECK(SignSignature(keystore, CTransaction(txFrom), txTo, 0, SIGHASH_ALL, dummy_b));
     scriptSig = DataFromTransaction(txTo, 0, txFrom.vout[0]);
     combined = CombineSignatures(txFrom.vout[0], txTo, scriptSigCopy, scriptSig);
     BOOST_CHECK(combined.scriptSig == scriptSigCopy.scriptSig || combined.scriptSig == scriptSig.scriptSig);
@@ -1197,14 +1199,16 @@ BOOST_AUTO_TEST_CASE(script_combineSigs)
     CScript pkSingle; pkSingle << ToByteVector(keys[0].GetPubKey()) << OP_CHECKSIG;
     BOOST_CHECK(keystore.AddCScript(pkSingle));
     scriptPubKey = GetScriptForDestination(ScriptHash(pkSingle));
-    BOOST_CHECK(SignSignature(keystore, CTransaction(txFrom), txTo, 0, SIGHASH_ALL));
+    SignatureData dummy_c;
+    BOOST_CHECK(SignSignature(keystore, CTransaction(txFrom), txTo, 0, SIGHASH_ALL, dummy_c));
     scriptSig = DataFromTransaction(txTo, 0, txFrom.vout[0]);
     combined = CombineSignatures(txFrom.vout[0], txTo, scriptSig, empty);
     BOOST_CHECK(combined.scriptSig == scriptSig.scriptSig);
     combined = CombineSignatures(txFrom.vout[0], txTo, empty, scriptSig);
     BOOST_CHECK(combined.scriptSig == scriptSig.scriptSig);
     scriptSigCopy = scriptSig;
-    BOOST_CHECK(SignSignature(keystore, CTransaction(txFrom), txTo, 0, SIGHASH_ALL));
+    SignatureData dummy_d;
+    BOOST_CHECK(SignSignature(keystore, CTransaction(txFrom), txTo, 0, SIGHASH_ALL, dummy_d));
     scriptSig = DataFromTransaction(txTo, 0, txFrom.vout[0]);
     combined = CombineSignatures(txFrom.vout[0], txTo, scriptSigCopy, scriptSig);
     BOOST_CHECK(combined.scriptSig == scriptSigCopy.scriptSig || combined.scriptSig == scriptSig.scriptSig);
@@ -1212,7 +1216,8 @@ BOOST_AUTO_TEST_CASE(script_combineSigs)
     // Hardest case:  Multisig 2-of-3
     scriptPubKey = GetScriptForMultisig(2, pubkeys);
     BOOST_CHECK(keystore.AddCScript(scriptPubKey));
-    BOOST_CHECK(SignSignature(keystore, CTransaction(txFrom), txTo, 0, SIGHASH_ALL));
+    SignatureData dummy_e;
+    BOOST_CHECK(SignSignature(keystore, CTransaction(txFrom), txTo, 0, SIGHASH_ALL, dummy_e));
     scriptSig = DataFromTransaction(txTo, 0, txFrom.vout[0]);
     combined = CombineSignatures(txFrom.vout[0], txTo, scriptSig, empty);
     BOOST_CHECK(combined.scriptSig == scriptSig.scriptSig);
diff --git a/src/test/transaction_tests.cpp b/src/test/transaction_tests.cpp
index 0180fa47e8..507284a566 100644
--- a/src/test/transaction_tests.cpp
+++ b/src/test/transaction_tests.cpp
@@ -433,7 +433,8 @@ static void CreateCreditAndSpend(const FillableSigningProvider& keystore, const
     inputm.vout.resize(1);
     inputm.vout[0].nValue = 1;
     inputm.vout[0].scriptPubKey = CScript();
-    bool ret = SignSignature(keystore, *output, inputm, 0, SIGHASH_ALL);
+    SignatureData empty;
+    bool ret = SignSignature(keystore, *output, inputm, 0, SIGHASH_ALL, empty);
     assert(ret == success);
     CDataStream ssin(SER_NETWORK, PROTOCOL_VERSION);
     ssin << inputm;
@@ -517,7 +518,8 @@ BOOST_AUTO_TEST_CASE(test_big_witness_transaction)
 
     // sign all inputs
     for(uint32_t i = 0; i < mtx.vin.size(); i++) {
-        bool hashSigned = SignSignature(keystore, scriptPubKey, mtx, i, 1000, sigHashes.at(i % sigHashes.size()));
+        SignatureData empty;
+        bool hashSigned = SignSignature(keystore, scriptPubKey, mtx, i, 1000, sigHashes.at(i % sigHashes.size()), empty);
         assert(hashSigned);
     }
 
diff --git a/src/test/util/xoroshiro128plusplus.h b/src/test/util/xoroshiro128plusplus.h
new file mode 100644
index 0000000000..ac9f59b3f5
--- /dev/null
+++ b/src/test/util/xoroshiro128plusplus.h
@@ -0,0 +1,71 @@
+// 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.
+
+#ifndef BITCOIN_TEST_UTIL_XOROSHIRO128PLUSPLUS_H
+#define BITCOIN_TEST_UTIL_XOROSHIRO128PLUSPLUS_H
+
+#include <cstdint>
+#include <limits>
+
+/** xoroshiro128++ PRNG. Extremely fast, not appropriate for cryptographic purposes.
+ *
+ * Memory footprint is 128bit, period is 2^128 - 1.
+ * This class is not thread-safe.
+ *
+ * Reference implementation available at https://prng.di.unimi.it/xoroshiro128plusplus.c
+ * See https://prng.di.unimi.it/
+ */
+class XoRoShiRo128PlusPlus
+{
+    uint64_t m_s0;
+    uint64_t m_s1;
+
+    [[nodiscard]] constexpr static uint64_t rotl(uint64_t x, int n)
+    {
+        return (x << n) | (x >> (64 - n));
+    }
+
+    [[nodiscard]] constexpr static uint64_t SplitMix64(uint64_t& seedval) noexcept
+    {
+        uint64_t z = (seedval += UINT64_C(0x9e3779b97f4a7c15));
+        z = (z ^ (z >> 30U)) * UINT64_C(0xbf58476d1ce4e5b9);
+        z = (z ^ (z >> 27U)) * UINT64_C(0x94d049bb133111eb);
+        return z ^ (z >> 31U);
+    }
+
+public:
+    using result_type = uint64_t;
+
+    constexpr explicit XoRoShiRo128PlusPlus(uint64_t seedval) noexcept
+        : m_s0(SplitMix64(seedval)), m_s1(SplitMix64(seedval))
+    {
+    }
+
+    // no copy - that is dangerous, we don't want accidentally copy the RNG and then have two streams
+    // with exactly the same results. If you need a copy, call copy().
+    XoRoShiRo128PlusPlus(const XoRoShiRo128PlusPlus&) = delete;
+    XoRoShiRo128PlusPlus& operator=(const XoRoShiRo128PlusPlus&) = delete;
+
+    // allow moves
+    XoRoShiRo128PlusPlus(XoRoShiRo128PlusPlus&&) = default;
+    XoRoShiRo128PlusPlus& operator=(XoRoShiRo128PlusPlus&&) = default;
+
+    ~XoRoShiRo128PlusPlus() = default;
+
+    constexpr result_type operator()() noexcept
+    {
+        uint64_t s0 = m_s0, s1 = m_s1;
+        const uint64_t result = rotl(s0 + s1, 17) + s0;
+        s1 ^= s0;
+        m_s0 = rotl(s0, 49) ^ s1 ^ (s1 << 21);
+        m_s1 = rotl(s1, 28);
+        return result;
+    }
+
+    static constexpr result_type min() noexcept { return std::numeric_limits<result_type>::min(); }
+    static constexpr result_type max() noexcept { return std::numeric_limits<result_type>::max(); }
+    static constexpr double entropy() noexcept { return 0.0; }
+};
+
+#endif // BITCOIN_TEST_UTIL_XOROSHIRO128PLUSPLUS_H
diff --git a/src/test/xoroshiro128plusplus_tests.cpp b/src/test/xoroshiro128plusplus_tests.cpp
new file mode 100644
index 0000000000..ea1b3e355f
--- /dev/null
+++ b/src/test/xoroshiro128plusplus_tests.cpp
@@ -0,0 +1,29 @@
+// 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/util/setup_common.h>
+#include <test/util/xoroshiro128plusplus.h>
+
+#include <boost/test/unit_test.hpp>
+
+BOOST_FIXTURE_TEST_SUITE(xoroshiro128plusplus_tests, BasicTestingSetup)
+
+BOOST_AUTO_TEST_CASE(reference_values)
+{
+    // numbers generated from reference implementation
+    XoRoShiRo128PlusPlus rng(0);
+    BOOST_TEST(0x6f68e1e7e2646ee1 == rng());
+    BOOST_TEST(0xbf971b7f454094ad == rng());
+    BOOST_TEST(0x48f2de556f30de38 == rng());
+    BOOST_TEST(0x6ea7c59f89bbfc75 == rng());
+
+    // seed with a random number
+    rng = XoRoShiRo128PlusPlus(0x1a26f3fa8546b47a);
+    BOOST_TEST(0xc8dc5e08d844ac7d == rng());
+    BOOST_TEST(0x5b5f1f6d499dad1b == rng());
+    BOOST_TEST(0xbeb0031f93313d6f == rng());
+    BOOST_TEST(0xbfbcf4f43a264497 == rng());
+}
+
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/src/util/hasher.cpp b/src/util/hasher.cpp
index a3a3f7a429..81e9b990e1 100644
--- a/src/util/hasher.cpp
+++ b/src/util/hasher.cpp
@@ -9,7 +9,10 @@
 
 SaltedTxidHasher::SaltedTxidHasher() : k0(GetRand<uint64_t>()), k1(GetRand<uint64_t>()) {}
 
-SaltedOutpointHasher::SaltedOutpointHasher() : k0(GetRand<uint64_t>()), k1(GetRand<uint64_t>()) {}
+SaltedOutpointHasher::SaltedOutpointHasher(bool deterministic) :
+    k0(deterministic ? 0x8e819f2607a18de6 : GetRand<uint64_t>()),
+    k1(deterministic ? 0xf4020d2e3983b0eb : GetRand<uint64_t>())
+{}
 
 SaltedSipHasher::SaltedSipHasher() : m_k0(GetRand<uint64_t>()), m_k1(GetRand<uint64_t>()) {}
 
diff --git a/src/util/hasher.h b/src/util/hasher.h
index 82d278b086..506ae9415d 100644
--- a/src/util/hasher.h
+++ b/src/util/hasher.h
@@ -36,7 +36,7 @@ class SaltedOutpointHasher
     const uint64_t k0, k1;
 
 public:
-    SaltedOutpointHasher();
+    SaltedOutpointHasher(bool deterministic = false);
 
     /**
      * Having the hash noexcept allows libstdc++'s unordered_map to recalculate
diff --git a/src/wallet/feebumper.cpp b/src/wallet/feebumper.cpp
index bd158b5985..37a704bfa4 100644
--- a/src/wallet/feebumper.cpp
+++ b/src/wallet/feebumper.cpp
@@ -155,7 +155,7 @@ bool TransactionCanBeBumped(const CWallet& wallet, const uint256& txid)
 }
 
 Result CreateRateBumpTransaction(CWallet& wallet, const uint256& txid, const CCoinControl& coin_control, std::vector<bilingual_str>& errors,
-                                 CAmount& old_fee, CAmount& new_fee, CMutableTransaction& mtx, bool require_mine)
+                                 CAmount& old_fee, CAmount& new_fee, CMutableTransaction& mtx, bool require_mine, const std::vector<CTxOut>& outputs)
 {
     // We are going to modify coin control later, copy to re-use
     CCoinControl new_coin_control(coin_control);
@@ -222,11 +222,19 @@ Result CreateRateBumpTransaction(CWallet& wallet, const uint256& txid, const CCo
         return result;
     }
 
-    // Fill in recipients(and preserve a single change key if there is one)
-    // While we're here, calculate the output amount
-    std::vector<CRecipient> recipients;
+    // Calculate the old output amount.
     CAmount output_value = 0;
-    for (const auto& output : wtx.tx->vout) {
+    for (const auto& old_output : wtx.tx->vout) {
+        output_value += old_output.nValue;
+    }
+
+    old_fee = input_value - output_value;
+
+    // Fill in recipients (and preserve a single change key if there
+    // is one). If outputs vector is non-empty, replace original
+    // outputs with its contents, otherwise use original outputs.
+    std::vector<CRecipient> recipients;
+    for (const auto& output : outputs.empty() ? wtx.tx->vout : outputs) {
         if (!OutputIsChange(wallet, output)) {
             CRecipient recipient = {output.scriptPubKey, output.nValue, false};
             recipients.push_back(recipient);
@@ -235,11 +243,8 @@ Result CreateRateBumpTransaction(CWallet& wallet, const uint256& txid, const CCo
             ExtractDestination(output.scriptPubKey, change_dest);
             new_coin_control.destChange = change_dest;
         }
-        output_value += output.nValue;
     }
 
-    old_fee = input_value - output_value;
-
     if (coin_control.m_feerate) {
         // The user provided a feeRate argument.
         // We calculate this here to avoid compiler warning on the cs_wallet lock
diff --git a/src/wallet/feebumper.h b/src/wallet/feebumper.h
index a96871b26f..53cf16e0f1 100644
--- a/src/wallet/feebumper.h
+++ b/src/wallet/feebumper.h
@@ -51,7 +51,8 @@ Result CreateRateBumpTransaction(CWallet& wallet,
     CAmount& old_fee,
     CAmount& new_fee,
     CMutableTransaction& mtx,
-    bool require_mine);
+    bool require_mine,
+    const std::vector<CTxOut>& outputs);
 
 //! Sign the new transaction,
 //! @return false if the tx couldn't be found or if it was
diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp
index 68dd3da9b5..1a76e46c54 100644
--- a/src/wallet/interfaces.cpp
+++ b/src/wallet/interfaces.cpp
@@ -291,7 +291,8 @@ class WalletImpl : public Wallet
         CAmount& new_fee,
         CMutableTransaction& mtx) override
     {
-        return feebumper::CreateRateBumpTransaction(*m_wallet.get(), txid, coin_control, errors, old_fee, new_fee, mtx, /* require_mine= */ true) == feebumper::Result::OK;
+        std::vector<CTxOut> outputs; // just an empty list of new recipients for now
+        return feebumper::CreateRateBumpTransaction(*m_wallet.get(), txid, coin_control, errors, old_fee, new_fee, mtx, /* require_mine= */ true, outputs) == feebumper::Result::OK;
     }
     bool signBumpTransaction(CMutableTransaction& mtx) override { return feebumper::SignTransaction(*m_wallet.get(), mtx); }
     bool commitBumpTransaction(const uint256& txid,
diff --git a/src/wallet/rpc/spend.cpp b/src/wallet/rpc/spend.cpp
index cab797bbce..88ee6e96b0 100644
--- a/src/wallet/rpc/spend.cpp
+++ b/src/wallet/rpc/spend.cpp
@@ -956,6 +956,26 @@ RPCHelpMan signrawtransactionwithwallet()
     };
 }
 
+// Definition of allowed formats of specifying transaction outputs in
+// `bumpfee`, `psbtbumpfee`, `send` and `walletcreatefundedpsbt` RPCs.
+static std::vector<RPCArg> OutputsDoc()
+{
+    return
+    {
+        {"", RPCArg::Type::OBJ_USER_KEYS, RPCArg::Optional::OMITTED, "",
+            {
+                {"address", RPCArg::Type::AMOUNT, RPCArg::Optional::NO, "A key-value pair. The key (string) is the bitcoin address,\n"
+                         "the value (float or string) is the amount in " + CURRENCY_UNIT + ""},
+            },
+        },
+        {"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "",
+            {
+                {"data", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "A key-value pair. The key must be \"data\", the value is hex-encoded data"},
+            },
+        },
+    };
+}
+
 static RPCHelpMan bumpfee_helper(std::string method_name)
 {
     const bool want_psbt = method_name == "psbtbumpfee";
@@ -992,7 +1012,12 @@ static RPCHelpMan bumpfee_helper(std::string method_name)
                              "still be replaceable in practice, for example if it has unconfirmed ancestors which\n"
                              "are replaceable).\n"},
                     {"estimate_mode", RPCArg::Type::STR, RPCArg::Default{"unset"}, "The fee estimate mode, must be one of (case insensitive):\n"
-                     "\"" + FeeModes("\"\n\"") + "\""},
+                             "\"" + FeeModes("\"\n\"") + "\""},
+                    {"outputs", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "New outputs (key-value pairs) which will replace\n"
+                             "the original ones, if provided. Each address can only appear once and there can\n"
+                             "only be one \"data\" object.\n",
+                        OutputsDoc(),
+                        RPCArgOptions{.skip_type_check = true}},
                 },
                 RPCArgOptions{.oneline_description="options"}},
         },
@@ -1029,6 +1054,7 @@ static RPCHelpMan bumpfee_helper(std::string method_name)
     coin_control.fAllowWatchOnly = pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS);
     // optional parameters
     coin_control.m_signal_bip125_rbf = true;
+    std::vector<CTxOut> outputs;
 
     if (!request.params[1].isNull()) {
         UniValue options = request.params[1];
@@ -1039,6 +1065,7 @@ static RPCHelpMan bumpfee_helper(std::string method_name)
                 {"fee_rate", UniValueType()}, // will be checked by AmountFromValue() in SetFeeEstimateMode()
                 {"replaceable", UniValueType(UniValue::VBOOL)},
                 {"estimate_mode", UniValueType(UniValue::VSTR)},
+                {"outputs", UniValueType()}, // will be checked by AddOutputs()
             },
             true, true);
 
@@ -1052,6 +1079,16 @@ static RPCHelpMan bumpfee_helper(std::string method_name)
             coin_control.m_signal_bip125_rbf = options["replaceable"].get_bool();
         }
         SetFeeEstimateMode(*pwallet, coin_control, conf_target, options["estimate_mode"], options["fee_rate"], /*override_min_fee=*/false);
+
+        // Prepare new outputs by creating a temporary tx and calling AddOutputs().
+        if (!options["outputs"].isNull()) {
+            if (options["outputs"].isArray() && options["outputs"].empty()) {
+                throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, output argument cannot be an empty array");
+            }
+            CMutableTransaction tempTx;
+            AddOutputs(tempTx, options["outputs"]);
+            outputs = tempTx.vout;
+        }
     }
 
     // Make sure the results are valid at least up to the most recent block
@@ -1069,7 +1106,7 @@ static RPCHelpMan bumpfee_helper(std::string method_name)
     CMutableTransaction mtx;
     feebumper::Result res;
     // Targeting feerate bump.
-    res = feebumper::CreateRateBumpTransaction(*pwallet, hash, coin_control, errors, old_fee, new_fee, mtx, /*require_mine=*/ !want_psbt);
+    res = feebumper::CreateRateBumpTransaction(*pwallet, hash, coin_control, errors, old_fee, new_fee, mtx, /*require_mine=*/ !want_psbt, outputs);
     if (res != feebumper::Result::OK) {
         switch(res) {
             case feebumper::Result::INVALID_ADDRESS_OR_KEY:
@@ -1144,18 +1181,7 @@ RPCHelpMan send()
             {"outputs", RPCArg::Type::ARR, RPCArg::Optional::NO, "The outputs (key-value pairs), where none of the keys are duplicated.\n"
                     "That is, each address can only appear once and there can only be one 'data' object.\n"
                     "For convenience, a dictionary, which holds the key-value pairs directly, is also accepted.",
-                {
-                    {"", RPCArg::Type::OBJ_USER_KEYS, RPCArg::Optional::OMITTED, "",
-                        {
-                            {"address", RPCArg::Type::AMOUNT, RPCArg::Optional::NO, "A key-value pair. The key (string) is the bitcoin address, the value (float or string) is the amount in " + CURRENCY_UNIT + ""},
-                        },
-                        },
-                    {"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "",
-                        {
-                            {"data", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "A key-value pair. The key must be \"data\", the value is hex-encoded data"},
-                        },
-                    },
-                },
+                OutputsDoc(),
                 RPCArgOptions{.skip_type_check = true}},
             {"conf_target", RPCArg::Type::NUM, RPCArg::DefaultHint{"wallet -txconfirmtarget"}, "Confirmation target in blocks"},
             {"estimate_mode", RPCArg::Type::STR, RPCArg::Default{"unset"}, "The fee estimate mode, must be one of (case insensitive):\n"
@@ -1606,19 +1632,8 @@ RPCHelpMan walletcreatefundedpsbt()
                             "That is, each address can only appear once and there can only be one 'data' object.\n"
                             "For compatibility reasons, a dictionary, which holds the key-value pairs directly, is also\n"
                             "accepted as second parameter.",
-                        {
-                            {"", RPCArg::Type::OBJ_USER_KEYS, RPCArg::Optional::OMITTED, "",
-                                {
-                                    {"address", RPCArg::Type::AMOUNT, RPCArg::Optional::NO, "A key-value pair. The key (string) is the bitcoin address, the value (float or string) is the amount in " + CURRENCY_UNIT + ""},
-                                },
-                                },
-                            {"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "",
-                                {
-                                    {"data", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "A key-value pair. The key must be \"data\", the value is hex-encoded data"},
-                                },
-                            },
-                        },
-                     RPCArgOptions{.skip_type_check = true}},
+                        OutputsDoc(),
+                        RPCArgOptions{.skip_type_check = true}},
                     {"locktime", RPCArg::Type::NUM, RPCArg::Default{0}, "Raw locktime. Non-0 value also locktime-activates inputs"},
                     {"options", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "",
                         Cat<std::vector<RPCArg>>(
diff --git a/src/wallet/spend.cpp b/src/wallet/spend.cpp
index e66ff8c97c..0ef56ab8ed 100644
--- a/src/wallet/spend.cpp
+++ b/src/wallet/spend.cpp
@@ -306,9 +306,7 @@ CoinsResult AvailableCoins(const CWallet& wallet,
             std::unique_ptr<SigningProvider> provider = wallet.GetSolvingProvider(output.scriptPubKey);
 
             int input_bytes = CalculateMaximumSignedInputSize(output, COutPoint(), provider.get(), coinControl);
-            // Because CalculateMaximumSignedInputSize just uses ProduceSignature and makes a dummy signature,
-            // it is safe to assume that this input is solvable if input_bytes is greater -1.
-            bool solvable = input_bytes > -1;
+            bool solvable = provider ? InferDescriptor(output.scriptPubKey, *provider)->IsSolvable() : false;
             bool spendable = ((mine & ISMINE_SPENDABLE) != ISMINE_NO) || (((mine & ISMINE_WATCH_ONLY) != ISMINE_NO) && (coinControl && coinControl->fAllowWatchOnly && solvable));
 
             // Filter by spendable outputs only
diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp
index 5a92dbe428..b709bd9650 100644
--- a/src/wallet/wallet.cpp
+++ b/src/wallet/wallet.cpp
@@ -26,6 +26,7 @@
 #include <script/descriptor.h>
 #include <script/script.h>
 #include <script/signingprovider.h>
+#include <support/cleanse.h>
 #include <txmempool.h>
 #include <util/bip32.h>
 #include <util/check.h>
@@ -3407,7 +3408,10 @@ bool CWallet::Lock()
 
     {
         LOCK(cs_wallet);
-        vMasterKey.clear();
+        if (!vMasterKey.empty()) {
+            memory_cleanse(vMasterKey.data(), vMasterKey.size() * sizeof(decltype(vMasterKey)::value_type));
+            vMasterKey.clear();
+        }
     }
 
     NotifyStatusChanged(this);
diff --git a/test/functional/feature_maxuploadtarget.py b/test/functional/feature_maxuploadtarget.py
index 28a8959e93..c551c0b449 100755
--- a/test/functional/feature_maxuploadtarget.py
+++ b/test/functional/feature_maxuploadtarget.py
@@ -164,6 +164,9 @@ def run_test(self):
         assert_equal(len(peer_info), 1)  # node is still connected
         assert_equal(peer_info[0]['permissions'], ['download'])
 
+        self.log.info("Test passing an unparsable value to -maxuploadtarget throws an error")
+        self.stop_node(0)
+        self.nodes[0].assert_start_raises_init_error(extra_args=["-maxuploadtarget=abc"], expected_msg="Error: Unable to parse -maxuploadtarget: 'abc'")
 
 if __name__ == '__main__':
     MaxUploadTest().main()
diff --git a/test/functional/mempool_updatefromblock.py b/test/functional/mempool_updatefromblock.py
index 68cbb5dbed..8350e9c91e 100755
--- a/test/functional/mempool_updatefromblock.py
+++ b/test/functional/mempool_updatefromblock.py
@@ -7,14 +7,12 @@
 Test mempool update of transaction descendants/ancestors information (count, size)
 when transactions have been re-added from a disconnected block to the mempool.
 """
+from math import ceil
 import time
 
-from decimal import Decimal
 from test_framework.test_framework import BitcoinTestFramework
 from test_framework.util import assert_equal
-from test_framework.address import key_to_p2pkh
-from test_framework.wallet_util import bytes_to_wif
-from test_framework.key import ECKey
+from test_framework.wallet import MiniWallet
 
 
 class MempoolUpdateFromBlockTest(BitcoinTestFramework):
@@ -22,15 +20,7 @@ def set_test_params(self):
         self.num_nodes = 1
         self.extra_args = [['-limitdescendantsize=1000', '-limitancestorsize=1000', '-limitancestorcount=100']]
 
-    def get_new_address(self):
-        key = ECKey()
-        key.generate()
-        pubkey = key.get_pubkey().get_bytes()
-        address = key_to_p2pkh(pubkey)
-        self.priv_keys.append(bytes_to_wif(key.get_bytes()))
-        return address
-
-    def transaction_graph_test(self, size, n_tx_to_mine=None, start_input_txid='', end_address='', fee=Decimal(0.00100000)):
+    def transaction_graph_test(self, size, n_tx_to_mine=None, fee=100_000):
         """Create an acyclic tournament (a type of directed graph) of transactions and use it for testing.
 
         Keyword arguments:
@@ -45,14 +35,7 @@ def transaction_graph_test(self, size, n_tx_to_mine=None, start_input_txid='', e
 
         More details: https://en.wikipedia.org/wiki/Tournament_(graph_theory)
         """
-
-        self.priv_keys = [self.nodes[0].get_deterministic_priv_key().key]
-        if not start_input_txid:
-            start_input_txid = self.nodes[0].getblock(self.nodes[0].getblockhash(1))['tx'][0]
-
-        if not end_address:
-            end_address = self.get_new_address()
-
+        wallet = MiniWallet(self.nodes[0])
         first_block_hash = ''
         tx_id = []
         tx_size = []
@@ -61,41 +44,31 @@ def transaction_graph_test(self, size, n_tx_to_mine=None, start_input_txid='', e
             self.log.debug('Preparing transaction #{}...'.format(i))
             # Prepare inputs.
             if i == 0:
-                inputs = [{'txid': start_input_txid, 'vout': 0}]
-                inputs_value = self.nodes[0].gettxout(start_input_txid, 0)['value']
+                inputs = [wallet.get_utxo()]  # let MiniWallet provide a start UTXO
             else:
                 inputs = []
-                inputs_value = 0
                 for j, tx in enumerate(tx_id[0:i]):
                     # Transaction tx[K] is a child of each of previous transactions tx[0]..tx[K-1] at their output K-1.
                     vout = i - j - 1
-                    inputs.append({'txid': tx_id[j], 'vout': vout})
-                    inputs_value += self.nodes[0].gettxout(tx, vout)['value']
-
-            self.log.debug('inputs={}'.format(inputs))
-            self.log.debug('inputs_value={}'.format(inputs_value))
+                    inputs.append(wallet.get_utxo(txid=tx_id[j], vout=vout))
 
             # Prepare outputs.
             tx_count = i + 1
             if tx_count < size:
                 # Transaction tx[K] is an ancestor of each of subsequent transactions tx[K+1]..tx[N-1].
                 n_outputs = size - tx_count
-                output_value = ((inputs_value - fee) / Decimal(n_outputs)).quantize(Decimal('0.00000001'))
-                outputs = {}
-                for _ in range(n_outputs):
-                    outputs[self.get_new_address()] = output_value
             else:
-                output_value = (inputs_value - fee).quantize(Decimal('0.00000001'))
-                outputs = {end_address: output_value}
-
-            self.log.debug('output_value={}'.format(output_value))
-            self.log.debug('outputs={}'.format(outputs))
+                n_outputs = 1
 
             # Create a new transaction.
-            unsigned_raw_tx = self.nodes[0].createrawtransaction(inputs, outputs)
-            signed_raw_tx = self.nodes[0].signrawtransactionwithkey(unsigned_raw_tx, self.priv_keys)
-            tx_id.append(self.nodes[0].sendrawtransaction(signed_raw_tx['hex']))
-            tx_size.append(self.nodes[0].getmempoolentry(tx_id[-1])['vsize'])
+            new_tx = wallet.send_self_transfer_multi(
+                from_node=self.nodes[0],
+                utxos_to_spend=inputs,
+                num_outputs=n_outputs,
+                fee_per_output=ceil(fee / n_outputs)
+            )
+            tx_id.append(new_tx['txid'])
+            tx_size.append(new_tx['tx'].get_vsize())
 
             if tx_count in n_tx_to_mine:
                 # The created transactions are mined into blocks by batches.
diff --git a/test/functional/p2p_invalid_messages.py b/test/functional/p2p_invalid_messages.py
index 3109ad2b56..ea4999a965 100755
--- a/test/functional/p2p_invalid_messages.py
+++ b/test/functional/p2p_invalid_messages.py
@@ -13,11 +13,12 @@
     MAX_HEADERS_RESULTS,
     MAX_INV_SIZE,
     MAX_PROTOCOL_MESSAGE_LENGTH,
+    MSG_TX,
+    from_hex,
     msg_getdata,
     msg_headers,
     msg_inv,
     msg_ping,
-    MSG_TX,
     msg_version,
     ser_string,
 )
@@ -73,6 +74,7 @@ def run_test(self):
         self.test_oversized_inv_msg()
         self.test_oversized_getdata_msg()
         self.test_oversized_headers_msg()
+        self.test_invalid_pow_headers_msg()
         self.test_resource_exhaustion()
 
     def test_buffer(self):
@@ -248,6 +250,36 @@ def test_oversized_headers_msg(self):
         size = MAX_HEADERS_RESULTS + 1
         self.test_oversized_msg(msg_headers([CBlockHeader()] * size), size)
 
+    def test_invalid_pow_headers_msg(self):
+        self.log.info("Test headers message with invalid proof-of-work is logged as misbehaving and disconnects peer")
+        blockheader_tip_hash = self.nodes[0].getbestblockhash()
+        blockheader_tip = from_hex(CBlockHeader(), self.nodes[0].getblockheader(blockheader_tip_hash, False))
+
+        # send valid headers message first
+        assert_equal(self.nodes[0].getblockchaininfo()['headers'], 0)
+        blockheader = CBlockHeader()
+        blockheader.hashPrevBlock = int(blockheader_tip_hash, 16)
+        blockheader.nTime = int(time.time())
+        blockheader.nBits = blockheader_tip.nBits
+        blockheader.rehash()
+        while not blockheader.hash.startswith('0'):
+            blockheader.nNonce += 1
+            blockheader.rehash()
+        peer = self.nodes[0].add_p2p_connection(P2PInterface())
+        peer.send_and_ping(msg_headers([blockheader]))
+        assert_equal(self.nodes[0].getblockchaininfo()['headers'], 1)
+        chaintips = self.nodes[0].getchaintips()
+        assert_equal(chaintips[0]['status'], 'headers-only')
+        assert_equal(chaintips[0]['hash'], blockheader.hash)
+
+        # invalidate PoW
+        while not blockheader.hash.startswith('f'):
+            blockheader.nNonce += 1
+            blockheader.rehash()
+        with self.nodes[0].assert_debug_log(['Misbehaving', 'header with invalid proof of work']):
+            peer.send_message(msg_headers([blockheader]))
+            peer.wait_for_disconnect()
+
     def test_resource_exhaustion(self):
         self.log.info("Test node stays up despite many large junk messages")
         conn = self.nodes[0].add_p2p_connection(P2PDataStore())
diff --git a/test/functional/wallet_backwards_compatibility.py b/test/functional/wallet_backwards_compatibility.py
index f55a3758ce..76aac3e486 100755
--- a/test/functional/wallet_backwards_compatibility.py
+++ b/test/functional/wallet_backwards_compatibility.py
@@ -33,11 +33,12 @@ def add_options(self, parser):
 
     def set_test_params(self):
         self.setup_clean_chain = True
-        self.num_nodes = 10
+        self.num_nodes = 11
         # Add new version after each release:
         self.extra_args = [
             ["-addresstype=bech32", "-whitelist=noban@127.0.0.1"], # Pre-release: use to mine blocks. noban for immediate tx relay
             ["-nowallet", "-walletrbf=1", "-addresstype=bech32", "-whitelist=noban@127.0.0.1"], # Pre-release: use to receive coins, swap wallets, etc
+            ["-nowallet", "-walletrbf=1", "-addresstype=bech32", "-whitelist=noban@127.0.0.1"], # v24.0.1
             ["-nowallet", "-walletrbf=1", "-addresstype=bech32", "-whitelist=noban@127.0.0.1"], # v23.0
             ["-nowallet", "-walletrbf=1", "-addresstype=bech32", "-whitelist=noban@127.0.0.1"], # v22.0
             ["-nowallet", "-walletrbf=1", "-addresstype=bech32", "-whitelist=noban@127.0.0.1"], # v0.21.0
@@ -57,6 +58,7 @@ def setup_nodes(self):
         self.add_nodes(self.num_nodes, extra_args=self.extra_args, versions=[
             None,
             None,
+            240001,
             230000,
             220000,
             210000,
diff --git a/test/functional/wallet_bumpfee.py b/test/functional/wallet_bumpfee.py
index a2ae997ecb..ad79e0288c 100755
--- a/test/functional/wallet_bumpfee.py
+++ b/test/functional/wallet_bumpfee.py
@@ -81,7 +81,7 @@ def run_test(self):
 
         self.log.info("Running tests")
         dest_address = peer_node.getnewaddress()
-        for mode in ["default", "fee_rate"]:
+        for mode in ["default", "fee_rate", "new_outputs"]:
             test_simple_bumpfee_succeeds(self, mode, rbf_node, peer_node, dest_address)
         self.test_invalid_parameters(rbf_node, peer_node, dest_address)
         test_segwit_bumpfee_succeeds(self, rbf_node, dest_address)
@@ -157,6 +157,14 @@ def test_invalid_parameters(self, rbf_node, peer_node, dest_address):
             assert_raises_rpc_error(-8, 'Invalid estimate_mode parameter, must be one of: "unset", "economical", "conservative"',
                 rbf_node.bumpfee, rbfid, {"estimate_mode": mode})
 
+        self.log.info("Test invalid outputs values")
+        assert_raises_rpc_error(-8, "Invalid parameter, output argument cannot be an empty array",
+                rbf_node.bumpfee, rbfid, {"outputs": []})
+        assert_raises_rpc_error(-8, "Invalid parameter, duplicated address: " + dest_address,
+                rbf_node.bumpfee, rbfid, {"outputs": [{dest_address: 0.1}, {dest_address: 0.2}]})
+        assert_raises_rpc_error(-8, "Invalid parameter, duplicate key: data",
+                rbf_node.bumpfee, rbfid, {"outputs": [{"data": "deadbeef"}, {"data": "deadbeef"}]})
+
         self.clear_mempool()
 
 
@@ -169,6 +177,10 @@ def test_simple_bumpfee_succeeds(self, mode, rbf_node, peer_node, dest_address):
     if mode == "fee_rate":
         bumped_psbt = rbf_node.psbtbumpfee(rbfid, {"fee_rate": str(NORMAL)})
         bumped_tx = rbf_node.bumpfee(rbfid, {"fee_rate": NORMAL})
+    elif mode == "new_outputs":
+        new_address = peer_node.getnewaddress()
+        bumped_psbt = rbf_node.psbtbumpfee(rbfid, {"outputs": {new_address: 0.0003}})
+        bumped_tx = rbf_node.bumpfee(rbfid, {"outputs": {new_address: 0.0003}})
     else:
         bumped_psbt = rbf_node.psbtbumpfee(rbfid)
         bumped_tx = rbf_node.bumpfee(rbfid)
@@ -192,6 +204,10 @@ def test_simple_bumpfee_succeeds(self, mode, rbf_node, peer_node, dest_address):
     bumpedwtx = rbf_node.gettransaction(bumped_tx["txid"])
     assert_equal(oldwtx["replaced_by_txid"], bumped_tx["txid"])
     assert_equal(bumpedwtx["replaces_txid"], rbfid)
+    # if this is a new_outputs test, check that outputs were indeed replaced
+    if mode == "new_outputs":
+        assert len(bumpedwtx["details"]) == 1
+        assert bumpedwtx["details"][0]["address"] == new_address
     self.clear_mempool()
 
 
@@ -628,12 +644,14 @@ def get_change_address(tx):
     self.clear_mempool()
 
 
-def spend_one_input(node, dest_address, change_size=Decimal("0.00049000")):
+def spend_one_input(node, dest_address, change_size=Decimal("0.00049000"), data=None):
     tx_input = dict(
         sequence=MAX_BIP125_RBF_SEQUENCE, **next(u for u in node.listunspent() if u["amount"] == Decimal("0.00100000")))
     destinations = {dest_address: Decimal("0.00050000")}
     if change_size > 0:
         destinations[node.getrawchangeaddress()] = change_size
+    if data:
+        destinations['data'] = data
     rawtx = node.createrawtransaction([tx_input], destinations)
     signedtx = node.signrawtransactionwithwallet(rawtx)
     txid = node.sendrawtransaction(signedtx["hex"])
diff --git a/test/functional/wallet_groups.py b/test/functional/wallet_groups.py
index 83c1826a41..bdb9081261 100755
--- a/test/functional/wallet_groups.py
+++ b/test/functional/wallet_groups.py
@@ -41,6 +41,11 @@ def skip_test_if_missing_module(self):
 
     def run_test(self):
         self.log.info("Setting up")
+        # To take full use of immediate tx relay, all nodes need to be reachable
+        # via inbound peers, i.e. connect first to last to close the circle
+        # (the default test network topology looks like this:
+        #  node0 <-- node1 <-- node2 <-- node3 <-- node4 <-- node5)
+        self.connect_nodes(0, self.num_nodes - 1)
         # Mine some coins
         self.generate(self.nodes[0], COINBASE_MATURITY + 1)
 
diff --git a/test/functional/wallet_miniscript.py b/test/functional/wallet_miniscript.py
index cefcaf4dc7..7bc3424bf4 100755
--- a/test/functional/wallet_miniscript.py
+++ b/test/functional/wallet_miniscript.py
@@ -5,19 +5,137 @@
 """Test Miniscript descriptors integration in the wallet."""
 
 from test_framework.descriptors import descsum_create
+from test_framework.psbt import PSBT, PSBT_IN_SHA256
 from test_framework.test_framework import BitcoinTestFramework
 from test_framework.util import assert_equal
 
 
+TPRVS = [
+    "tprv8ZgxMBicQKsPerQj6m35no46amfKQdjY7AhLnmatHYXs8S4MTgeZYkWAn4edSGwwL3vkSiiGqSZQrmy5D3P5gBoqgvYP2fCUpBwbKTMTAkL",
+    "tprv8ZgxMBicQKsPd3cbrKjE5GKKJLDEidhtzSSmPVtSPyoHQGL2LZw49yt9foZsN9BeiC5VqRaESUSDV2PS9w7zAVBSK6EQH3CZW9sMKxSKDwD",
+    "tprv8iF7W37EHnVEtDr9EFeyFjQJFL6SfGby2AnZ2vQARxTQHQXy9tdzZvBBVp8a19e5vXhskczLkJ1AZjqgScqWL4FpmXVp8LLjiorcrFK63Sr",
+]
+TPUBS = [
+    "tpubD6NzVbkrYhZ4YPAbyf6urxqqnmJF79PzQtyERAmvkSVS9fweCTjxjDh22Z5St9fGb1a5DUCv8G27nYupKP1Ctr1pkamJossoetzws1moNRn",
+    "tpubD6NzVbkrYhZ4YMQC15JS7QcrsAyfGrGiykweqMmPxTkEVScu7vCZLNpPXW1XphHwzsgmqdHWDQAfucbM72EEB1ZEyfgZxYvkZjYVXx1xS9p",
+    "tpubD6NzVbkrYhZ4YU9vM1s53UhD75UyJatx8EMzMZ3VUjR2FciNfLLkAw6a4pWACChzobTseNqdWk4G7ZdBqRDLtLSACKykTScmqibb1ZrCvJu",
+    "tpubD6NzVbkrYhZ4XRMcMFMMFvzVt6jaDAtjZhD7JLwdPdMm9xa76DnxYYP7w9TZGJDVFkek3ArwVsuacheqqPog8TH5iBCX1wuig8PLXim4n9a",
+    "tpubD6NzVbkrYhZ4WsqRzDmkL82SWcu42JzUvKWzrJHQ8EC2vEHRHkXj1De93sD3biLrKd8XGnamXURGjMbYavbszVDXpjXV2cGUERucLJkE6cy",
+    "tpubDEFLeBkKTm8aiYkySz8hXAXPVnPSfxMi7Fxhg9sejUrkwJuRWvPdLEiXjTDbhGbjLKCZUDUUibLxTnK5UP1q7qYrSnPqnNe7M8mvAW1STcc",
+]
+PUBKEYS = [
+    "02aebf2d10b040eb936a6f02f44ee82f8b34f5c1ccb20ff3949c2b28206b7c1068",
+    "030f64b922aee2fd597f104bc6cb3b670f1ca2c6c49b1071a1a6c010575d94fe5a",
+    "02abe475b199ec3d62fa576faee16a334fdb86ffb26dce75becebaaedf328ac3fe",
+    "0314f3dc33595b0d016bb522f6fe3a67680723d842c1b9b8ae6b59fdd8ab5cccb4",
+    "025eba3305bd3c829e4e1551aac7358e4178832c739e4fc4729effe428de0398ab",
+    "029ffbe722b147f3035c87cb1c60b9a5947dd49c774cc31e94773478711a929ac0",
+    "0211c7b2e18b6fd330f322de087da62da92ae2ae3d0b7cec7e616479cce175f183",
+]
+
 MINISCRIPTS = [
     # One of two keys
-    "or_b(pk(tpubD6NzVbkrYhZ4XRMcMFMMFvzVt6jaDAtjZhD7JLwdPdMm9xa76DnxYYP7w9TZGJDVFkek3ArwVsuacheqqPog8TH5iBCX1wuig8PLXim4n9a/*),s:pk(tpubD6NzVbkrYhZ4WsqRzDmkL82SWcu42JzUvKWzrJHQ8EC2vEHRHkXj1De93sD3biLrKd8XGnamXURGjMbYavbszVDXpjXV2cGUERucLJkE6cy/*))",
+    f"or_b(pk({TPUBS[0]}/*),s:pk({TPUBS[1]}/*))",
     # A script similar (same spending policy) to BOLT3's offered HTLC (with anchor outputs)
-    "or_d(pk(tpubD6NzVbkrYhZ4XRMcMFMMFvzVt6jaDAtjZhD7JLwdPdMm9xa76DnxYYP7w9TZGJDVFkek3ArwVsuacheqqPog8TH5iBCX1wuig8PLXim4n9a/*),and_v(and_v(v:pk(tpubD6NzVbkrYhZ4WsqRzDmkL82SWcu42JzUvKWzrJHQ8EC2vEHRHkXj1De93sD3biLrKd8XGnamXURGjMbYavbszVDXpjXV2cGUERucLJkE6cy/*),or_c(pk(tpubD6NzVbkrYhZ4YNwtTWrKRJQzQX3PjPKeUQg1gYh1hiLMkk1cw8SRLgB1yb7JzE8bHKNt6EcZXkJ6AqpCZL1aaRSjnG36mLgbQvJZBNsjWnG/*),v:hash160(7f999c905d5e35cefd0a37673f746eb13fba3640))),older(1)))",
+    f"or_d(pk({TPUBS[0]}/*),and_v(and_v(v:pk({TPUBS[1]}/*),or_c(pk({TPUBS[2]}/*),v:hash160(7f999c905d5e35cefd0a37673f746eb13fba3640))),older(1)))",
     # A Revault Unvault policy with the older() replaced by an after()
-    "andor(multi(2,tpubD6NzVbkrYhZ4YMQC15JS7QcrsAyfGrGiykweqMmPxTkEVScu7vCZLNpPXW1XphHwzsgmqdHWDQAfucbM72EEB1ZEyfgZxYvkZjYVXx1xS9p/*,tpubD6NzVbkrYhZ4WkCyc7E3z6g6NkypHMiecnwc4DpWHTPqFdteRGkEKukdrSSyJGNnGrHNMfy4BCw2UXo5soYRCtCDDfy4q8pc8oyB7RgTFv8/*),and_v(v:multi(4,030f64b922aee2fd597f104bc6cb3b670f1ca2c6c49b1071a1a6c010575d94fe5a,02abe475b199ec3d62fa576faee16a334fdb86ffb26dce75becebaaedf328ac3fe,0314f3dc33595b0d016bb522f6fe3a67680723d842c1b9b8ae6b59fdd8ab5cccb4,025eba3305bd3c829e4e1551aac7358e4178832c739e4fc4729effe428de0398ab),after(424242)),thresh(4,pkh(tpubD6NzVbkrYhZ4YVrNggiT2ptVHwnFbLBqDkCtV5HkxR4WtcRLAQReKTkqZGNcV6GE7cQsmpBzzSzhk16DUwB1gn1L7ZPnJF2dnNePP1uMBCY/*),a:pkh(tpubD6NzVbkrYhZ4YU9vM1s53UhD75UyJatx8EMzMZ3VUjR2FciNfLLkAw6a4pWACChzobTseNqdWk4G7ZdBqRDLtLSACKykTScmqibb1ZrCvJu/*),a:pkh(tpubD6NzVbkrYhZ4YUHcFfuH9iEBLiH8CBRJTpS7X3qjHmh82m1KCNbzs6w9gyK8oWHSZmKHWcakAXCGfbKg6xoCvKzQCWAHyxaC7QcWfmzyBf4/*),a:pkh(tpubD6NzVbkrYhZ4XXEmQtS3sgxpJbMyMg4McqRR1Af6ULzyrTRnhwjyr1etPD7svap9oFtJf4MM72brUb5o7uvF2Jyszc5c1t836fJW7SX2e8D/*)))",
+    f"andor(multi(2,{TPUBS[0]}/*,{TPUBS[1]}/*),and_v(v:multi(4,{PUBKEYS[0]},{PUBKEYS[1]},{PUBKEYS[2]},{PUBKEYS[3]}),after(424242)),thresh(4,pkh({TPUBS[2]}/*),a:pkh({TPUBS[3]}/*),a:pkh({TPUBS[4]}/*),a:pkh({TPUBS[5]}/*)))",
     # Liquid-like federated pegin with emergency recovery keys
-    "or_i(and_b(pk(029ffbe722b147f3035c87cb1c60b9a5947dd49c774cc31e94773478711a929ac0),a:and_b(pk(025f05815e3a1a8a83bfbb03ce016c9a2ee31066b98f567f6227df1d76ec4bd143),a:and_b(pk(025625f41e4a065efc06d5019cbbd56fe8c07595af1231e7cbc03fafb87ebb71ec),a:and_b(pk(02a27c8b850a00f67da3499b60562673dcf5fdfb82b7e17652a7ac54416812aefd),s:pk(03e618ec5f384d6e19ca9ebdb8e2119e5bef978285076828ce054e55c4daf473e2))))),and_v(v:thresh(2,pkh(tpubD6NzVbkrYhZ4YK67cd5fDe4fBVmGB2waTDrAt1q4ey9HPq9veHjWkw3VpbaCHCcWozjkhgAkWpFrxuPMUrmXVrLHMfEJ9auoZA6AS1g3grC/*),a:pkh(033841045a531e1adf9910a6ec279589a90b3b8a904ee64ffd692bd08a8996c1aa),a:pkh(02aebf2d10b040eb936a6f02f44ee82f8b34f5c1ccb20ff3949c2b28206b7c1068)),older(4209713)))",
+    f"or_i(and_b(pk({PUBKEYS[0]}),a:and_b(pk({PUBKEYS[1]}),a:and_b(pk({PUBKEYS[2]}),a:and_b(pk({PUBKEYS[3]}),s:pk({PUBKEYS[4]}))))),and_v(v:thresh(2,pkh({TPUBS[0]}/*),a:pkh({PUBKEYS[5]}),a:pkh({PUBKEYS[6]})),older(4209713)))",
+]
+
+MINISCRIPTS_PRIV = [
+    # One of two keys, of which one private key is known
+    {
+        "ms": f"or_i(pk({TPRVS[0]}/*),pk({TPUBS[0]}/*))",
+        "sequence": None,
+        "locktime": None,
+        "sigs_count": 1,
+        "stack_size": 3,
+    },
+    # A more complex policy, that can't be satisfied through the first branch (need for a preimage)
+    {
+        "ms": f"andor(ndv:older(2),and_v(v:pk({TPRVS[0]}),sha256(2a8ce30189b2ec3200b47aeb4feaac8fcad7c0ba170389729f4898b0b7933bcb)),and_v(v:pkh({TPRVS[1]}),pk({TPRVS[2]}/*)))",
+        "sequence": 2,
+        "locktime": None,
+        "sigs_count": 3,
+        "stack_size": 5,
+    },
+    # The same policy but we provide the preimage. This path will be chosen as it's a smaller witness.
+    {
+        "ms": f"andor(ndv:older(2),and_v(v:pk({TPRVS[0]}),sha256(61e33e9dbfefc45f6a194187684d278f789fd4d5e207a357e79971b6519a8b12)),and_v(v:pkh({TPRVS[1]}),pk({TPRVS[2]}/*)))",
+        "sequence": 2,
+        "locktime": None,
+        "sigs_count": 3,
+        "stack_size": 4,
+        "sha256_preimages": {
+            "61e33e9dbfefc45f6a194187684d278f789fd4d5e207a357e79971b6519a8b12": "e8774f330f5f330c23e8bbefc5595cb87009ddb7ac3b8deaaa8e9e41702d919c"
+        },
+    },
+    # Signature with a relative timelock
+    {
+        "ms": f"and_v(v:older(2),pk({TPRVS[0]}/*))",
+        "sequence": 2,
+        "locktime": None,
+        "sigs_count": 1,
+        "stack_size": 2,
+    },
+    # Signature with an absolute timelock
+    {
+        "ms": f"and_v(v:after(20),pk({TPRVS[0]}/*))",
+        "sequence": None,
+        "locktime": 20,
+        "sigs_count": 1,
+        "stack_size": 2,
+    },
+    # Signature with both
+    {
+        "ms": f"and_v(v:older(4),and_v(v:after(30),pk({TPRVS[0]}/*)))",
+        "sequence": 4,
+        "locktime": 30,
+        "sigs_count": 1,
+        "stack_size": 2,
+    },
+    # We have one key on each branch; Core signs both (can't finalize)
+    {
+        "ms": f"c:andor(pk({TPRVS[0]}/*),pk_k({TPUBS[0]}),and_v(v:pk({TPRVS[1]}),pk_k({TPUBS[1]})))",
+        "sequence": None,
+        "locktime": None,
+        "sigs_count": 2,
+        "stack_size": None,
+    },
+    # We have all the keys, wallet selects the timeout path to sign since it's smaller and sequence is set
+    {
+        "ms": f"andor(pk({TPRVS[0]}/*),pk({TPRVS[2]}),and_v(v:pk({TPRVS[1]}),older(10)))",
+        "sequence": 10,
+        "locktime": None,
+        "sigs_count": 3,
+        "stack_size": 3,
+    },
+    # We have all the keys, wallet selects the primary path to sign unconditionally since nsequence wasn't set to be valid for timeout path
+    {
+        "ms": f"andor(pk({TPRVS[0]}/*),pk({TPRVS[2]}),and_v(v:pkh({TPRVS[1]}),older(10)))",
+        "sequence": None,
+        "locktime": None,
+        "sigs_count": 3,
+        "stack_size": 3,
+    },
+    # Finalizes to the smallest valid witness, regardless of sequence
+    {
+        "ms": f"or_d(pk({TPRVS[0]}/*),and_v(v:pk({TPRVS[1]}),and_v(v:pk({TPRVS[2]}),older(10))))",
+        "sequence": 12,
+        "locktime": None,
+        "sigs_count": 3,
+        "stack_size": 2,
+    },
+    # Liquid-like federated pegin with emergency recovery privkeys
+    {
+        "ms": f"or_i(and_b(pk({TPUBS[0]}/*),a:and_b(pk({TPUBS[1]}),a:and_b(pk({TPUBS[2]}),a:and_b(pk({TPUBS[3]}),s:pk({PUBKEYS[0]}))))),and_v(v:thresh(2,pkh({TPRVS[0]}),a:pkh({TPRVS[1]}),a:pkh({TPUBS[4]})),older(42)))",
+        "sequence": 42,
+        "locktime": None,
+        "sigs_count": 2,
+        "stack_size": 8,
+    },
 ]
 
 
@@ -62,7 +180,77 @@ def watchonly_test(self, ms):
             lambda: len(self.ms_wo_wallet.listunspent(minconf=0, addresses=[addr])) == 1
         )
         utxo = self.ms_wo_wallet.listunspent(minconf=0, addresses=[addr])[0]
-        assert utxo["txid"] == txid and not utxo["solvable"]  # No satisfaction logic (yet)
+        assert utxo["txid"] == txid and utxo["solvable"]
+
+    def signing_test(
+        self, ms, sequence, locktime, sigs_count, stack_size, sha256_preimages
+    ):
+        self.log.info(f"Importing private Miniscript '{ms}'")
+        desc = descsum_create(f"wsh({ms})")
+        res = self.ms_sig_wallet.importdescriptors(
+            [
+                {
+                    "desc": desc,
+                    "active": True,
+                    "range": 0,
+                    "next_index": 0,
+                    "timestamp": "now",
+                }
+            ]
+        )
+        assert res[0]["success"], res
+
+        self.log.info("Generating an address for it and testing it detects funds")
+        addr = self.ms_sig_wallet.getnewaddress()
+        txid = self.funder.sendtoaddress(addr, 0.01)
+        self.wait_until(lambda: txid in self.funder.getrawmempool())
+        self.funder.generatetoaddress(1, self.funder.getnewaddress())
+        utxo = self.ms_sig_wallet.listunspent(addresses=[addr])[0]
+        assert txid == utxo["txid"] and utxo["solvable"]
+
+        self.log.info("Creating a transaction spending these funds")
+        dest_addr = self.funder.getnewaddress()
+        seq = sequence if sequence is not None else 0xFFFFFFFF - 2
+        lt = locktime if locktime is not None else 0
+        psbt = self.ms_sig_wallet.createpsbt(
+            [
+                {
+                    "txid": txid,
+                    "vout": utxo["vout"],
+                    "sequence": seq,
+                }
+            ],
+            [{dest_addr: 0.009}],
+            lt,
+        )
+
+        self.log.info("Signing it and checking the satisfaction.")
+        if sha256_preimages is not None:
+            psbt = PSBT.from_base64(psbt)
+            for (h, preimage) in sha256_preimages.items():
+                k = PSBT_IN_SHA256.to_bytes(1, "big") + bytes.fromhex(h)
+                psbt.i[0].map[k] = bytes.fromhex(preimage)
+            psbt = psbt.to_base64()
+        res = self.ms_sig_wallet.walletprocesspsbt(psbt=psbt, finalize=False)
+        psbtin = self.nodes[0].rpc.decodepsbt(res["psbt"])["inputs"][0]
+        assert len(psbtin["partial_signatures"]) == sigs_count
+        res = self.ms_sig_wallet.finalizepsbt(res["psbt"])
+        assert res["complete"] == (stack_size is not None)
+
+        if stack_size is not None:
+            txin = self.nodes[0].rpc.decoderawtransaction(res["hex"])["vin"][0]
+            assert len(txin["txinwitness"]) == stack_size, txin["txinwitness"]
+            self.log.info("Broadcasting the transaction.")
+            # If necessary, satisfy a relative timelock
+            if sequence is not None:
+                self.funder.generatetoaddress(sequence, self.funder.getnewaddress())
+            # If necessary, satisfy an absolute timelock
+            height = self.funder.getblockcount()
+            if locktime is not None and height < locktime:
+                self.funder.generatetoaddress(
+                    locktime - height, self.funder.getnewaddress()
+                )
+            self.ms_sig_wallet.sendrawtransaction(res["hex"])
 
     def run_test(self):
         self.log.info("Making a descriptor wallet")
@@ -71,6 +259,8 @@ def run_test(self):
             wallet_name="ms_wo", descriptors=True, disable_private_keys=True
         )
         self.ms_wo_wallet = self.nodes[0].get_wallet_rpc("ms_wo")
+        self.nodes[0].createwallet(wallet_name="ms_sig", descriptors=True)
+        self.ms_sig_wallet = self.nodes[0].get_wallet_rpc("ms_sig")
 
         # Sanity check we wouldn't let an insane Miniscript descriptor in
         res = self.ms_wo_wallet.importdescriptors(
@@ -91,6 +281,17 @@ def run_test(self):
         for ms in MINISCRIPTS:
             self.watchonly_test(ms)
 
+        # Test we can sign for any Miniscript.
+        for ms in MINISCRIPTS_PRIV:
+            self.signing_test(
+                ms["ms"],
+                ms["sequence"],
+                ms["locktime"],
+                ms["sigs_count"],
+                ms["stack_size"],
+                ms.get("sha256_preimages"),
+            )
+
 
 if __name__ == "__main__":
     WalletMiniscriptTest().main()
diff --git a/test/functional/wallet_pruning.py b/test/functional/wallet_pruning.py
index 504014f839..1ceceaee93 100755
--- a/test/functional/wallet_pruning.py
+++ b/test/functional/wallet_pruning.py
@@ -123,7 +123,7 @@ def run_test(self):
 
         # A blk*.dat file is 128MB
         # Generate 250 light blocks
-        self.generate(self.nodes[0], 250, sync_fun=self.no_op)
+        self.generate(self.nodes[0], 250)
         # Generate 50MB worth of large blocks in the blk00000.dat file
         self.mine_large_blocks(self.nodes[0], 50)
 
diff --git a/test/get_previous_releases.py b/test/get_previous_releases.py
index 7f5f15655c..60c868ca04 100755
--- a/test/get_previous_releases.py
+++ b/test/get_previous_releases.py
@@ -80,6 +80,15 @@
     "078f96b1e92895009c798ab827fb3fde5f6719eee886bd0c0e93acab18ea4865": {"tag": "v23.0", "tarball": "bitcoin-23.0-riscv64-linux-gnu.tar.gz"},
     "c816780583009a9dad426dc0c183c89be9da98906e1e2c7ebae91041c1aaaaf3": {"tag": "v23.0", "tarball": "bitcoin-23.0-x86_64-apple-darwin.tar.gz"},
     "2cca490c1f2842884a3c5b0606f179f9f937177da4eadd628e3f7fd7e25d26d0": {"tag": "v23.0", "tarball": "bitcoin-23.0-x86_64-linux-gnu.tar.gz"},
+
+    "0b48b9e69b30037b41a1e6b78fb7cbcc48c7ad627908c99686e81f3802454609": {"tag": "v24.0.1", "tarball": "bitcoin-24.0.1-aarch64-linux-gnu.tar.gz"},
+    "37d7660f0277301744e96426bbb001d2206b8d4505385dfdeedf50c09aaaef60": {"tag": "v24.0.1", "tarball": "bitcoin-24.0.1-arm-linux-gnueabihf.tar.gz"},
+    "90ed59e86bfda1256f4b4cad8cc1dd77ee0efec2492bcb5af61402709288b62c": {"tag": "v24.0.1", "tarball": "bitcoin-24.0.1-arm64-apple-darwin.tar.gz"},
+    "7590645e8676f8b5fda62dc20174474c4ac8fd0defc83a19ed908ebf2e94dc11": {"tag": "v24.0.1", "tarball": "bitcoin-24.0.1-powerpc64-linux-gnu.tar.gz"},
+    "79e89a101f23ff87816675b98769cd1ee91059f95c5277f38f48f21a9f7f8509": {"tag": "v24.0.1", "tarball": "bitcoin-24.0.1-powerpc64le-linux-gnu.tar.gz"},
+    "6b163cef7de4beb07b8cb3347095e0d76a584019b1891135cd1268a1f05b9d88": {"tag": "v24.0.1", "tarball": "bitcoin-24.0.1-riscv64-linux-gnu.tar.gz"},
+    "e2f751512f3c0f00eb68ba946d9c829e6cf99422a61e8f5e0a7c109c318674d0": {"tag": "v24.0.1", "tarball": "bitcoin-24.0.1-x86_64-apple-darwin.tar.gz"},
+    "49df6e444515d457ea0b885d66f521f2a26ca92ccf73d5296082e633544253bf": {"tag": "v24.0.1", "tarball": "bitcoin-24.0.1-x86_64-linux-gnu.tar.gz"},
 }
 
 
diff --git a/test/sanitizer_suppressions/ubsan b/test/sanitizer_suppressions/ubsan
index 67ef512895..2fa4e383e2 100644
--- a/test/sanitizer_suppressions/ubsan
+++ b/test/sanitizer_suppressions/ubsan
@@ -53,6 +53,7 @@ unsigned-integer-overflow:policy/fees.cpp
 unsigned-integer-overflow:prevector.h
 unsigned-integer-overflow:script/interpreter.cpp
 unsigned-integer-overflow:txmempool.cpp
+unsigned-integer-overflow:xoroshiro128plusplus.h
 implicit-integer-sign-change:compat/stdin.cpp
 implicit-integer-sign-change:compressor.h
 implicit-integer-sign-change:crypto/
@@ -69,3 +70,4 @@ shift-base:crypto/
 shift-base:hash.cpp
 shift-base:streams.h
 shift-base:util/bip32.cpp
+shift-base:xoroshiro128plusplus.h
diff --git a/test/util/rpcauth-test.py b/test/util/rpcauth-test.py
index 53058dc394..8a7ff26dcb 100755
--- a/test/util/rpcauth-test.py
+++ b/test/util/rpcauth-test.py
@@ -4,7 +4,7 @@
 # file COPYING or http://www.opensource.org/licenses/mit-license.php.
 """Test share/rpcauth/rpcauth.py
 """
-import base64
+import re
 import configparser
 import hmac
 import importlib
@@ -28,18 +28,17 @@ def test_generate_salt(self):
             self.assertEqual(len(self.rpcauth.generate_salt(i)), i * 2)
 
     def test_generate_password(self):
+        """Test that generated passwords only consist of urlsafe characters."""
+        r = re.compile(r"[0-9a-zA-Z_-]*")
         password = self.rpcauth.generate_password()
-        expected_password = base64.urlsafe_b64encode(
-            base64.urlsafe_b64decode(password)).decode('utf-8')
-        self.assertEqual(expected_password, password)
+        self.assertTrue(r.fullmatch(password))
 
     def test_check_password_hmac(self):
         salt = self.rpcauth.generate_salt(16)
         password = self.rpcauth.generate_password()
         password_hmac = self.rpcauth.password_to_hmac(salt, password)
 
-        m = hmac.new(bytearray(salt, 'utf-8'),
-            bytearray(password, 'utf-8'), 'SHA256')
+        m = hmac.new(salt.encode('utf-8'), password.encode('utf-8'), 'SHA256')
         expected_password_hmac = m.hexdigest()
 
         self.assertEqual(expected_password_hmac, password_hmac)