mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-02-02 09:46:52 -05:00
Implement joinpsbts RPC and tests
Adds a joinpsbts RPC which combines multiple distinct PSBTs into one PSBT.
This commit is contained in:
parent
7344a7b998
commit
08f749c914
5 changed files with 115 additions and 0 deletions
20
src/psbt.cpp
20
src/psbt.cpp
|
@ -42,6 +42,26 @@ bool PartiallySignedTransaction::IsSane() const
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool PartiallySignedTransaction::AddInput(const CTxIn& txin, PSBTInput& psbtin)
|
||||||
|
{
|
||||||
|
if (std::find(tx->vin.begin(), tx->vin.end(), txin) != tx->vin.end()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
tx->vin.push_back(txin);
|
||||||
|
psbtin.partial_sigs.clear();
|
||||||
|
psbtin.final_script_sig.clear();
|
||||||
|
psbtin.final_script_witness.SetNull();
|
||||||
|
inputs.push_back(psbtin);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PartiallySignedTransaction::AddOutput(const CTxOut& txout, const PSBTOutput& psbtout)
|
||||||
|
{
|
||||||
|
tx->vout.push_back(txout);
|
||||||
|
outputs.push_back(psbtout);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool PSBTInput::IsNull() const
|
bool PSBTInput::IsNull() const
|
||||||
{
|
{
|
||||||
return !non_witness_utxo && witness_utxo.IsNull() && partial_sigs.empty() && unknown.empty() && hd_keypaths.empty() && redeem_script.empty() && witness_script.empty();
|
return !non_witness_utxo && witness_utxo.IsNull() && partial_sigs.empty() && unknown.empty() && hd_keypaths.empty() && redeem_script.empty() && witness_script.empty();
|
||||||
|
|
|
@ -389,6 +389,8 @@ struct PartiallySignedTransaction
|
||||||
* same actual Bitcoin transaction.) Returns true if the merge succeeded, false otherwise. */
|
* same actual Bitcoin transaction.) Returns true if the merge succeeded, false otherwise. */
|
||||||
NODISCARD bool Merge(const PartiallySignedTransaction& psbt);
|
NODISCARD bool Merge(const PartiallySignedTransaction& psbt);
|
||||||
bool IsSane() const;
|
bool IsSane() const;
|
||||||
|
bool AddInput(const CTxIn& txin, PSBTInput& psbtin);
|
||||||
|
bool AddOutput(const CTxOut& txout, const PSBTOutput& psbtout);
|
||||||
PartiallySignedTransaction() {}
|
PartiallySignedTransaction() {}
|
||||||
PartiallySignedTransaction(const PartiallySignedTransaction& psbt_in) : tx(psbt_in.tx), inputs(psbt_in.inputs), outputs(psbt_in.outputs), unknown(psbt_in.unknown) {}
|
PartiallySignedTransaction(const PartiallySignedTransaction& psbt_in) : tx(psbt_in.tx), inputs(psbt_in.inputs), outputs(psbt_in.outputs), unknown(psbt_in.unknown) {}
|
||||||
explicit PartiallySignedTransaction(const CMutableTransaction& tx);
|
explicit PartiallySignedTransaction(const CMutableTransaction& tx);
|
||||||
|
|
|
@ -112,6 +112,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
||||||
{ "createpsbt", 2, "locktime" },
|
{ "createpsbt", 2, "locktime" },
|
||||||
{ "createpsbt", 3, "replaceable" },
|
{ "createpsbt", 3, "replaceable" },
|
||||||
{ "combinepsbt", 0, "txs"},
|
{ "combinepsbt", 0, "txs"},
|
||||||
|
{ "joinpsbts", 0, "txs"},
|
||||||
{ "finalizepsbt", 1, "extract"},
|
{ "finalizepsbt", 1, "extract"},
|
||||||
{ "converttopsbt", 1, "permitsigdata"},
|
{ "converttopsbt", 1, "permitsigdata"},
|
||||||
{ "converttopsbt", 2, "iswitness"},
|
{ "converttopsbt", 2, "iswitness"},
|
||||||
|
|
|
@ -1755,6 +1755,80 @@ UniValue utxoupdatepsbt(const JSONRPCRequest& request)
|
||||||
return EncodeBase64((unsigned char*)ssTx.data(), ssTx.size());
|
return EncodeBase64((unsigned char*)ssTx.data(), ssTx.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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((unsigned char*)ssTx.data(), ssTx.size());
|
||||||
|
}
|
||||||
|
|
||||||
// clang-format off
|
// clang-format off
|
||||||
static const CRPCCommand commands[] =
|
static const CRPCCommand commands[] =
|
||||||
{ // category name actor (function) argNames
|
{ // category name actor (function) argNames
|
||||||
|
@ -1774,6 +1848,7 @@ static const CRPCCommand commands[] =
|
||||||
{ "rawtransactions", "createpsbt", &createpsbt, {"inputs","outputs","locktime","replaceable"} },
|
{ "rawtransactions", "createpsbt", &createpsbt, {"inputs","outputs","locktime","replaceable"} },
|
||||||
{ "rawtransactions", "converttopsbt", &converttopsbt, {"hexstring","permitsigdata","iswitness"} },
|
{ "rawtransactions", "converttopsbt", &converttopsbt, {"hexstring","permitsigdata","iswitness"} },
|
||||||
{ "rawtransactions", "utxoupdatepsbt", &utxoupdatepsbt, {"psbt"} },
|
{ "rawtransactions", "utxoupdatepsbt", &utxoupdatepsbt, {"psbt"} },
|
||||||
|
{ "rawtransactions", "joinpsbts", &joinpsbts, {"txs"} },
|
||||||
|
|
||||||
{ "blockchain", "gettxoutproof", &gettxoutproof, {"txids", "blockhash"} },
|
{ "blockchain", "gettxoutproof", &gettxoutproof, {"txids", "blockhash"} },
|
||||||
{ "blockchain", "verifytxoutproof", &verifytxoutproof, {"proof"} },
|
{ "blockchain", "verifytxoutproof", &verifytxoutproof, {"proof"} },
|
||||||
|
|
|
@ -321,6 +321,23 @@ class PSBTTest(BitcoinTestFramework):
|
||||||
assert "witness_utxo" not in decoded['inputs'][1] and "non_witness_utxo" not in decoded['inputs'][1]
|
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]
|
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("", "p2sh-segwit")
|
||||||
|
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" 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__':
|
if __name__ == '__main__':
|
||||||
|
|
Loading…
Add table
Reference in a new issue