Skip to content

Commit

Permalink
Merge bitcoin#12196: Add scantxoutset RPC method
Browse files Browse the repository at this point in the history
  • Loading branch information
kwvg committed Jul 13, 2021
1 parent fa1b6c0 commit 63ea35b
Show file tree
Hide file tree
Showing 4 changed files with 324 additions and 0 deletions.
274 changes: 274 additions & 0 deletions src/rpc/blockchain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@
#include <rpc/blockchain.h>

#include <amount.h>
#include <base58.h>
#include <chain.h>
#include <chainparams.h>
#include <checkpoints.h>
#include <coins.h>
#include <node/coinstats.h>
#include <core_io.h>
#include <consensus/validation.h>
#include <key_io.h>
#include <validation.h>
#include <index/txindex.h>
#include <policy/feerate.h>
Expand All @@ -36,6 +39,7 @@
#include <llmq/quorums_chainlocks.h>
#include <llmq/quorums_instantsend.h>

#include <assert.h>
#include <stdint.h>

#include <univalue.h>
Expand Down Expand Up @@ -2271,6 +2275,275 @@ static UniValue savemempool(const JSONRPCRequest& request)
return NullUniValue;
}

//! Search for a given set of pubkey scripts
bool FindScriptPubKey(std::atomic<int>& scan_progress, const std::atomic<bool>& should_abort, int64_t& count, CCoinsViewCursor* cursor, const std::set<CScript>& needles, std::map<COutPoint, Coin>& 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
uint32_t high = 0x100 * *key.hash.begin() + *(key.hash.begin() + 1);
scan_progress = (int)(high * 100.0 / 65536.0 + 0.5);
}
if (needles.count(coin.out.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<int> g_scan_progress;
static std::atomic<bool> g_scan_in_progress;
static std::atomic<bool> 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<std::mutex> 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<std::mutex> 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);
}
}

UniValue scantxoutset(const JSONRPCRequest& request)
{
if (request.fHelp || request.params.size() < 1 || request.params.size() > 2)
throw std::runtime_error(
"scantxoutset <action> ( <scanobjects> )\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\" : \"<address>\" }, (string, optional) Dash address\n"
" { \"script\" : \"<scriptPubKey>\" }, (string, optional) HEX encoded script (scriptPubKey)\n"
" { \"pubkey\" : (object, optional) Public key\n"
" {\n"
" \"pubkey\" : \"<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<CScript> needles;
CAmount total_in = 0;

// 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());
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<CTxOut> input_txos;
std::map<COutPoint, Coin> coins;
g_should_abort_scan = false;
g_scan_progress = 0;
int64_t count = 0;
std::unique_ptr<CCoinsViewCursor> pcursor;
{
LOCK(cs_main);
FlushStateToDisk();
pcursor = std::unique_ptr<CCoinsViewCursor>(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.out;
input_txos.push_back(txo);
total_in += txo.nValue;

UniValue unspent(UniValue::VOBJ);
unspent.pushKV("txid", outpoint.hash.GetHex());
unspent.pushKV("vout", (int32_t)outpoint.n);
unspent.pushKV("scriptPubKey", HexStr(txo.scriptPubKey.begin(), txo.scriptPubKey.end()));
unspent.pushKV("amount", ValueFromAmount(txo.nValue));
unspent.pushKV("height", (int32_t)coin.nHeight);

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;
}

static const CRPCCommand commands[] =
{ // category name actor (function) argNames
// --------------------- ------------------------ ----------------------- ----------
Expand Down Expand Up @@ -2301,6 +2574,7 @@ static const CRPCCommand commands[] =
{ "blockchain", "verifychain", &verifychain, {"checklevel","nblocks"} },

{ "blockchain", "preciousblock", &preciousblock, {"blockhash"} },
{ "blockchain", "scantxoutset", &scantxoutset, {"action", "scanobjects"} },

/* Not shown in help */
{ "hidden", "invalidateblock", &invalidateblock, {"blockhash"} },
Expand Down
1 change: 1 addition & 0 deletions src/rpc/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "sendmany", 6, "use_is" },
{ "sendmany", 7, "use_cj" },
{ "sendmany", 8, "conf_target" },
{ "scantxoutset", 1, "scanobjects" },
{ "addmultisigaddress", 0, "nrequired" },
{ "addmultisigaddress", 1, "keys" },
{ "createmultisig", 0, "nrequired" },
Expand Down
48 changes: 48 additions & 0 deletions test/functional/rpc_scantxoutset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/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."""
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import *

import shutil
import os

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_P2SH_SEGWIT = self.nodes[0].getnewaddress("", "p2sh-segwit")
pubk1 = self.nodes[0].getaddressinfo(addr_P2SH_SEGWIT)['pubkey']
addr_LEGACY = self.nodes[0].getnewaddress("", "legacy")
pubk2 = self.nodes[0].getaddressinfo(addr_LEGACY)['pubkey']
addr_BECH32 = self.nodes[0].getnewaddress("", "bech32")
pubk3 = self.nodes[0].getaddressinfo(addr_BECH32)['pubkey']
self.nodes[0].sendtoaddress(addr_P2SH_SEGWIT, 1)
self.nodes[0].sendtoaddress(addr_LEGACY, 2)
self.nodes[0].sendtoaddress(addr_BECH32, 3)
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": pubk1}}, {"pubkey": {"pubkey": pubk2}}, {"pubkey": {"pubkey": pubk3}}])['total_amount'], 6)
assert_equal(self.nodes[0].scantxoutset("start", [ {"address": addr_P2SH_SEGWIT}, {"address": addr_LEGACY}, {"address": addr_BECH32}])['total_amount'], 6)
assert_equal(self.nodes[0].scantxoutset("start", [ {"address": addr_P2SH_SEGWIT}, {"address": addr_LEGACY}, {"pubkey": {"pubkey": pubk3}} ])['total_amount'], 6)

self.log.info("Test invalid parameters.")
assert_raises_rpc_error(-8, 'Scanobject "pubkey" must contain an object as value', self.nodes[0].scantxoutset, "start", [ {"pubkey": pubk1}]) #missing pubkey object
assert_raises_rpc_error(-8, 'Scanobject "address" must contain a single string as value', self.nodes[0].scantxoutset, "start", [ {"address": {"address": addr_P2SH_SEGWIT}}]) #invalid object for address object

if __name__ == '__main__':
ScantxoutsetTest().main()
1 change: 1 addition & 0 deletions test/functional/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@
'p2p_unrequested_blocks.py',
'feature_asmap.py',
'feature_includeconf.py',
'rpc_scantxoutset.py',
'feature_logging.py',
'p2p_node_network_limited.py',
'feature_blocksdir.py',
Expand Down

0 comments on commit 63ea35b

Please sign in to comment.