Repository: bitcoin/bitcoin (language: cpp)

Issue:
bitcoind dumps core when deriveaddresses is called with index 2147483647 (2^31-1)
The `deriveaddresses` JSON-RPC endpoint accepts a descriptor and an index for the derivation.

Currently, the RPC interface caps the allowed index derivation range in the interval [0,2^31-1].

However, calling `deriveaddresses` using a range that includes the index `2147483647` (2^31-1) results in a crash of bitcoind (on an AMD64 machine):

```
bitcoin-cli deriveaddresses "descriptor" "[2147483647, 2147483647]"

[bitcoind dumps core]
```

In the PR that addresses this pull request (#26275) is included a test case that causes the crash.
For convenience, here's the contents of a core dump generated with that test:

```
$ sudo coredumpctl debug bitcoind
           PID: 110184 (bitcoind)
           UID: 1000 (muxator)
           GID: 1000 (muxator)
        Signal: 6 (ABRT)
     Timestamp: Thu 2022-10-06 17:48:08 CEST (4min 56s ago)
  Command Line: <base>/src/bitcoind -datadir=/tmp/test_runner_₿_🏃_20221006_174805/rpc_deriveaddresses_crash_0/node0 -logtimemicros -debug -debugexclude=libevent -debugexclude=leveldb -uacomment=testnode0 -logthreadnames -logsourcelocations -loglevel=trace -sandbox=log-and-abort
    Executable: <base>/src/bitcoind
[...]
    Message: Process 110184 (bitcoind) of user 1000 dumped core.

[...]
                ELF object binary architecture: AMD x86-64

GNU gdb (GDB) Fedora 12.1-1.fc36
[...]
Core was generated by `<base>/src/bitcoind -datadir=/tmp/test_runner_₿_��'.
Program terminated with signal SIGABRT, Aborted.
#0  0x00007f55a8ac6c4c in __pthread_kill_implementation () from /lib64/libc.so.6
[Current thread is 1 (Thread 0x7f5576ffd640 (LWP 110197))]
Missing separate debuginfos, use: dnf debuginfo-install glibc-2.35-17.fc36.x86_64 libevent-2.1.12-6.fc36.x86_64 libgcc-12.2.1-2.fc36.x86_64 libstdc++-12.2.1-2.fc36.x86_64 sqlite-libs-3.36.0-5.fc36.x86_64 zlib-1.2.11-33.fc36.x86_64
(gdb) bt
#0  0x00007f55a8ac6c4c in __pthread_kill_implementation () from /lib64/libc.so.6
#1  0x00007f55a8a769c6 in raise () from /lib64/libc.so.6
#2  0x00007f55a8a607f4 in abort () from /lib64/libc.so.6
#3  0x00007f55a8c40c11 in __addvsi3 () from /lib64/libgcc_s.so.1
#4  0x0000563935a84d73 in operator() (__closure=0x7f5576ffbd50, self=..., request=...) at rpc/output_script.cpp:276
#5  0x0000563935a87ec2 in std::__invoke_impl<UniValue, deriveaddresses()::<lambda(const RPCHelpMan&, const JSONRPCRequest&)>&, const RPCHelpMan&, const JSONRPCRequest&>(std::__invoke_other, struct {...} &) (__f=...)
    at /usr/include/c++/12/bits/invoke.h:61
#6  0x0000563935a8773f in std::__invoke_r<UniValue, deriveaddresses()::<lambda(const RPCHelpMan&, const JSONRPCRequest&)>&, const RPCHelpMan&, const JSONRPCRequest&>(struct {...} &) (__fn=...) at /usr/include/c++/12/bits/invoke.h:116
#7  0x0000563935a86ec3 in std::_Function_handler<UniValue(const RPCHelpMan&, const JSONRPCRequest&), deriveaddresses()::<lambda(const RPCHelpMan&, const JSONRPCRequest&)> >::_M_invoke(const std::_Any_data &, const RPCHelpMan &, const JSONRPCRequest &) (__functor=..., __args#0=..., __args#1=...) at /usr/include/c++/12/bits/std_function.h:291
#8  0x00005639360b4641 in std::function<UniValue (RPCHelpMan const&, JSONRPCRequest const&)>::operator()(RPCHelpMan const&, JSONRPCRequest const&) const (this=0x7f5576ffbd50, __args#0=..., __args#1=...)
    at /usr/include/c++/12/bits/std_function.h:591
#9  0x00005639360a93b8 in RPCHelpMan::HandleRequest (this=0x7f5576ffbd30, request=...) at rpc/util.cpp:585
#10 0x00005639359cc1a2 in CRPCCommand::CRPCCommand(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, RPCHelpMan (*)())::{lambda(JSONRPCRequest const&, UniValue&, bool)#1}::operator()(JSONRPCRequest const&, UniValue&, bool) const (__closure=0x5639368fbb00 <RegisterOutputScriptRPCCommands(CRPCTable&)::commands+320>, request=..., result=...) at ./rpc/server.h:109
#11 0x00005639359e0be3 in std::__invoke_impl<bool, CRPCCommand::CRPCCommand(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, RPCHelpMan (*)())::{lambda(JSONRPCRequest const&, UniValue&, bool)#1}&, JSONRPCRequest const&, UniValue&, bool>(std::__invoke_other, CRPCCommand::CRPCCommand(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, RPCHelpMan (*)())::{lambda(JSONRPCRequest const&, UniValue&, bool)#1}&, JSONRPCRequest const&, UniValue&, bool&&) (__f=...) at /usr/include/c++/12/bits/invoke.h:61
#12 0x00005639359da9ba in std::__invoke_r<bool, CRPCCommand::CRPCCommand(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, RPCHelpMan (*)())::{lambda(JSONRPCRequest const&, UniValue&, bool)#1}&, JSONRPCRequest const&, UniValue&, bool>(CRPCCommand::CRPCCommand(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, RPCHelpMan (*)())::{lambda(JSONRPCRequest const&, UniValue&, bool)#1}&, JSONRPCRequest const&, UniValue&, bool&&) (__fn=...) at /usr/include/c++/12/bits/invoke.h:114
#13 0x00005639359d4916 in std::_Function_handler<bool (JSONRPCRequest const&, UniValue&, bool), CRPCCommand::CRPCCommand(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, RPCHelpMan (*)())::{lambda(JSONRPCRequest const&, UniValue&, bool)#1}>::_M_invoke(std::_Any_data const&, JSONRPCRequest const&, UniValue&, bool&&) (__functor=..., __args#0=..., __args#1=..., __args#2=@0x7f5576ffbf44: true) at /usr/include/c++/12/bits/std_function.h:290
#14 0x00005639359302c6 in std::function<bool (JSONRPCRequest const&, UniValue&, bool)>::operator()(JSONRPCRequest const&, UniValue&, bool) const (this=0x5639368fbb00 <RegisterOutputScriptRPCCommands(CRPCTable&)::commands+320>,
    __args#0=..., __args#1=..., __args#2=true) at /usr/include/c++/12/bits/std_function.h:591
#15 0x0000563935b0afd5 in ExecuteCommand (command=..., request=..., result=..., last_handler=true) at rpc/server.cpp:475
#16 0x0000563935b0abae in ExecuteCommands (commands=std::vector of length 1, capacity 1 = {...}, request=..., result=...) at rpc/server.cpp:440
#17 0x0000563935b0ad52 in CRPCTable::execute (this=0x5639368fc4c0 <tableRPC>, request=...) at rpc/server.cpp:460
#18 0x0000563935cbe3f5 in HTTPReq_JSONRPC (context=std::any containing node::NodeContext * = {...}, req=0x7f556c002da0) at httprpc.cpp:201
#19 0x0000563935cc0548 in operator() (__closure=0x7f556c001980, req=0x7f556c002da0) at httprpc.cpp:300
#20 0x0000563935cc1c3a in std::__invoke_impl<bool, StartHTTPRPC(const std::any&)::<lambda(HTTPRequest*, const std::string&)>&, HTTPRequest*, const std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&>(std::__invoke_other, struct {...} &) (__f=...) at /usr/include/c++/12/bits/invoke.h:61
#21 0x0000563935cc1a78 in std::__invoke_r<bool, StartHTTPRPC(const std::any&)::<lambda(HTTPRequest*, const std::string&)>&, HTTPRequest*, const std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&>(struct {...} &) (__fn=...) at /usr/include/c++/12/bits/invoke.h:114
#22 0x0000563935cc182e in std::_Function_handler<bool(HTTPRequest*, const std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&), StartHTTPRPC(const std::any&)::<lambda(HTTPRequest*, const std::string&)> >::_M_invoke(const std::_Any_data &, HTTPRequest *&&, const std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > &) (__functor=..., __args#0=@0x7f5576ffcb80: 0x7f556c002da0, __args#1="")
    at /usr/include/c++/12/bits/std_function.h:290
#23 0x0000563935cd1c0c in std::function<bool (HTTPRequest*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)>::operator()(HTTPRequest*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) const (this=0x7f556c003fb0, __args#0=0x7f556c002da0, __args#1="") at /usr/include/c++/12/bits/std_function.h:591
#24 0x0000563935cd0fc2 in HTTPWorkItem::operator() (this=0x7f556c003f80) at httpserver.cpp:56
#25 0x0000563935cd326d in WorkQueue<HTTPClosure>::Run (this=0x563938765d10) at httpserver.cpp:111
#26 0x0000563935ccbf1d in HTTPWorkQueueRun (queue=0x563938765d10, worker_num=3) at httpserver.cpp:343
#27 0x0000563935cdfd51 in std::__invoke_impl<void, void (*)(WorkQueue<HTTPClosure>*, int), WorkQueue<HTTPClosure>*, int> (__f=@0x56393877e7f8: 0x563935ccbeb2 <HTTPWorkQueueRun(WorkQueue<HTTPClosure>*, int)>)
    at /usr/include/c++/12/bits/invoke.h:61
#28 0x0000563935cdfaaa in std::__invoke<void (*)(WorkQueue<HTTPClosure>*, int), WorkQueue<HTTPClosure>*, int> (__fn=@0x56393877e7f8: 0x563935ccbeb2 <HTTPWorkQueueRun(WorkQueue<HTTPClosure>*, int)>)
    at /usr/include/c++/12/bits/invoke.h:96
#29 0x0000563935cdf8a6 in std::thread::_Invoker<std::tuple<void (*)(WorkQueue<HTTPClosure>*, int), WorkQueue<HTTPClosure>*, int> >::_M_invoke<0ul, 1ul, 2ul> (this=0x56393877e7e8) at /usr/include/c++/12/bits/std_thread.h:252
#30 0x0000563935cdf7db in std::thread::_Invoker<std::tuple<void (*)(WorkQueue<HTTPClosure>*, int), WorkQueue<HTTPClosure>*, int> >::operator() (this=0x56393877e7e8) at /usr/include/c++/12/bits/std_thread.h:259
#31 0x0000563935cdf60f in std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (*)(WorkQueue<HTTPClosure>*, int), WorkQueue<HTTPClosure>*, int> > >::_M_run (this=0x56393877e7e0) at /usr/include/c++/12/bits/std_thread.h:210
#32 0x00007f55a8e15b03 in execute_native_thread_routine () from /lib64/libstdc++.so.6
#33 0x00007f55a8ac4e2d in start_thread () from /lib64/libc.so.6
#34 0x00007f55a8b4a1b0 in clone3 () from /lib64/libc.so.6
```

The affected code seems to be in frame 4, and corresponds to the following code:

```
(gdb) frame 4
#4  0x0000563935a84d73 in operator() (__closure=0x7f5576ffbd50, self=..., request=...) at rpc/output_script.cpp:276
276                 for (int i = range_begin; i <= range_end; ++i) {
```

I think the reason is that, while `range_begin` and `range_end` are `uint64_t`, `i` is an `int`, which on many platforms means `int32_t`.
When `i` is assigned `2^31-1` and is then incremented, it wraps back and causes the crash.



Discussion / hints:

Fix patch:
diff --git a/.tx/config b/.tx/config
index 37c0bca0ab86a..20eff98d28b7f 100644
--- a/.tx/config
+++ b/.tx/config
@@ -1,7 +1,7 @@
 [main]
 host = https://www.transifex.com
 
-[bitcoin.qt-translation-024x]
+[o:bitcoin:p:bitcoin:r:qt-translation-024x]
 file_filter = src/qt/locale/bitcoin_<lang>.xlf
 source_file = src/qt/locale/bitcoin_en.xlf
 source_lang = en
diff --git a/src/rpc/output_script.cpp b/src/rpc/output_script.cpp
index 744f8098147c7..a980c609e808a 100644
--- a/src/rpc/output_script.cpp
+++ b/src/rpc/output_script.cpp
@@ -273,7 +273,7 @@ static RPCHelpMan deriveaddresses()
 
             UniValue addresses(UniValue::VARR);
 
-            for (int i = range_begin; i <= range_end; ++i) {
+            for (int64_t i = range_begin; i <= range_end; ++i) {
                 FlatSigningProvider provider;
                 std::vector<CScript> scripts;
                 if (!desc->Expand(i, key_provider, scripts, provider)) {
diff --git a/src/wallet/rpc/spend.cpp b/src/wallet/rpc/spend.cpp
index e38b13624c2d5..bc65cbf7bfe46 100644
--- a/src/wallet/rpc/spend.cpp
+++ b/src/wallet/rpc/spend.cpp
@@ -1380,7 +1380,7 @@ RPCHelpMan sendall()
                         throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Input not available. UTXO (%s:%d) was already spent.", input.prevout.hash.ToString(), input.prevout.n));
                     }
                     const CWalletTx* tx{pwallet->GetWalletTx(input.prevout.hash)};
-                    if (!tx || pwallet->IsMine(tx->tx->vout[input.prevout.n]) != (coin_control.fAllowWatchOnly ? ISMINE_ALL : ISMINE_SPENDABLE)) {
+                    if (!tx || input.prevout.n >= tx->tx->vout.size() || !(pwallet->IsMine(tx->tx->vout[input.prevout.n]) & (coin_control.fAllowWatchOnly ? ISMINE_ALL : ISMINE_SPENDABLE))) {
                         throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Input not found. UTXO (%s:%d) is not part of wallet.", input.prevout.hash.ToString(), input.prevout.n));
                     }
                     total_input_value += tx->tx->vout[input.prevout.n].nValue;
diff --git a/src/wallet/rpc/transactions.cpp b/src/wallet/rpc/transactions.cpp
index 0e13e4756bc45..6b9ec8ce10bfd 100644
--- a/src/wallet/rpc/transactions.cpp
+++ b/src/wallet/rpc/transactions.cpp
@@ -447,7 +447,7 @@ RPCHelpMan listtransactions()
                         {RPCResult::Type::OBJ, "", "", Cat(Cat<std::vector<RPCResult>>(
                         {
                             {RPCResult::Type::BOOL, "involvesWatchonly", /*optional=*/true, "Only returns true if imported addresses were involved in transaction."},
-                            {RPCResult::Type::STR, "address", "The bitcoin address of the transaction."},
+                            {RPCResult::Type::STR, "address",  /*optional=*/true, "The bitcoin address of the transaction (not returned if the output does not have an address, e.g. OP_RETURN null data)."},
                             {RPCResult::Type::STR, "category", "The transaction category.\n"
                                 "\"send\"                  Transactions sent.\n"
                                 "\"receive\"               Non-coinbase transactions received.\n"
@@ -561,7 +561,7 @@ RPCHelpMan listsinceblock()
                             {RPCResult::Type::OBJ, "", "", Cat(Cat<std::vector<RPCResult>>(
                             {
                                 {RPCResult::Type::BOOL, "involvesWatchonly", /*optional=*/true, "Only returns true if imported addresses were involved in transaction."},
-                                {RPCResult::Type::STR, "address", "The bitcoin address of the transaction."},
+                                {RPCResult::Type::STR, "address",  /*optional=*/true, "The bitcoin address of the transaction (not returned if the output does not have an address, e.g. OP_RETURN null data)."},
                                 {RPCResult::Type::STR, "category", "The transaction category.\n"
                                     "\"send\"                  Transactions sent.\n"
                                     "\"receive\"               Non-coinbase transactions received.\n"


Test patch:
diff --git a/test/functional/rpc_deriveaddresses.py b/test/functional/rpc_deriveaddresses.py
index 42d7d59d56313..a69326736d73c 100755
--- a/test/functional/rpc_deriveaddresses.py
+++ b/test/functional/rpc_deriveaddresses.py
@@ -44,6 +44,13 @@ def run_test(self):
         combo_descriptor = descsum_create("combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/0)")
         assert_equal(self.nodes[0].deriveaddresses(combo_descriptor), ["mtfUoUax9L4tzXARpw1oTGxWyoogp52KhJ", "mtfUoUax9L4tzXARpw1oTGxWyoogp52KhJ", address, "2NDvEwGfpEqJWfybzpKPHF2XH3jwoQV3D7x"])
 
+        # Before #26275, bitcoind would crash when deriveaddresses was
+        # called with derivation index 2147483647, which is the maximum
+        # positive value of a signed int32, and - currently - the
+        # maximum value that the deriveaddresses bitcoin RPC call
+        # accepts as derivation index.
+        assert_equal(self.nodes[0].deriveaddresses(descsum_create("wpkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/*)"), [2147483647, 2147483647]), ["bcrt1qtzs23vgzpreks5gtygwxf8tv5rldxvvsyfpdkg"])
+
         hardened_without_privkey_descriptor = descsum_create("wpkh(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1'/1/0)")
         assert_raises_rpc_error(-5, "Cannot derive script without private keys", self.nodes[0].deriveaddresses, hardened_without_privkey_descriptor)
 
diff --git a/test/functional/wallet_listsinceblock.py b/test/functional/wallet_listsinceblock.py
index f259449bef482..aff408ceb1447 100755
--- a/test/functional/wallet_listsinceblock.py
+++ b/test/functional/wallet_listsinceblock.py
@@ -45,6 +45,7 @@ def run_test(self):
         if self.options.descriptors:
             self.test_desc()
         self.test_send_to_self()
+        self.test_op_return()
 
     def test_no_blockhash(self):
         self.log.info("Test no blockhash")
@@ -448,6 +449,19 @@ def test_send_to_self(self):
         assert any(c["address"] == addr for c in coins)
         assert all(self.nodes[2].getaddressinfo(c["address"])["ischange"] for c in coins)
 
+    def test_op_return(self):
+        """Test if OP_RETURN outputs will be displayed correctly."""
+        block_hash = self.nodes[2].getbestblockhash()
+
+        raw_tx = self.nodes[2].createrawtransaction([], [{'data': 'aa'}])
+        funded_tx = self.nodes[2].fundrawtransaction(raw_tx)
+        signed_tx = self.nodes[2].signrawtransactionwithwallet(funded_tx['hex'])
+        tx_id = self.nodes[2].sendrawtransaction(signed_tx['hex'])
+
+        op_ret_tx = [tx for tx in self.nodes[2].listsinceblock(blockhash=block_hash)["transactions"] if tx['txid'] == tx_id][0]
+
+        assert 'address' not in op_ret_tx
+
 
 if __name__ == '__main__':
     ListSinceBlockTest().main()
diff --git a/test/functional/wallet_listtransactions.py b/test/functional/wallet_listtransactions.py
index 7c16b6328dd8f..9bb06774a5e4d 100755
--- a/test/functional/wallet_listtransactions.py
+++ b/test/functional/wallet_listtransactions.py
@@ -109,6 +109,7 @@ def run_test(self):
         self.run_rbf_opt_in_test()
         self.run_externally_generated_address_test()
         self.run_invalid_parameters_test()
+        self.test_op_return()
 
     def run_rbf_opt_in_test(self):
         """Test the opt-in-rbf flag for sent and received transactions."""
@@ -284,6 +285,17 @@ def run_invalid_parameters_test(self):
         assert_raises_rpc_error(-8, "Negative count", self.nodes[0].listtransactions, count=-1)
         assert_raises_rpc_error(-8, "Negative from", self.nodes[0].listtransactions, skip=-1)
 
+    def test_op_return(self):
+        """Test if OP_RETURN outputs will be displayed correctly."""
+        raw_tx = self.nodes[0].createrawtransaction([], [{'data': 'aa'}])
+        funded_tx = self.nodes[0].fundrawtransaction(raw_tx)
+        signed_tx = self.nodes[0].signrawtransactionwithwallet(funded_tx['hex'])
+        tx_id = self.nodes[0].sendrawtransaction(signed_tx['hex'])
+
+        op_ret_tx = [tx for tx in self.nodes[0].listtransactions() if tx['txid'] == tx_id][0]
+
+        assert 'address' not in op_ret_tx
+
 
 if __name__ == '__main__':
     ListTransactionsTest().main()
diff --git a/test/functional/wallet_sendall.py b/test/functional/wallet_sendall.py
index db4f32fe16179..4fe11455b1382 100755
--- a/test/functional/wallet_sendall.py
+++ b/test/functional/wallet_sendall.py
@@ -221,6 +221,11 @@ def sendall_fails_on_missing_input(self):
         self.add_utxos([16, 5])
         spent_utxo = self.wallet.listunspent()[0]
 
+        # fails on out of bounds vout
+        assert_raises_rpc_error(-8,
+                "Input not found. UTXO ({}:{}) is not part of wallet.".format(spent_utxo["txid"], 1000),
+                self.wallet.sendall, recipients=[self.remainder_target], options={"inputs": [{"txid": spent_utxo["txid"], "vout": 1000}]})
+
         # fails on unconfirmed spent UTXO
         self.wallet.sendall(recipients=[self.remainder_target])
         assert_raises_rpc_error(-8,
@@ -276,6 +281,33 @@ def sendall_fails_on_high_fee(self):
                 recipients=[self.remainder_target],
                 fee_rate=100000)
 
+    @cleanup
+    def sendall_watchonly_specific_inputs(self):
+        self.log.info("Test sendall with a subset of UTXO pool in a watchonly wallet")
+        self.add_utxos([17, 4])
+        utxo = self.wallet.listunspent()[0]
+
+        self.nodes[0].createwallet(wallet_name="watching", disable_private_keys=True)
+        watchonly = self.nodes[0].get_wallet_rpc("watching")
+
+        import_req = [{
+            "desc": utxo["desc"],
+            "timestamp": 0,
+        }]
+        if self.options.descriptors:
+            watchonly.importdescriptors(import_req)
+        else:
+            watchonly.importmulti(import_req)
+
+        sendall_tx_receipt = watchonly.sendall(recipients=[self.remainder_target], options={"inputs": [utxo]})
+        psbt = sendall_tx_receipt["psbt"]
+        decoded = self.nodes[0].decodepsbt(psbt)
+        assert_equal(len(decoded["inputs"]), 1)
+        assert_equal(len(decoded["outputs"]), 1)
+        assert_equal(decoded["tx"]["vin"][0]["txid"], utxo["txid"])
+        assert_equal(decoded["tx"]["vin"][0]["vout"], utxo["vout"])
+        assert_equal(decoded["tx"]["vout"][0]["scriptPubKey"]["address"], self.remainder_target)
+
     # This tests needs to be the last one otherwise @cleanup will fail with "Transaction too large" error
     def sendall_fails_with_transaction_too_large(self):
         self.log.info("Test that sendall fails if resulting transaction is too large")
@@ -341,6 +373,9 @@ def run_test(self):
         # Sendall fails when providing a fee that is too high
         self.sendall_fails_on_high_fee()
 
+        # Sendall succeeds with watchonly wallets spending specific UTXOs
+        self.sendall_watchonly_specific_inputs()
+
         # Sendall fails when many inputs result to too large transaction
         self.sendall_fails_with_transaction_too_large()