Skip to content

Commit

Permalink
Merge #2780: RPC: Backport scantxoutset from BitCoin Core v0.17.2
Browse files Browse the repository at this point in the history
fe55859 Add functional test for scantxoutset (PeterL73)
4452207 Remove commented out code (PeterL73)
c5ba307 Backport scantxoutset and descriptors from BitCoin Core v0.17.2 (PeterL73)

Pull request description:

  ## Issue being fixed or feature implemented
  As I didn't like the idea of backporting partial functionality of an upstream feature I looked at how `scantxoutset` and descriptors where implemented in BitCoin Core. Thanks to @tecnovert in #2778 I was able to easily locate the introduction point of `scantxoutset` and descriptors into BitCoin Core. As there were only a few files involved I tried to backport the complete functionality from [BitCoin Core v0.17.2](https://github.com/bitcoin/bitcoin/tree/v0.17.2).

  ## What was done
  - added `script/descriptor.cpp` and `script/descriptor.h` from BitCoin Core
  - removed non PIVX compatible code from `script/descriptor.cpp`
    - changed `#include <util.h>` to `#include <util/system.h>`
    - removed wpkh, wsh support
  - added `scantxoutset` related code to `rpc/blockchain.cpp` and `rpc/client.cpp`
  - made some minor adjustments in backported code in `rpc/blockchain.cpp`
    - changed `HexStr` call used for outputting scriptPubKey
    - removed P2WPKH references from the help description
    - changed _xpub/xprv_ to _DRKV/DRKP_ as prefix for extended keys in the help description
  - added SigningProvider related code to `script/sign.cpp` and `script/sign.h`
  - added the functional test `rpc_scantxoutset.py` from BitCoin Core
    - changed the addresses and keys to fit PIVX requirements
    - changed the first of the "non HD unspent outputs" tests from 0.002 to 0.007, because PIVX does not have P2SH_SEGWIT or BECH32 addresses only LEGACY addresses are used in this test
    - dashpay@4127918

  ## How Has This Been Tested
  This was tested using pivx-cli and pivx-qt, by scanning for unspent transaction output with different descriptors and with the functional test.

  ## Breaking Changes

ACKs for top commit:
  tecnovert:
    tACK fe55859
  Liquid369:
    tACK fe55859
  Fuzzbawls:
    ACK fe55859

Tree-SHA512: a2b57706e98d8ea8794f7e85eedab3e55546e3e6ec813ed5a784bbbc4071a34dc3cce2861374cd74c5bf20097676163bb100be309121b595b89395d90a7588d8
  • Loading branch information
Fuzzbawls committed Nov 20, 2022
2 parents abb888b + fe55859 commit 8cc2181
Show file tree
Hide file tree
Showing 10 changed files with 994 additions and 4 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down Expand Up @@ -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 \
Expand Down
216 changes: 215 additions & 1 deletion src/rpc/blockchain.cpp
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -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"
Expand Down Expand Up @@ -1458,6 +1459,217 @@ UniValue getfeeinfo(const JSONRPCRequest& request)
return getblockindexstats(newRequest);
}

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

UniValue scantxoutset(const JSONRPCRequest& request)
{
if (request.fHelp || request.params.size() < 1 || request.params.size() > 2)
throw std::runtime_error(
"scantxoutset <action> ( <scanobjects> )\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(<address>) Outputs whose scriptPubKey corresponds to the specified address (does not include P2PK)\n"
" raw(<hex script>) Outputs whose scriptPubKey equals the specified hex scripts\n"
" combo(<pubkey>) P2PK and P2PKH outputs for the given pubkey\n"
" pkh(<pubkey>) P2PKH outputs for the given pubkey\n"
" sh(multi(<n>,<pubkey>,<pubkey>,...)) P2SH-multisig outputs for the given threshold and pubkeys\n"
"\nIn the above, <pubkey> 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<CScript> 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<CScript> 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<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));
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
// --------------------- ------------------------ ----------------------- ------ --------
Expand All @@ -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"} },
Expand Down
3 changes: 2 additions & 1 deletion src/rpc/client.cpp
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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" },
Expand Down
Loading

0 comments on commit 8cc2181

Please sign in to comment.