diff --git a/doc/release-notes.md b/doc/release-notes.md
--- a/doc/release-notes.md
+++ b/doc/release-notes.md
@@ -3,3 +3,9 @@
This release includes the following features and fixes:
+ - The new RPC `scantxoutset` can be used to scan the UTXO set for entries
+ that match certain output descriptors. Refer to the [output descriptors
+ reference documentation](/doc/descriptors.md) for more details. This call
+ is similar to `listunspent` but does not use a wallet, meaning that the
+ wallet can be disabled at compile or run time. This call is experimental,
+ as such, is subject to changes or removal in future releases.
diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp
--- a/src/rpc/blockchain.cpp
+++ b/src/rpc/blockchain.cpp
@@ -15,6 +15,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -31,6 +32,7 @@
#include
#include // boost::thread::interrupt
+#include
#include
#include
#include
@@ -2211,6 +2213,334 @@
return NullUniValue;
}
+//! Search for a given set of pubkey scripts
+static bool FindScriptPubKey(std::atomic &scan_progress,
+ const std::atomic &should_abort,
+ int64_t &count, CCoinsViewCursor *cursor,
+ const std::set &needles,
+ std::map &out_results) {
+ scan_progress = 0;
+ count = 0;
+ while (cursor->Valid()) {
+ COutPoint key;
+ Coin coin;
+ if (!cursor->GetKey(key) || !cursor->GetValue(coin)) {
+ return false;
+ }
+ if (++count % 8192 == 0) {
+ boost::this_thread::interruption_point();
+ if (should_abort) {
+ // allow to abort the scan via the abort reference
+ return false;
+ }
+ }
+ if (count % 256 == 0) {
+ // update progress reference every 256 item
+ const TxId &txid = key.GetTxId();
+ uint32_t high = 0x100 * *txid.begin() + *(txid.begin() + 1);
+ scan_progress = int(high * 100.0 / 65536.0 + 0.5);
+ }
+ if (needles.count(coin.GetTxOut().scriptPubKey)) {
+ out_results.emplace(key, coin);
+ }
+ cursor->Next();
+ }
+ scan_progress = 100;
+ return true;
+}
+
+/** RAII object to prevent concurrency issue when scanning the txout set */
+static std::mutex g_utxosetscan;
+static std::atomic g_scan_progress;
+static std::atomic g_scan_in_progress;
+static std::atomic g_should_abort_scan;
+class CoinsViewScanReserver {
+private:
+ bool m_could_reserve;
+
+public:
+ explicit CoinsViewScanReserver() : m_could_reserve(false) {}
+
+ bool reserve() {
+ assert(!m_could_reserve);
+ std::lock_guard lock(g_utxosetscan);
+ if (g_scan_in_progress) {
+ return false;
+ }
+ g_scan_in_progress = true;
+ m_could_reserve = true;
+ return true;
+ }
+
+ ~CoinsViewScanReserver() {
+ if (m_could_reserve) {
+ std::lock_guard lock(g_utxosetscan);
+ g_scan_in_progress = false;
+ }
+ }
+};
+
+static const char *g_default_scantxoutset_script_types[] = {"P2PKH"};
+
+enum class OutputScriptType { UNKNOWN, P2PK, P2PKH };
+
+static inline OutputScriptType
+GetOutputScriptTypeFromString(const std::string &outputtype) {
+ if (outputtype == "P2PK") {
+ return OutputScriptType::P2PK;
+ } else if (outputtype == "P2PKH") {
+ return OutputScriptType::P2PKH;
+ } else {
+ return OutputScriptType::UNKNOWN;
+ }
+}
+
+CTxDestination GetDestinationForKey(const CPubKey &key, OutputScriptType type) {
+ switch (type) {
+ case OutputScriptType::P2PKH:
+ return key.GetID();
+ default:
+ assert(false);
+ }
+}
+
+static UniValue scantxoutset(const Config &config,
+ const JSONRPCRequest &request) {
+ if (request.fHelp || request.params.size() < 1 ||
+ request.params.size() > 2) {
+ throw std::runtime_error(
+ "scantxoutset ( )\n"
+ "\nScans the unspent transaction output set for possible entries "
+ "that matches common scripts of given public keys.\n"
+ "Using addresses as scanobjects will _not_ detect unspent P2PK "
+ "txouts\n"
+ "\nArguments:\n"
+ "1. \"action\" (string, required) The action "
+ "to execute\n"
+ " \"start\" for starting a "
+ "scan\n"
+ " \"abort\" for aborting the "
+ "current scan (returns true when abort was successful)\n"
+ " \"status\" for progress "
+ "report (in %) of the current scan\n"
+ "2. \"scanobjects\" (array, optional) Array of "
+ "scan objects (only one object type per scan object allowed)\n"
+ " [\n"
+ " { \"address\" : \"\" }, (string, optional) "
+ "Bitcoin address\n"
+ " { \"script\" : \"\" }, (string, optional) "
+ "HEX encoded script (scriptPubKey)\n"
+ " { \"pubkey\" : (object, optional) "
+ "Public key\n"
+ " {\n"
+ " \"pubkey\" : \", (string, required) "
+ "HEX encoded public key\n"
+ " \"script_types\" : [ ... ], (array, optional) "
+ "Array of script-types to derive from the pubkey (possible values: "
+ "\"P2PK\", \"P2PKH\")\n"
+ " }\n"
+ " },\n"
+ " ]\n"
+ "\nResult:\n"
+ "{\n"
+ " \"unspents\": [\n"
+ " {\n"
+ " \"txid\" : \"transactionid\", (string) The transaction "
+ "id\n"
+ " \"vout\": n, (numeric) the vout value\n"
+ " \"scriptPubKey\" : \"script\", (string) the script key\n"
+ " \"amount\" : x.xxx, (numeric) The total amount "
+ "in " +
+ CURRENCY_UNIT +
+ " of the unspent output\n"
+ " \"height\" : n, (numeric) Height of the "
+ "unspent transaction output\n"
+ " }\n"
+ " ,...], \n"
+ " \"total_amount\" : x.xxx, (numeric) The total amount of "
+ "all found unspent outputs in " +
+ CURRENCY_UNIT +
+ "\n"
+ "]\n");
+ }
+
+ RPCTypeCheck(request.params, {UniValue::VSTR, UniValue::VARR});
+
+ UniValue result(UniValue::VOBJ);
+ if (request.params[0].get_str() == "status") {
+ CoinsViewScanReserver reserver;
+ if (reserver.reserve()) {
+ // no scan in progress
+ return NullUniValue;
+ }
+ result.pushKV("progress", g_scan_progress);
+ return result;
+ } else if (request.params[0].get_str() == "abort") {
+ CoinsViewScanReserver reserver;
+ if (reserver.reserve()) {
+ // reserve was possible which means no scan was running
+ return false;
+ }
+ // set the abort flag
+ g_should_abort_scan = true;
+ return true;
+ } else if (request.params[0].get_str() == "start") {
+ CoinsViewScanReserver reserver;
+ if (!reserver.reserve()) {
+ throw JSONRPCError(
+ RPC_INVALID_PARAMETER,
+ "Scan already in progress, use action \"abort\" or \"status\"");
+ }
+ std::set needles;
+ Amount total_in = Amount::zero();
+
+ // loop through the scan objects
+ for (const UniValue &scanobject :
+ request.params[1].get_array().getValues()) {
+ if (!scanobject.isObject()) {
+ throw JSONRPCError(RPC_INVALID_PARAMETER,
+ "Invalid scan object");
+ }
+ UniValue address_uni = find_value(scanobject, "address");
+ UniValue pubkey_uni = find_value(scanobject, "pubkey");
+ UniValue script_uni = find_value(scanobject, "script");
+
+ // make sure only one object type is present
+ if (1 != !address_uni.isNull() + !pubkey_uni.isNull() +
+ !script_uni.isNull()) {
+ throw JSONRPCError(
+ RPC_INVALID_PARAMETER,
+ "Only one object type is allowed per scan object");
+ } else if (!address_uni.isNull() && !address_uni.isStr()) {
+ throw JSONRPCError(RPC_INVALID_PARAMETER,
+ "Scanobject \"address\" must contain a "
+ "single string as value");
+ } else if (!pubkey_uni.isNull() && !pubkey_uni.isObject()) {
+ throw JSONRPCError(
+ RPC_INVALID_PARAMETER,
+ "Scanobject \"pubkey\" must contain an object as value");
+ } else if (!script_uni.isNull() && !script_uni.isStr()) {
+ throw JSONRPCError(RPC_INVALID_PARAMETER,
+ "Scanobject \"script\" must contain a "
+ "single string as value");
+ } else if (address_uni.isStr()) {
+ // type: address
+ // decode destination and derive the scriptPubKey
+ // add the script to the scan containers
+ CTxDestination dest = DecodeDestination(
+ address_uni.get_str(), config.GetChainParams());
+ if (!IsValidDestination(dest)) {
+ throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY,
+ "Invalid address");
+ }
+ CScript script = GetScriptForDestination(dest);
+ assert(!script.empty());
+ needles.insert(script);
+ } else if (pubkey_uni.isObject()) {
+ // type: pubkey
+ // derive script(s) according to the script_type parameter
+ UniValue script_types_uni =
+ find_value(pubkey_uni, "script_types");
+ UniValue pubkeydata_uni = find_value(pubkey_uni, "pubkey");
+
+ // check the script types and use the default if not provided
+ if (!script_types_uni.isNull() && !script_types_uni.isArray()) {
+ throw JSONRPCError(RPC_INVALID_PARAMETER,
+ "script_types must be an array");
+ } else if (script_types_uni.isNull()) {
+ // use the default script types
+ script_types_uni = UniValue(UniValue::VARR);
+ for (const char *t : g_default_scantxoutset_script_types) {
+ script_types_uni.push_back(t);
+ }
+ }
+
+ // check the acctual pubkey
+ if (!pubkeydata_uni.isStr() ||
+ !IsHex(pubkeydata_uni.get_str())) {
+ throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY,
+ "Public key must be hex encoded");
+ }
+ CPubKey pubkey(ParseHexV(pubkeydata_uni, "pubkey"));
+ if (!pubkey.IsFullyValid()) {
+ throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY,
+ "Invalid public key");
+ }
+
+ // loop through the script types and derive the script
+ for (const UniValue &script_type_uni :
+ script_types_uni.get_array().getValues()) {
+ OutputScriptType script_type =
+ GetOutputScriptTypeFromString(
+ script_type_uni.get_str());
+ if (script_type == OutputScriptType::UNKNOWN)
+ throw JSONRPCError(RPC_INVALID_PARAMETER,
+ "Invalid script type");
+ CScript script;
+ if (script_type == OutputScriptType::P2PK) {
+ // support legacy P2PK scripts
+ script << ToByteVector(pubkey) << OP_CHECKSIG;
+ } else {
+ script = GetScriptForDestination(
+ GetDestinationForKey(pubkey, script_type));
+ }
+ assert(!script.empty());
+ needles.insert(script);
+ }
+ } else if (script_uni.isStr()) {
+ // type: script
+ // check and add the script to the scan containers (needles
+ // array)
+ CScript script(ParseHexV(script_uni, "script"));
+ // TODO: check script: max length, has OP, is unspenable etc.
+ needles.insert(script);
+ }
+ }
+
+ // Scan the unspent transaction output set for inputs
+ UniValue unspents(UniValue::VARR);
+ std::vector input_txos;
+ std::map coins;
+ g_should_abort_scan = false;
+ g_scan_progress = 0;
+ int64_t count = 0;
+ std::unique_ptr pcursor;
+ {
+ LOCK(cs_main);
+ FlushStateToDisk();
+ pcursor = std::unique_ptr(pcoinsdbview->Cursor());
+ assert(pcursor);
+ }
+ bool res = FindScriptPubKey(g_scan_progress, g_should_abort_scan, count,
+ pcursor.get(), needles, coins);
+ result.pushKV("success", res);
+ result.pushKV("searched_items", count);
+
+ for (const auto &it : coins) {
+ const COutPoint &outpoint = it.first;
+ const Coin &coin = it.second;
+ const CTxOut &txo = coin.GetTxOut();
+ input_txos.push_back(txo);
+ total_in += txo.nValue;
+
+ UniValue unspent(UniValue::VOBJ);
+ unspent.pushKV("txid", outpoint.GetTxId().GetHex());
+ unspent.pushKV("vout", int32_t(outpoint.GetN()));
+ unspent.pushKV("scriptPubKey", HexStr(txo.scriptPubKey.begin(),
+ txo.scriptPubKey.end()));
+ unspent.pushKV("amount", ValueFromAmount(txo.nValue));
+ unspent.pushKV("height", int32_t(coin.GetHeight()));
+
+ unspents.push_back(unspent);
+ }
+ result.pushKV("unspents", unspents);
+ result.pushKV("total_amount", ValueFromAmount(total_in));
+ } else {
+ throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid command");
+ }
+ return result;
+}
+
// clang-format off
static const ContextFreeRPCCommand commands[] = {
// category name actor (function) argNames
@@ -2236,6 +2566,7 @@
{ "blockchain", "savemempool", savemempool, {} },
{ "blockchain", "verifychain", verifychain, {"checklevel","nblocks"} },
{ "blockchain", "preciousblock", preciousblock, {"blockhash"} },
+ { "blockchain", "scantxoutset", scantxoutset, {"action", "scanobjects"} },
/* Not shown in help */
{ "hidden", "getfinalizedblockhash", getfinalizedblockhash, {} },
diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp
--- a/src/rpc/client.cpp
+++ b/src/rpc/client.cpp
@@ -75,6 +75,7 @@
{"sendmany", 1, "amounts"},
{"sendmany", 2, "minconf"},
{"sendmany", 4, "subtractfeefrom"},
+ {"scantxoutset", 1, "scanobjects"},
{"addmultisigaddress", 0, "nrequired"},
{"addmultisigaddress", 1, "keys"},
{"createmultisig", 0, "nrequired"},
diff --git a/test/functional/rpc_scantxoutset.py b/test/functional/rpc_scantxoutset.py
new file mode 100755
--- /dev/null
+++ b/test/functional/rpc_scantxoutset.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+# Copyright (c) 2018 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+"""Test the scantxoutset rpc call."""
+import os
+import shutil
+
+from test_framework.test_framework import BitcoinTestFramework
+from test_framework.util import (assert_equal, assert_raises_rpc_error)
+
+
+class ScantxoutsetTest(BitcoinTestFramework):
+ def set_test_params(self):
+ self.num_nodes = 1
+ self.setup_clean_chain = True
+
+ def run_test(self):
+ self.log.info("Mining blocks...")
+ self.nodes[0].generate(110)
+
+ addr = self.nodes[0].getnewaddress("")
+ pubkey = self.nodes[0].getaddressinfo(addr)['pubkey']
+ self.nodes[0].sendtoaddress(addr, 2)
+ self.nodes[0].generate(1)
+
+ self.log.info("Stop node, remove wallet, mine again some blocks...")
+ self.stop_node(0)
+ shutil.rmtree(os.path.join(
+ self.nodes[0].datadir, "regtest", 'wallets'))
+ self.start_node(0)
+ self.nodes[0].generate(110)
+
+ self.restart_node(0, ['-nowallet'])
+ self.log.info("Test if we have found the non HD unspent outputs.")
+ assert_equal(self.nodes[0].scantxoutset(
+ "start", [{"pubkey": {"pubkey": pubkey}}])['total_amount'], 2)
+
+ self.log.info("Test invalid parameters.")
+ # missing pubkey object
+ assert_raises_rpc_error(-8, 'Scanobject "pubkey" must contain an object as value',
+ self.nodes[0].scantxoutset, "start", [{"pubkey": pubkey}])
+ # invalid object for address object
+ assert_raises_rpc_error(-8, 'Scanobject "address" must contain a single string as value', self.nodes[0].scantxoutset, "start", [
+ {"address": {"address": addr}}])
+
+
+if __name__ == '__main__':
+ ScantxoutsetTest().main()