Skip to content

Commit 7830494

Browse files
committed
Blockchain/RPC: Add scantxoutset method to scan UTXO set
1 parent 9048575 commit 7830494

File tree

2 files changed

+246
-0
lines changed

2 files changed

+246
-0
lines changed

src/rpc/blockchain.cpp

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@
66
#include <rpc/blockchain.h>
77

88
#include <amount.h>
9+
#include <base58.h>
10+
#include <chain.h>
911
#include <chainparams.h>
1012
#include <checkpoints.h>
1113
#include <coins.h>
1214
#include <consensus/validation.h>
1315
#include <validation.h>
1416
#include <core_io.h>
1517
#include <index/txindex.h>
18+
#include <key_io.h>
1619
#include <policy/feerate.h>
1720
#include <policy/policy.h>
1821
#include <primitives/transaction.h>
@@ -27,6 +30,7 @@
2730
#include <validationinterface.h>
2831
#include <warnings.h>
2932

33+
#include <assert.h>
3034
#include <stdint.h>
3135

3236
#include <univalue.h>
@@ -1945,6 +1949,246 @@ bool FindScriptPubKey(std::atomic<int>& scan_progress, const std::atomic<bool>&
19451949
return true;
19461950
}
19471951

1952+
/** RAII object to prevent concurrency issue when scanning the txout set */
1953+
static std::mutex g_utxosetscan;
1954+
static std::atomic<int> g_scan_progress;
1955+
static std::atomic<bool> g_scan_in_progress;
1956+
static std::atomic<bool> g_should_abort_scan;
1957+
class CoinsViewScanReserver
1958+
{
1959+
private:
1960+
bool m_could_reserve;
1961+
public:
1962+
explicit CoinsViewScanReserver() : m_could_reserve(false) {}
1963+
1964+
bool reserve() {
1965+
assert (!m_could_reserve);
1966+
std::lock_guard<std::mutex> lock(g_utxosetscan);
1967+
if (g_scan_in_progress) {
1968+
return false;
1969+
}
1970+
g_scan_in_progress = true;
1971+
m_could_reserve = true;
1972+
return true;
1973+
}
1974+
1975+
~CoinsViewScanReserver() {
1976+
if (m_could_reserve) {
1977+
std::lock_guard<std::mutex> lock(g_utxosetscan);
1978+
g_scan_in_progress = false;
1979+
}
1980+
}
1981+
};
1982+
1983+
static const char *g_default_scantxoutset_script_types[] = { "P2PKH", "P2SH_P2WPKH", "P2WPKH" };
1984+
1985+
enum class OutputScriptType {
1986+
UNKNOWN,
1987+
P2PK,
1988+
P2PKH,
1989+
P2SH_P2WPKH,
1990+
P2WPKH
1991+
};
1992+
1993+
static inline OutputScriptType GetOutputScriptTypeFromString(const std::string& outputtype)
1994+
{
1995+
if (outputtype == "P2PK") return OutputScriptType::P2PK;
1996+
else if (outputtype == "P2PKH") return OutputScriptType::P2PKH;
1997+
else if (outputtype == "P2SH_P2WPKH") return OutputScriptType::P2SH_P2WPKH;
1998+
else if (outputtype == "P2WPKH") return OutputScriptType::P2WPKH;
1999+
else return OutputScriptType::UNKNOWN;
2000+
}
2001+
2002+
CTxDestination GetDestinationForKey(const CPubKey& key, OutputScriptType type)
2003+
{
2004+
switch (type) {
2005+
case OutputScriptType::P2PKH: return key.GetID();
2006+
case OutputScriptType::P2SH_P2WPKH:
2007+
case OutputScriptType::P2WPKH: {
2008+
if (!key.IsCompressed()) return key.GetID();
2009+
CTxDestination witdest = WitnessV0KeyHash(key.GetID());
2010+
if (type == OutputScriptType::P2SH_P2WPKH) {
2011+
CScript witprog = GetScriptForDestination(witdest);
2012+
return CScriptID(witprog);
2013+
} else {
2014+
return witdest;
2015+
}
2016+
}
2017+
default: assert(false);
2018+
}
2019+
}
2020+
2021+
UniValue scantxoutset(const JSONRPCRequest& request)
2022+
{
2023+
if (request.fHelp || request.params.size() < 1 || request.params.size() > 2)
2024+
throw std::runtime_error(
2025+
"scantxoutset <action> ( <scanobjects> )\n"
2026+
"\nScans the unspent transaction output set for possible entries that matches common scripts of given public keys.\n"
2027+
"\nArguments:\n"
2028+
"1. \"action\" (string, required) The action to execute\n"
2029+
" \"start\" for starting a scan\n"
2030+
" \"abort\" for aborting the current scan (returns true when abort was successful)\n"
2031+
" \"status\" for progress report (in %) of the current scan\n"
2032+
"2. \"scanobjects\" (array, optional) Array of scan objects (only one object type per scan object allowed)\n"
2033+
" [\n"
2034+
" { \"address\" : \"<address>\" }, (string, optional) Bitcoin address\n"
2035+
" { \"pubkey\" : (object, optional) Public key\n"
2036+
" {\n"
2037+
" \"pubkey\" : \"<pubkey\">, (string, required) HEX encoded public key\n"
2038+
" \"script_types\" : [ ... ], (array, optional) Array of script-types to derive from the pubkey (possible values: \"P2PKH\", \"P2SH-P2WPKH\", \"P2WPKH\")\n"
2039+
" }\n"
2040+
" },\n"
2041+
" ]\n"
2042+
"\nResult:\n"
2043+
"{\n"
2044+
" \"unspents\": [\n"
2045+
" {\n"
2046+
" \"txid\" : \"transactionid\", (string) The transaction id\n"
2047+
" \"vout\": n, (numeric) the vout value\n"
2048+
" \"scriptPubKey\" : \"script\", (string) the script key\n"
2049+
" \"amount\" : x.xxx, (numeric) The total amount in " + CURRENCY_UNIT + " of the unspent output\n"
2050+
" \"height\" : n, (numeric) Height of the unspent transaction output\n"
2051+
" }\n"
2052+
" ,...], \n"
2053+
" \"total_amount\" : x.xxx, (numeric) The total amount of all found unspent outputs in " + CURRENCY_UNIT + "\n"
2054+
"]\n"
2055+
);
2056+
2057+
RPCTypeCheck(request.params, {UniValue::VSTR, UniValue::VARR});
2058+
2059+
UniValue result(UniValue::VOBJ);
2060+
if (request.params[0].get_str() == "status") {
2061+
CoinsViewScanReserver reserver;
2062+
if (reserver.reserve()) {
2063+
// no scan in progress
2064+
return NullUniValue;
2065+
}
2066+
result.pushKV("progress", g_scan_progress);
2067+
return result;
2068+
} else if (request.params[0].get_str() == "abort") {
2069+
CoinsViewScanReserver reserver;
2070+
if (reserver.reserve()) {
2071+
// reserve was possible which means no scan was running
2072+
return false;
2073+
}
2074+
// set the abort flag
2075+
g_should_abort_scan = true;
2076+
return true;
2077+
} else if (request.params[0].get_str() == "start") {
2078+
CoinsViewScanReserver reserver;
2079+
if (!reserver.reserve()) {
2080+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Scan already in progress, use action \"abort\" or \"status\"");
2081+
}
2082+
std::set<CScript> needles;
2083+
CAmount total_in = 0;
2084+
2085+
// loop through the scan objects
2086+
for (const UniValue& scanobject : request.params[1].get_array().getValues()) {
2087+
if (!scanobject.isObject()) {
2088+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid scan object");
2089+
}
2090+
UniValue address_uni = find_value(scanobject, "address");
2091+
UniValue pubkey_uni = find_value(scanobject, "pubkey");
2092+
2093+
// make sure only one object type is present
2094+
if (1 != !address_uni.isNull() + !pubkey_uni.isNull()) {
2095+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Only one object type is allowed per scan object");
2096+
} else if (!address_uni.isNull() && !address_uni.isStr()) {
2097+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Scanobject \"address\" must contain a single string as value");
2098+
} else if (!pubkey_uni.isNull() && !pubkey_uni.isObject()) {
2099+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Scanobject \"pubkey\" must contain an object as value");
2100+
} else if (address_uni.isStr()) {
2101+
// type: address
2102+
// decode destination and derive the scriptPubKey
2103+
// add the script to the scan containers
2104+
CTxDestination dest = DecodeDestination(address_uni.get_str());
2105+
if (!IsValidDestination(dest)) {
2106+
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid address");
2107+
}
2108+
CScript script = GetScriptForDestination(dest);
2109+
assert(!script.empty());
2110+
needles.insert(script);
2111+
} else if (pubkey_uni.isObject()) {
2112+
// type: pubkey
2113+
// derive script(s) according to the script_type parameter
2114+
UniValue script_types_uni = find_value(pubkey_uni, "script_types");
2115+
UniValue pubkeydata_uni = find_value(pubkey_uni, "pubkey");
2116+
2117+
// check the script types and use the default if not provided
2118+
if (!script_types_uni.isNull() && !script_types_uni.isArray()) {
2119+
throw JSONRPCError(RPC_INVALID_PARAMETER, "script_types must be an array");
2120+
}
2121+
else if (script_types_uni.isNull()) {
2122+
// use the default script types
2123+
script_types_uni = UniValue(UniValue::VARR);
2124+
for (const char *t : g_default_scantxoutset_script_types) {
2125+
script_types_uni.push_back(t);
2126+
}
2127+
}
2128+
2129+
// check the acctual pubkey
2130+
if (!pubkeydata_uni.isStr() || !IsHex(pubkeydata_uni.get_str())) {
2131+
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Public key must be hex encoded");
2132+
}
2133+
CPubKey pubkey(ParseHexV(pubkeydata_uni, "pubkey"));
2134+
if (!pubkey.IsFullyValid()) {
2135+
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid public key");
2136+
}
2137+
2138+
// loop through the script types and derive the script
2139+
for (const UniValue& script_type_uni : script_types_uni.get_array().getValues()) {
2140+
OutputScriptType script_type = GetOutputScriptTypeFromString(script_type_uni.get_str());
2141+
if (script_type == OutputScriptType::UNKNOWN) throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid script type");
2142+
2143+
CScript script = GetScriptForDestination(GetDestinationForKey(pubkey, script_type));
2144+
assert(!script.empty());
2145+
needles.insert(script);
2146+
}
2147+
}
2148+
}
2149+
2150+
// Scan the unspent transaction output set for inputs
2151+
UniValue unspents(UniValue::VARR);
2152+
std::vector<CTxOut> input_txos;
2153+
std::map<COutPoint, Coin> coins;
2154+
g_should_abort_scan = false;
2155+
g_scan_progress = 0;
2156+
int64_t count = 0;
2157+
std::unique_ptr<CCoinsViewCursor> pcursor;
2158+
{
2159+
LOCK(cs_main);
2160+
FlushStateToDisk();
2161+
pcursor = std::unique_ptr<CCoinsViewCursor>(pcoinsdbview->Cursor());
2162+
assert(pcursor);
2163+
}
2164+
bool res = FindScriptPubKey(g_scan_progress, g_should_abort_scan, count, pcursor.get(), needles, coins);
2165+
result.pushKV("success", res);
2166+
result.pushKV("searched_items", count);
2167+
2168+
for (const auto& it : coins) {
2169+
const COutPoint& outpoint = it.first;
2170+
const Coin& coin = it.second;
2171+
const CTxOut& txo = coin.out;
2172+
input_txos.push_back(txo);
2173+
total_in += txo.nValue;
2174+
2175+
UniValue unspent(UniValue::VOBJ);
2176+
unspent.pushKV("txid", outpoint.hash.GetHex());
2177+
unspent.pushKV("vout", (int32_t)outpoint.n);
2178+
unspent.pushKV("scriptPubKey", HexStr(txo.scriptPubKey.begin(), txo.scriptPubKey.end()));
2179+
unspent.pushKV("amount", ValueFromAmount(txo.nValue));
2180+
unspent.pushKV("height", (int32_t)coin.nHeight);
2181+
2182+
unspents.push_back(unspent);
2183+
}
2184+
result.pushKV("unspents", unspents);
2185+
result.pushKV("total_amount", ValueFromAmount(total_in));
2186+
} else {
2187+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid command");
2188+
}
2189+
return result;
2190+
}
2191+
19482192
static const CRPCCommand commands[] =
19492193
{ // category name actor (function) argNames
19502194
// --------------------- ------------------------ ----------------------- ----------
@@ -1970,6 +2214,7 @@ static const CRPCCommand commands[] =
19702214
{ "blockchain", "verifychain", &verifychain, {"checklevel","nblocks"} },
19712215

19722216
{ "blockchain", "preciousblock", &preciousblock, {"blockhash"} },
2217+
{ "blockchain", "scantxoutset", &scantxoutset, {"action", "scanobjects"} },
19732218

19742219
/* Not shown in help */
19752220
{ "hidden", "invalidateblock", &invalidateblock, {"blockhash"} },

src/rpc/client.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
7979
{ "sendmany", 4, "subtractfeefrom" },
8080
{ "sendmany", 5 , "replaceable" },
8181
{ "sendmany", 6 , "conf_target" },
82+
{ "scantxoutset", 1, "scanobjects" },
8283
{ "addmultisigaddress", 0, "nrequired" },
8384
{ "addmultisigaddress", 1, "keys" },
8485
{ "createmultisig", 0, "nrequired" },

0 commit comments

Comments
 (0)