mirror of
https://github.com/dashpay/dash.git
synced 2024-12-25 20:12:57 +01:00
partial bitcoin#13932: Additional utility RPCs for PSBT
This commit is contained in:
parent
73ce859de2
commit
afad681789
@ -128,6 +128,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
||||
{ "createpsbt", 1, "outputs" },
|
||||
{ "createpsbt", 2, "locktime" },
|
||||
{ "combinepsbt", 0, "txs"},
|
||||
{ "joinpsbts", 0, "txs"},
|
||||
{ "finalizepsbt", 1, "extract"},
|
||||
{ "converttopsbt", 1, "permitsigdata"},
|
||||
{ "gettxout", 1, "n" },
|
||||
|
@ -8,6 +8,7 @@
|
||||
#include <chainparams.h>
|
||||
#include <coins.h>
|
||||
#include <consensus/validation.h>
|
||||
#include <consensus/tx_verify.h>
|
||||
#include <core_io.h>
|
||||
#include <index/txindex.h>
|
||||
#include <init.h>
|
||||
@ -38,6 +39,7 @@
|
||||
#include <llmq/chainlocks.h>
|
||||
#include <llmq/instantsend.h>
|
||||
|
||||
#include <numeric>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <univalue.h>
|
||||
@ -1392,6 +1394,141 @@ UniValue converttopsbt(const JSONRPCRequest& request)
|
||||
return EncodeBase64(ssTx.str());
|
||||
}
|
||||
|
||||
UniValue utxoupdatepsbt(const JSONRPCRequest& request)
|
||||
{
|
||||
if (request.fHelp || request.params.size() != 1) {
|
||||
throw std::runtime_error(
|
||||
RPCHelpMan{"utxoupdatepsbt",
|
||||
"\nUpdates a PSBT with UTXOs retrieved from the UTXO set or the mempool.\n",
|
||||
{
|
||||
{"psbt", RPCArg::Type::STR, RPCArg::Optional::NO, "A base64 string of a PSBT"}
|
||||
},
|
||||
RPCResult {
|
||||
" \"psbt\" (string) The base64-encoded partially signed transaction with inputs updated\n"
|
||||
},
|
||||
RPCExamples {
|
||||
HelpExampleCli("utxoupdatepsbt", "\"psbt\"")
|
||||
}}.ToString());
|
||||
}
|
||||
|
||||
RPCTypeCheck(request.params, {UniValue::VSTR}, true);
|
||||
|
||||
// Unserialize the transactions
|
||||
PartiallySignedTransaction psbtx;
|
||||
std::string error;
|
||||
if (!DecodeBase64PSBT(psbtx, request.params[0].get_str(), error)) {
|
||||
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, strprintf("TX decode failed %s", error));
|
||||
}
|
||||
|
||||
// Fetch previous transactions (inputs):
|
||||
CCoinsView viewDummy;
|
||||
CCoinsViewCache view(&viewDummy);
|
||||
{
|
||||
LOCK2(cs_main, mempool.cs);
|
||||
CCoinsViewCache &viewChain = ::ChainstateActive().CoinsTip();
|
||||
CCoinsViewMemPool viewMempool(&viewChain, mempool);
|
||||
view.SetBackend(viewMempool); // temporarily switch cache backend to db+mempool view
|
||||
|
||||
for (const CTxIn& txin : psbtx.tx->vin) {
|
||||
view.AccessCoin(txin.prevout); // Load entries from viewChain into view; can fail.
|
||||
}
|
||||
|
||||
view.SetBackend(viewDummy); // switch back to avoid locking mempool for too long
|
||||
}
|
||||
|
||||
// Fill the inputs
|
||||
for (unsigned int i = 0; i < psbtx.tx->vin.size(); ++i) {
|
||||
PSBTInput& input = psbtx.inputs.at(i);
|
||||
|
||||
if (input.non_witness_utxo) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const Coin& coin = view.AccessCoin(psbtx.tx->vin[i].prevout);
|
||||
|
||||
std::vector<std::vector<unsigned char>> solutions_data;
|
||||
Solver(coin.out.scriptPubKey, solutions_data);
|
||||
}
|
||||
|
||||
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
|
||||
ssTx << psbtx;
|
||||
return EncodeBase64(ssTx.str());
|
||||
}
|
||||
|
||||
UniValue joinpsbts(const JSONRPCRequest& request)
|
||||
{
|
||||
if (request.fHelp || request.params.size() != 1) {
|
||||
throw std::runtime_error(
|
||||
RPCHelpMan{"joinpsbts",
|
||||
"\nJoins multiple distinct PSBTs with different inputs and outputs into one PSBT with inputs and outputs from all of the PSBTs\n"
|
||||
"No input in any of the PSBTs can be in more than one of the PSBTs.\n",
|
||||
{
|
||||
{"txs", RPCArg::Type::ARR, RPCArg::Optional::NO, "A json array of base64 strings of partially signed transactions",
|
||||
{
|
||||
{"psbt", RPCArg::Type::STR, RPCArg::Optional::NO, "A base64 string of a PSBT"}
|
||||
}}
|
||||
},
|
||||
RPCResult {
|
||||
" \"psbt\" (string) The base64-encoded partially signed transaction\n"
|
||||
},
|
||||
RPCExamples {
|
||||
HelpExampleCli("joinpsbts", "\"psbt\"")
|
||||
}}.ToString());
|
||||
}
|
||||
|
||||
RPCTypeCheck(request.params, {UniValue::VARR}, true);
|
||||
|
||||
// Unserialize the transactions
|
||||
std::vector<PartiallySignedTransaction> psbtxs;
|
||||
UniValue txs = request.params[0].get_array();
|
||||
|
||||
if (txs.size() <= 1) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "At least two PSBTs are required to join PSBTs.");
|
||||
}
|
||||
|
||||
int32_t best_version = 1;
|
||||
uint32_t best_locktime = 0xffffffff;
|
||||
for (unsigned int i = 0; i < txs.size(); ++i) {
|
||||
PartiallySignedTransaction psbtx;
|
||||
std::string error;
|
||||
if (!DecodeBase64PSBT(psbtx, txs[i].get_str(), error)) {
|
||||
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, strprintf("TX decode failed %s", error));
|
||||
}
|
||||
psbtxs.push_back(psbtx);
|
||||
// Choose the highest version number
|
||||
if (psbtx.tx->nVersion > best_version) {
|
||||
best_version = psbtx.tx->nVersion;
|
||||
}
|
||||
// Choose the lowest lock time
|
||||
if (psbtx.tx->nLockTime < best_locktime) {
|
||||
best_locktime = psbtx.tx->nLockTime;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a blank psbt where everything will be added
|
||||
PartiallySignedTransaction merged_psbt;
|
||||
merged_psbt.tx = CMutableTransaction();
|
||||
merged_psbt.tx->nVersion = best_version;
|
||||
merged_psbt.tx->nLockTime = best_locktime;
|
||||
|
||||
// Merge
|
||||
for (auto& psbt : psbtxs) {
|
||||
for (unsigned int i = 0; i < psbt.tx->vin.size(); ++i) {
|
||||
if (!merged_psbt.AddInput(psbt.tx->vin[i], psbt.inputs[i])) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Input %s:%d exists in multiple PSBTs", psbt.tx->vin[i].prevout.hash.ToString().c_str(), psbt.tx->vin[i].prevout.n));
|
||||
}
|
||||
}
|
||||
for (unsigned int i = 0; i < psbt.tx->vout.size(); ++i) {
|
||||
merged_psbt.AddOutput(psbt.tx->vout[i], psbt.outputs[i]);
|
||||
}
|
||||
merged_psbt.unknown.insert(psbt.unknown.begin(), psbt.unknown.end());
|
||||
}
|
||||
|
||||
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
|
||||
ssTx << merged_psbt;
|
||||
return EncodeBase64(ssTx.str());
|
||||
}
|
||||
|
||||
// clang-format off
|
||||
static const CRPCCommand commands[] =
|
||||
{ // category name actor (function) argNames
|
||||
@ -1409,6 +1546,8 @@ static const CRPCCommand commands[] =
|
||||
{ "rawtransactions", "finalizepsbt", &finalizepsbt, {"psbt", "extract"} },
|
||||
{ "rawtransactions", "createpsbt", &createpsbt, {"inputs","outputs","locktime"} },
|
||||
{ "rawtransactions", "converttopsbt", &converttopsbt, {"hexstring","permitsigdata"} },
|
||||
{ "rawtransactions", "utxoupdatepsbt", &utxoupdatepsbt, {"psbt"} },
|
||||
{ "rawtransactions", "joinpsbts", &joinpsbts, {"txs"} },
|
||||
|
||||
{ "blockchain", "gettxoutproof", &gettxoutproof, {"txids", "blockhash"} },
|
||||
{ "blockchain", "verifytxoutproof", &verifytxoutproof, {"proof"} },
|
||||
|
@ -7,6 +7,7 @@
|
||||
|
||||
import os
|
||||
import json
|
||||
from decimal import Decimal
|
||||
from test_framework.test_framework import BitcoinTestFramework
|
||||
from test_framework.util import assert_equal, assert_raises_rpc_error, find_output
|
||||
|
||||
@ -17,7 +18,8 @@ class PSBTTest(BitcoinTestFramework):
|
||||
self.setup_clean_chain = False
|
||||
self.num_nodes = 3
|
||||
# TODO: remove -txindex. Currently required for getrawtransaction call.
|
||||
self.extra_args = [[],
|
||||
self.extra_args = [
|
||||
["-txindex"],
|
||||
["-txindex"],
|
||||
["-txindex"]
|
||||
]
|
||||
@ -195,5 +197,49 @@ class PSBTTest(BitcoinTestFramework):
|
||||
# Test decoding error: invalid base64
|
||||
assert_raises_rpc_error(-22, "TX decode failed invalid base64", self.nodes[0].decodepsbt, ";definitely not base64;")
|
||||
|
||||
# Send to all types of addresses
|
||||
addr1 = self.nodes[1].getnewaddress()
|
||||
txid1 = self.nodes[0].sendtoaddress(addr1, 11)
|
||||
vout1 = find_output(self.nodes[0], txid1, 11)
|
||||
addr2 = self.nodes[1].getnewaddress()
|
||||
txid2 = self.nodes[0].sendtoaddress(addr2, 11)
|
||||
vout2 = find_output(self.nodes[0], txid2, 11)
|
||||
addr3 = self.nodes[1].getnewaddress()
|
||||
txid3 = self.nodes[0].sendtoaddress(addr3, 11)
|
||||
vout3 = find_output(self.nodes[0], txid3, 11)
|
||||
self.sync_all()
|
||||
|
||||
# Update a PSBT with UTXOs from the node
|
||||
# Inputs should not be filled because they are non-witness
|
||||
psbt = self.nodes[1].createpsbt([{"txid":txid1, "vout":vout1},{"txid":txid2, "vout":vout2},{"txid":txid3, "vout":vout3}], {self.nodes[0].getnewaddress():32.999})
|
||||
decoded = self.nodes[1].decodepsbt(psbt)
|
||||
assert "witness_utxo" not in decoded['inputs'][0] and "non_witness_utxo" not in decoded['inputs'][0]
|
||||
assert "witness_utxo" not in decoded['inputs'][1] and "non_witness_utxo" not in decoded['inputs'][1]
|
||||
assert "witness_utxo" not in decoded['inputs'][2] and "non_witness_utxo" not in decoded['inputs'][2]
|
||||
updated = self.nodes[1].utxoupdatepsbt(psbt)
|
||||
decoded = self.nodes[1].decodepsbt(updated)
|
||||
assert "witness_utxo" not in decoded['inputs'][0] and "non_witness_utxo" not in decoded['inputs'][0]
|
||||
assert "witness_utxo" not in decoded['inputs'][1] and "non_witness_utxo" not in decoded['inputs'][1]
|
||||
assert "witness_utxo" not in decoded['inputs'][2] and "non_witness_utxo" not in decoded['inputs'][2]
|
||||
|
||||
# Two PSBTs with a common input should not be joinable
|
||||
psbt1 = self.nodes[1].createpsbt([{"txid":txid1, "vout":vout1}], {self.nodes[0].getnewaddress():Decimal('10.999')})
|
||||
assert_raises_rpc_error(-8, "exists in multiple PSBTs", self.nodes[1].joinpsbts, [psbt1, updated])
|
||||
|
||||
# Join two distinct PSBTs
|
||||
addr4 = self.nodes[1].getnewaddress()
|
||||
txid4 = self.nodes[0].sendtoaddress(addr4, 5)
|
||||
vout4 = find_output(self.nodes[0], txid4, 5)
|
||||
self.nodes[0].generate(6)
|
||||
self.sync_all()
|
||||
psbt2 = self.nodes[1].createpsbt([{"txid":txid4, "vout":vout4}], {self.nodes[0].getnewaddress():Decimal('4.999')})
|
||||
psbt2 = self.nodes[1].walletprocesspsbt(psbt2)['psbt']
|
||||
psbt2_decoded = self.nodes[0].decodepsbt(psbt2)
|
||||
assert "final_scriptwitness" not in psbt2_decoded['inputs'][0] and "final_scriptSig" in psbt2_decoded['inputs'][0]
|
||||
joined = self.nodes[0].joinpsbts([psbt, psbt2])
|
||||
joined_decoded = self.nodes[0].decodepsbt(joined)
|
||||
assert len(joined_decoded['inputs']) == 4 and len(joined_decoded['outputs']) == 2 and "final_scriptwitness" not in joined_decoded['inputs'][3] and "final_scriptSig" not in joined_decoded['inputs'][3]
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
PSBTTest().main()
|
||||
|
Loading…
Reference in New Issue
Block a user