Skip to content

Commit

Permalink
Merge PIVX-Project#2780: RPC: Backport scantxoutset from BitCoin Core…
Browse files Browse the repository at this point in the history
… v0.17.2

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 PIVX-Project#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.