diff --git a/CMakeLists.txt b/CMakeLists.txt index 918cbcdca2358..2bce785e898b4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -449,6 +449,7 @@ set(COMMON_SOURCES ./src/pubkey.cpp ./src/saltedhasher.cpp ./src/scheduler.cpp + ./src/script/descriptor.cpp ./src/script/interpreter.cpp ./src/script/script.cpp ./src/script/sign.cpp diff --git a/src/Makefile.am b/src/Makefile.am index 717c3a95836af..6f932722dd041 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -269,6 +269,7 @@ BITCOIN_CORE_H = \ rpc/server.h \ saltedhasher.h \ scheduler.h \ + script/descriptor.h \ script/interpreter.h \ script/keyorigin.h \ script/script.h \ @@ -564,6 +565,7 @@ libbitcoin_common_a_SOURCES = \ pubkey.cpp \ saltedhasher.cpp \ scheduler.cpp \ + script/descriptor.cpp \ script/interpreter.cpp \ script/script.cpp \ script/sign.cpp \ diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index 6ba5acdaf3f38..f096b294a840a 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -1,7 +1,7 @@ // Copyright (c) 2010 Satoshi Nakamoto // Copyright (c) 2009-2014 The Bitcoin developers // Copyright (c) 2014-2015 The Dash developers -// Copyright (c) 2015-2020 The PIVX developers +// Copyright (c) 2015-2022 The PIVX developers // Distributed under the MIT software license, see the accompanying // file COPYING or https://www.opensource.org/licenses/mit-license.php. @@ -16,6 +16,7 @@ #include "policy/feerate.h" #include "policy/policy.h" #include "rpc/server.h" +#include "script/descriptor.h" #include "sync.h" #include "txdb.h" #include "util/system.h" @@ -1458,6 +1459,217 @@ UniValue getfeeinfo(const JSONRPCRequest& request) return getblockindexstats(newRequest); } +//! Search for a given set of pubkey scripts +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 + 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 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; + } + } +}; + +UniValue scantxoutset(const JSONRPCRequest& request) +{ + if (request.fHelp || request.params.size() < 1 || request.params.size() > 2) + throw std::runtime_error( + "scantxoutset ( )\n" + "\nEXPERIMENTAL warning: this call may be removed or changed in future releases.\n" + "\nScans the unspent transaction output set for entries that match certain output descriptors.\n" + "Examples of output descriptors are:\n" + " addr(
) Outputs whose scriptPubKey corresponds to the specified address (does not include P2PK)\n" + " raw() Outputs whose scriptPubKey equals the specified hex scripts\n" + " combo() P2PK and P2PKH outputs for the given pubkey\n" + " pkh() P2PKH outputs for the given pubkey\n" + " sh(multi(,,,...)) P2SH-multisig outputs for the given threshold and pubkeys\n" + "\nIn the above, either refers to a fixed public key in hexadecimal notation, or to an DRKV/DRKP optionally followed by one\n" + "or more path elements separated by \"/\", and optionally ending in \"/*\" (unhardened), or \"/*'\" or \"/*h\" (hardened) to specify all\n" + "unhardened or hardened child keys.\n" + "In the latter case, a range needs to be specified by below if different from 1000.\n" + "For more information on output descriptors, see the documentation in the doc/descriptors.md file.\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, required) Array of scan objects\n" + " [ Every scan object is either a string descriptor or an object:\n" + " \"descriptor\", (string, optional) An output descriptor\n" + " { (object, optional) An object with output descriptor and metadata\n" + " \"desc\": \"descriptor\", (string, required) An output descriptor\n" + " \"range\": n, (numeric, optional) Up to what child index HD chains should be explored (default: 1000)\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; + CAmount total_in = 0; + + // loop through the scan objects + for (const UniValue& scanobject : request.params[1].get_array().getValues()) { + std::string desc_str; + int range = 1000; + if (scanobject.isStr()) { + desc_str = scanobject.get_str(); + } else if (scanobject.isObject()) { + UniValue desc_uni = find_value(scanobject, "desc"); + if (desc_uni.isNull()) throw JSONRPCError(RPC_INVALID_PARAMETER, "Descriptor needs to be provided in scan object"); + desc_str = desc_uni.get_str(); + UniValue range_uni = find_value(scanobject, "range"); + if (!range_uni.isNull()) { + range = range_uni.get_int(); + if (range < 0 || range > 1000000) throw JSONRPCError(RPC_INVALID_PARAMETER, "range out of range"); + } + } else { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Scan object needs to be either a string or an object"); + } + + FlatSigningProvider provider; + auto desc = Parse(desc_str, provider); + if (!desc) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Invalid descriptor '%s'", desc_str)); + } + if (!desc->IsRange()) range = 0; + for (int i = 0; i <= range; ++i) { + std::vector scripts; + if (!desc->Expand(i, provider, scripts, provider)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Cannot derive script without private keys: '%s'", desc_str)); + } + needles.insert(scripts.begin(), scripts.end()); + } + } + + // 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.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)); + 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) okSafe argNames // --------------------- ------------------------ ----------------------- ------ -------- @@ -1479,6 +1691,8 @@ static const CRPCCommand commands[] = { "blockchain", "gettxoutsetinfo", &gettxoutsetinfo, true, {} }, { "blockchain", "verifychain", &verifychain, true, {"nblocks"} }, + { "blockchain", "scantxoutset", &scantxoutset, true, {"action", "scanobjects"} }, + /* Not shown in help */ { "hidden", "invalidateblock", &invalidateblock, true, {"blockhash"} }, { "hidden", "reconsiderblock", &reconsiderblock, true, {"blockhash"} }, diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 88a1beb9d3d54..56b4dfa3fbbf9 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -1,7 +1,7 @@ // Copyright (c) 2010 Satoshi Nakamoto // Copyright (c) 2009-2014 The Bitcoin developers // Copyright (c) 2014-2015 The Dash developers -// Copyright (c) 2015-2020 The PIVX developers +// Copyright (c) 2015-2022 The PIVX developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. @@ -143,6 +143,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { { "sendmany", 2, "minconf" }, { "sendmany", 4, "include_delegated" }, { "sendmany", 5, "subtract_fee_from" }, + { "scantxoutset", 1, "scanobjects" }, { "sendrawtransaction", 1, "allowhighfees" }, { "sendtoaddress", 1, "amount" }, { "sendtoaddress", 4, "subtract_fee" }, diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp new file mode 100644 index 0000000000000..dd66a056c9399 --- /dev/null +++ b/src/script/descriptor.cpp @@ -0,0 +1,546 @@ +// Copyright (c) 2018 The Bitcoin Core developers +// Copyright (c) 2022 The PIVX developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include