Allow createpsbt and walletcreatefundedpsbt to take psbt version

Use v2 for other RPCs. For some tests to work, PSBTv0 is set explicitly.
This commit is contained in:
Ava Chow 2024-07-22 17:14:50 -04:00
parent 29c3fb76ef
commit 2bcb9d07b2
11 changed files with 108 additions and 87 deletions

View file

@ -88,7 +88,7 @@ static constexpr uint8_t PSBT_SEPARATOR = 0x00;
const std::streamsize MAX_FILE_SIZE_PSBT = 100000000; // 100 MB const std::streamsize MAX_FILE_SIZE_PSBT = 100000000; // 100 MB
// PSBT version number // PSBT version number
static constexpr uint32_t PSBT_HIGHEST_VERSION = 0; static constexpr uint32_t PSBT_HIGHEST_VERSION = 2;
/** A structure for PSBT proprietary types */ /** A structure for PSBT proprietary types */
struct PSBTProprietary struct PSBTProprietary

View file

@ -170,6 +170,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "walletcreatefundedpsbt", 3, "solving_data"}, { "walletcreatefundedpsbt", 3, "solving_data"},
{ "walletcreatefundedpsbt", 3, "max_tx_weight"}, { "walletcreatefundedpsbt", 3, "max_tx_weight"},
{ "walletcreatefundedpsbt", 4, "bip32derivs" }, { "walletcreatefundedpsbt", 4, "bip32derivs" },
{ "walletcreatefundedpsbt", 5, "psbt_version" },
{ "walletprocesspsbt", 1, "sign" }, { "walletprocesspsbt", 1, "sign" },
{ "walletprocesspsbt", 3, "bip32derivs" }, { "walletprocesspsbt", 3, "bip32derivs" },
{ "walletprocesspsbt", 4, "finalize" }, { "walletprocesspsbt", 4, "finalize" },
@ -180,6 +181,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "createpsbt", 1, "outputs" }, { "createpsbt", 1, "outputs" },
{ "createpsbt", 2, "locktime" }, { "createpsbt", 2, "locktime" },
{ "createpsbt", 3, "replaceable" }, { "createpsbt", 3, "replaceable" },
{ "createpsbt", 4, "psbt_version" },
{ "combinepsbt", 0, "txs"}, { "combinepsbt", 0, "txs"},
{ "joinpsbts", 0, "txs"}, { "joinpsbts", 0, "txs"},
{ "finalizepsbt", 1, "extract"}, { "finalizepsbt", 1, "extract"},

View file

@ -1717,7 +1717,12 @@ static RPCHelpMan createpsbt()
"Implements the Creator role.\n" "Implements the Creator role.\n"
"Note that the transaction's inputs are not signed, and\n" "Note that the transaction's inputs are not signed, and\n"
"it is not stored in the wallet or transmitted to the network.\n", "it is not stored in the wallet or transmitted to the network.\n",
Cat<std::vector<RPCArg>>(
CreateTxDoc(), CreateTxDoc(),
{
{"psbt_version", RPCArg::Type::NUM, RPCArg::Default{2}, "The PSBT version number to use."},
}
),
RPCResult{ RPCResult{
RPCResult::Type::STR, "", "The resulting raw transaction (base64-encoded string)" RPCResult::Type::STR, "", "The resulting raw transaction (base64-encoded string)"
}, },
@ -1726,7 +1731,6 @@ static RPCHelpMan createpsbt()
}, },
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{ {
std::optional<bool> rbf; std::optional<bool> rbf;
if (!request.params[3].isNull()) { if (!request.params[3].isNull()) {
rbf = request.params[3].get_bool(); rbf = request.params[3].get_bool();
@ -1734,15 +1738,16 @@ static RPCHelpMan createpsbt()
CMutableTransaction rawTx = ConstructTransaction(request.params[0], request.params[1], request.params[2], rbf); CMutableTransaction rawTx = ConstructTransaction(request.params[0], request.params[1], request.params[2], rbf);
// Make a blank psbt // Make a blank psbt
PartiallySignedTransaction psbtx; uint32_t psbt_version = 2;
psbtx.tx = rawTx; if (!request.params[4].isNull()) {
for (unsigned int i = 0; i < rawTx.vin.size(); ++i) { psbt_version = request.params[4].getInt<int>();
psbtx.inputs.emplace_back(0);
} }
for (unsigned int i = 0; i < rawTx.vout.size(); ++i) { if (psbt_version != 2 && psbt_version != 0) {
psbtx.outputs.emplace_back(0); throw JSONRPCError(RPC_INVALID_PARAMETER, "The PSBT version can only be 2 or 0");
} }
PartiallySignedTransaction psbtx(rawTx, psbt_version);
// Serialize the PSBT // Serialize the PSBT
DataStream ssTx{}; DataStream ssTx{};
ssTx << psbtx; ssTx << psbtx;
@ -1801,14 +1806,7 @@ static RPCHelpMan converttopsbt()
} }
// Make a blank psbt // Make a blank psbt
PartiallySignedTransaction psbtx; PartiallySignedTransaction psbtx(tx, /*version=*/2);
psbtx.tx = tx;
for (unsigned int i = 0; i < tx.vin.size(); ++i) {
psbtx.inputs.emplace_back(0);
}
for (unsigned int i = 0; i < tx.vout.size(); ++i) {
psbtx.outputs.emplace_back(0);
}
// Serialize the PSBT // Serialize the PSBT
DataStream ssTx{}; DataStream ssTx{};

View file

@ -95,7 +95,7 @@ std::set<int> InterpretSubtractFeeFromOutputInstructions(const UniValue& sffo_in
static UniValue FinishTransaction(const std::shared_ptr<CWallet> pwallet, const UniValue& options, const CMutableTransaction& rawTx) static UniValue FinishTransaction(const std::shared_ptr<CWallet> pwallet, const UniValue& options, const CMutableTransaction& rawTx)
{ {
// Make a blank psbt // Make a blank psbt
PartiallySignedTransaction psbtx(rawTx); PartiallySignedTransaction psbtx(rawTx, /*version=*/2);
// First fill transaction with our data without signing, // First fill transaction with our data without signing,
// so external signers are not asked to sign more than once. // so external signers are not asked to sign more than once.
@ -1173,7 +1173,7 @@ static RPCHelpMan bumpfee_helper(std::string method_name)
result.pushKV("txid", txid.GetHex()); result.pushKV("txid", txid.GetHex());
} else { } else {
PartiallySignedTransaction psbtx(mtx); PartiallySignedTransaction psbtx(mtx, /*version=*/2);
bool complete = false; bool complete = false;
const auto err{pwallet->FillPSBT(psbtx, complete, SIGHASH_DEFAULT, /*sign=*/false, /*bip32derivs=*/true)}; const auto err{pwallet->FillPSBT(psbtx, complete, SIGHASH_DEFAULT, /*sign=*/false, /*bip32derivs=*/true)};
CHECK_NONFATAL(!err); CHECK_NONFATAL(!err);
@ -1725,6 +1725,7 @@ RPCHelpMan walletcreatefundedpsbt()
FundTxDoc()), FundTxDoc()),
RPCArgOptions{.oneline_description="options"}}, RPCArgOptions{.oneline_description="options"}},
{"bip32derivs", RPCArg::Type::BOOL, RPCArg::Default{true}, "Include BIP 32 derivation paths for public keys if we know them"}, {"bip32derivs", RPCArg::Type::BOOL, RPCArg::Default{true}, "Include BIP 32 derivation paths for public keys if we know them"},
{"psbt_version", RPCArg::Type::NUM, RPCArg::Default(2), "The PSBT version number to use."},
}, },
RPCResult{ RPCResult{
RPCResult::Type::OBJ, "", "", RPCResult::Type::OBJ, "", "",
@ -1772,7 +1773,15 @@ RPCHelpMan walletcreatefundedpsbt()
auto txr = FundTransaction(wallet, rawTx, recipients, options, coin_control, /*override_min_fee=*/true); auto txr = FundTransaction(wallet, rawTx, recipients, options, coin_control, /*override_min_fee=*/true);
// Make a blank psbt // Make a blank psbt
PartiallySignedTransaction psbtx(CMutableTransaction(*txr.tx)); uint32_t psbt_version = 2;
if (!request.params[5].isNull()) {
psbt_version = request.params[5].getInt<int>();
}
if (psbt_version != 2 && psbt_version != 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "The PSBT version can only be 2 or 0");
}
PartiallySignedTransaction psbtx(CMutableTransaction(*txr.tx), psbt_version);
// Fill transaction with out data but don't sign // Fill transaction with out data but don't sign
bool bip32derivs = request.params[4].isNull() ? true : request.params[4].get_bool(); bool bip32derivs = request.params[4].isNull() ? true : request.params[4].get_bool();

View file

@ -148,6 +148,7 @@
"bcrt1qqzh2ngh97ru8dfvgma25d6r595wcwqy0cee4cc": 1 "bcrt1qqzh2ngh97ru8dfvgma25d6r595wcwqy0cee4cc": 1
} }
], ],
"version": 0,
"result" : "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAAAAAA=" "result" : "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAAAAAA="
} }
], ],

View file

@ -32,6 +32,7 @@ from test_framework.psbt import (
PSBT_IN_NON_WITNESS_UTXO, PSBT_IN_NON_WITNESS_UTXO,
PSBT_IN_WITNESS_UTXO, PSBT_IN_WITNESS_UTXO,
PSBT_OUT_TAP_TREE, PSBT_OUT_TAP_TREE,
PSBT_OUT_SCRIPT,
) )
from test_framework.script import CScript, OP_TRUE from test_framework.script import CScript, OP_TRUE
from test_framework.script_util import MIN_STANDARD_TX_NONWITNESS_SIZE from test_framework.script_util import MIN_STANDARD_TX_NONWITNESS_SIZE
@ -85,9 +86,7 @@ class PSBTTest(BitcoinTestFramework):
# Modify the raw transaction by changing the output address, so the signature is no longer valid # Modify the raw transaction by changing the output address, so the signature is no longer valid
signed_psbt_obj = PSBT.from_base64(signed_psbt) signed_psbt_obj = PSBT.from_base64(signed_psbt)
substitute_addr = wallet.getnewaddress() signed_psbt_obj.o[0].map[PSBT_OUT_SCRIPT] = CScript([OP_TRUE])
raw = wallet.createrawtransaction([{"txid": utxos[0]["txid"], "vout": utxos[0]["vout"]}], [{substitute_addr: 0.9999}])
signed_psbt_obj.g.map[PSBT_GLOBAL_UNSIGNED_TX] = bytes.fromhex(raw)
# Check that the walletprocesspsbt call succeeds but also recognizes that the transaction is not complete # Check that the walletprocesspsbt call succeeds but also recognizes that the transaction is not complete
signed_psbt_incomplete = wallet.walletprocesspsbt(signed_psbt_obj.to_base64(), finalize=False) signed_psbt_incomplete = wallet.walletprocesspsbt(signed_psbt_obj.to_base64(), finalize=False)
@ -159,11 +158,11 @@ class PSBTTest(BitcoinTestFramework):
psbtx1 = wallet.walletcreatefundedpsbt([], {target_address: 0.1}, 0, {'fee_rate': 1, 'maxconf': 0})['psbt'] psbtx1 = wallet.walletcreatefundedpsbt([], {target_address: 0.1}, 0, {'fee_rate': 1, 'maxconf': 0})['psbt']
# Make sure we only had the one input # Make sure we only had the one input
tx1_inputs = self.nodes[0].decodepsbt(psbtx1)['tx']['vin'] tx1_inputs = self.nodes[0].decodepsbt(psbtx1)['inputs']
assert_equal(len(tx1_inputs), 1) assert_equal(len(tx1_inputs), 1)
utxo1 = tx1_inputs[0] utxo1 = tx1_inputs[0]
assert_equal(unconfirmed_txid, utxo1['txid']) assert_equal(unconfirmed_txid, utxo1['previous_txid'])
signed_tx1 = wallet.walletprocesspsbt(psbtx1) signed_tx1 = wallet.walletprocesspsbt(psbtx1)
txid1 = self.nodes[0].sendrawtransaction(signed_tx1['hex']) txid1 = self.nodes[0].sendrawtransaction(signed_tx1['hex'])
@ -172,23 +171,23 @@ class PSBTTest(BitcoinTestFramework):
assert txid1 in mempool assert txid1 in mempool
self.log.info("Fail to craft a new PSBT that sends more funds with add_inputs = False") self.log.info("Fail to craft a new PSBT that sends more funds with add_inputs = False")
assert_raises_rpc_error(-4, "The preselected coins total amount does not cover the transaction target. Please allow other inputs to be automatically selected or include more coins manually", wallet.walletcreatefundedpsbt, [{'txid': utxo1['txid'], 'vout': utxo1['vout']}], {target_address: 1}, 0, {'add_inputs': False}) assert_raises_rpc_error(-4, "The preselected coins total amount does not cover the transaction target. Please allow other inputs to be automatically selected or include more coins manually", wallet.walletcreatefundedpsbt, [{'txid': utxo1['previous_txid'], 'vout': utxo1['previous_vout']}], {target_address: 1}, 0, {'add_inputs': False})
self.log.info("Fail to craft a new PSBT with minconf above highest one") self.log.info("Fail to craft a new PSBT with minconf above highest one")
assert_raises_rpc_error(-4, "Insufficient funds", wallet.walletcreatefundedpsbt, [{'txid': utxo1['txid'], 'vout': utxo1['vout']}], {target_address: 1}, 0, {'add_inputs': True, 'minconf': 3, 'fee_rate': 10}) assert_raises_rpc_error(-4, "Insufficient funds", wallet.walletcreatefundedpsbt, [{'txid': utxo1['previous_txid'], 'vout': utxo1['previous_vout']}], {target_address: 1}, 0, {'add_inputs': True, 'minconf': 3, 'fee_rate': 10})
self.log.info("Fail to broadcast a new PSBT with maxconf 0 due to BIP125 rules to verify it actually chose unconfirmed outputs") self.log.info("Fail to broadcast a new PSBT with maxconf 0 due to BIP125 rules to verify it actually chose unconfirmed outputs")
psbt_invalid = wallet.walletcreatefundedpsbt([{'txid': utxo1['txid'], 'vout': utxo1['vout']}], {target_address: 1}, 0, {'add_inputs': True, 'maxconf': 0, 'fee_rate': 10})['psbt'] psbt_invalid = wallet.walletcreatefundedpsbt([{'txid': utxo1['previous_txid'], 'vout': utxo1['previous_vout']}], {target_address: 1}, 0, {'add_inputs': True, 'maxconf': 0, 'fee_rate': 10})['psbt']
signed_invalid = wallet.walletprocesspsbt(psbt_invalid) signed_invalid = wallet.walletprocesspsbt(psbt_invalid)
assert_raises_rpc_error(-26, "bad-txns-spends-conflicting-tx", self.nodes[0].sendrawtransaction, signed_invalid['hex']) assert_raises_rpc_error(-26, "bad-txns-spends-conflicting-tx", self.nodes[0].sendrawtransaction, signed_invalid['hex'])
self.log.info("Craft a replacement adding inputs with highest confs possible") self.log.info("Craft a replacement adding inputs with highest confs possible")
psbtx2 = wallet.walletcreatefundedpsbt([{'txid': utxo1['txid'], 'vout': utxo1['vout']}], {target_address: 1}, 0, {'add_inputs': True, 'minconf': 2, 'fee_rate': 10})['psbt'] psbtx2 = wallet.walletcreatefundedpsbt([{'txid': utxo1['previous_txid'], 'vout': utxo1['previous_vout']}], {target_address: 1}, 0, {'add_inputs': True, 'minconf': 2, 'fee_rate': 10})['psbt']
tx2_inputs = self.nodes[0].decodepsbt(psbtx2)['tx']['vin'] tx2_inputs = self.nodes[0].decodepsbt(psbtx2)['inputs']
assert_greater_than_or_equal(len(tx2_inputs), 2) assert_greater_than_or_equal(len(tx2_inputs), 2)
for vin in tx2_inputs: for vin in tx2_inputs:
if vin['txid'] != unconfirmed_txid: if vin['previous_txid'] != unconfirmed_txid:
assert_greater_than_or_equal(self.nodes[0].gettxout(vin['txid'], vin['vout'])['confirmations'], 2) assert_greater_than_or_equal(self.nodes[0].gettxout(vin['previous_txid'], vin['previous_vout'])['confirmations'], 2)
signed_tx2 = wallet.walletprocesspsbt(psbtx2) signed_tx2 = wallet.walletprocesspsbt(psbtx2)
txid2 = self.nodes[0].sendrawtransaction(signed_tx2['hex']) txid2 = self.nodes[0].sendrawtransaction(signed_tx2['hex'])
@ -205,7 +204,7 @@ class PSBTTest(BitcoinTestFramework):
# The decodepsbt RPC is stateless and independent of any settings, we can always just call it on the first node # The decodepsbt RPC is stateless and independent of any settings, we can always just call it on the first node
decoded_psbt = self.nodes[0].decodepsbt(psbtx["psbt"]) decoded_psbt = self.nodes[0].decodepsbt(psbtx["psbt"])
changepos = psbtx["changepos"] changepos = psbtx["changepos"]
assert_equal(decoded_psbt["tx"]["vout"][changepos]["scriptPubKey"]["type"], expected_type) assert_equal(decoded_psbt["outputs"][changepos]["script"]["type"], expected_type)
def run_test(self): def run_test(self):
# Create and fund a raw tx for sending 10 BTC # Create and fund a raw tx for sending 10 BTC
@ -247,7 +246,9 @@ class PSBTTest(BitcoinTestFramework):
max_tx_weight_sufficient = 1000 # 1k vbytes is enough max_tx_weight_sufficient = 1000 # 1k vbytes is enough
psbt = self.nodes[0].walletcreatefundedpsbt(outputs=dest_arg,locktime=0, options={"max_tx_weight": max_tx_weight_sufficient})["psbt"] psbt = self.nodes[0].walletcreatefundedpsbt(outputs=dest_arg,locktime=0, options={"max_tx_weight": max_tx_weight_sufficient})["psbt"]
weight = self.nodes[0].decodepsbt(psbt)["tx"]["weight"] psbt = self.nodes[0].walletprocesspsbt(psbt)["psbt"]
final_tx = self.nodes[0].finalizepsbt(psbt)["hex"]
weight = self.nodes[0].decoderawtransaction(final_tx)["weight"]
# ensure the transaction's weight is below the specified max_tx_weight. # ensure the transaction's weight is below the specified max_tx_weight.
assert_greater_than_or_equal(max_tx_weight_sufficient, weight) assert_greater_than_or_equal(max_tx_weight_sufficient, weight)
@ -258,7 +259,7 @@ class PSBTTest(BitcoinTestFramework):
self.nodes[0].walletcreatefundedpsbt, [{"txid": utxo1['txid'], "vout": utxo1['vout']}], {self.nodes[2].getnewaddress():90}) self.nodes[0].walletcreatefundedpsbt, [{"txid": utxo1['txid'], "vout": utxo1['vout']}], {self.nodes[2].getnewaddress():90})
psbtx1 = self.nodes[0].walletcreatefundedpsbt([{"txid": utxo1['txid'], "vout": utxo1['vout']}], {self.nodes[2].getnewaddress():90}, 0, {"add_inputs": True})['psbt'] psbtx1 = self.nodes[0].walletcreatefundedpsbt([{"txid": utxo1['txid'], "vout": utxo1['vout']}], {self.nodes[2].getnewaddress():90}, 0, {"add_inputs": True})['psbt']
assert_equal(len(self.nodes[0].decodepsbt(psbtx1)['tx']['vin']), 2) assert_equal(len(self.nodes[0].decodepsbt(psbtx1)['inputs']), 2)
# Inputs argument can be null # Inputs argument can be null
self.nodes[0].walletcreatefundedpsbt(None, {self.nodes[2].getnewaddress():10}) self.nodes[0].walletcreatefundedpsbt(None, {self.nodes[2].getnewaddress():10})
@ -491,12 +492,14 @@ class PSBTTest(BitcoinTestFramework):
# Update psbts, should only have data for one input and not the other # Update psbts, should only have data for one input and not the other
psbt1 = self.nodes[1].walletprocesspsbt(psbt_orig, False, "ALL")['psbt'] psbt1 = self.nodes[1].walletprocesspsbt(psbt_orig, False, "ALL")['psbt']
psbt1_decoded = self.nodes[0].decodepsbt(psbt1) psbt1_decoded = self.nodes[0].decodepsbt(psbt1)
assert psbt1_decoded['inputs'][0] and not psbt1_decoded['inputs'][1] assert len(psbt1_decoded['inputs'][0].keys()) > 3
assert len(psbt1_decoded['inputs'][1].keys()) == 3
# Check that BIP32 path was added # Check that BIP32 path was added
assert "bip32_derivs" in psbt1_decoded['inputs'][0] assert "bip32_derivs" in psbt1_decoded['inputs'][0]
psbt2 = self.nodes[2].walletprocesspsbt(psbt_orig, False, "ALL", False)['psbt'] psbt2 = self.nodes[2].walletprocesspsbt(psbt_orig, False, "ALL", False)['psbt']
psbt2_decoded = self.nodes[0].decodepsbt(psbt2) psbt2_decoded = self.nodes[0].decodepsbt(psbt2)
assert not psbt2_decoded['inputs'][0] and psbt2_decoded['inputs'][1] assert len(psbt2_decoded['inputs'][0].keys()) == 3
assert len(psbt2_decoded['inputs'][1].keys()) > 3
# Check that BIP32 paths were not added # Check that BIP32 paths were not added
assert "bip32_derivs" not in psbt2_decoded['inputs'][1] assert "bip32_derivs" not in psbt2_decoded['inputs'][1]
@ -518,33 +521,33 @@ class PSBTTest(BitcoinTestFramework):
unspent = self.nodes[0].listunspent()[0] unspent = self.nodes[0].listunspent()[0]
psbtx_info = self.nodes[0].walletcreatefundedpsbt([{"txid":unspent["txid"], "vout":unspent["vout"]}], [{self.nodes[2].getnewaddress():unspent["amount"]+1}], block_height+2, {"replaceable": False, "add_inputs": True}, False) psbtx_info = self.nodes[0].walletcreatefundedpsbt([{"txid":unspent["txid"], "vout":unspent["vout"]}], [{self.nodes[2].getnewaddress():unspent["amount"]+1}], block_height+2, {"replaceable": False, "add_inputs": True}, False)
decoded_psbt = self.nodes[0].decodepsbt(psbtx_info["psbt"]) decoded_psbt = self.nodes[0].decodepsbt(psbtx_info["psbt"])
for tx_in, psbt_in in zip(decoded_psbt["tx"]["vin"], decoded_psbt["inputs"]): for psbt_in in decoded_psbt["inputs"]:
assert_greater_than(tx_in["sequence"], MAX_BIP125_RBF_SEQUENCE) assert_greater_than(psbt_in["sequence"], MAX_BIP125_RBF_SEQUENCE)
assert "bip32_derivs" not in psbt_in assert "bip32_derivs" not in psbt_in
assert_equal(decoded_psbt["tx"]["locktime"], block_height+2) assert_equal(decoded_psbt["fallback_locktime"], block_height+2)
# Same construction with only locktime set and RBF explicitly enabled # Same construction with only locktime set and RBF explicitly enabled
psbtx_info = self.nodes[0].walletcreatefundedpsbt([{"txid":unspent["txid"], "vout":unspent["vout"]}], [{self.nodes[2].getnewaddress():unspent["amount"]+1}], block_height, {"replaceable": True, "add_inputs": True}, True) psbtx_info = self.nodes[0].walletcreatefundedpsbt([{"txid":unspent["txid"], "vout":unspent["vout"]}], [{self.nodes[2].getnewaddress():unspent["amount"]+1}], block_height, {"replaceable": True, "add_inputs": True}, True)
decoded_psbt = self.nodes[0].decodepsbt(psbtx_info["psbt"]) decoded_psbt = self.nodes[0].decodepsbt(psbtx_info["psbt"])
for tx_in, psbt_in in zip(decoded_psbt["tx"]["vin"], decoded_psbt["inputs"]): for psbt_in in decoded_psbt["inputs"]:
assert_equal(tx_in["sequence"], MAX_BIP125_RBF_SEQUENCE) assert_equal(psbt_in["sequence"], MAX_BIP125_RBF_SEQUENCE)
assert "bip32_derivs" in psbt_in assert "bip32_derivs" in psbt_in
assert_equal(decoded_psbt["tx"]["locktime"], block_height) assert_equal(decoded_psbt["fallback_locktime"], block_height)
# Same construction without optional arguments # Same construction without optional arguments
psbtx_info = self.nodes[0].walletcreatefundedpsbt([], [{self.nodes[2].getnewaddress():unspent["amount"]+1}]) psbtx_info = self.nodes[0].walletcreatefundedpsbt([], [{self.nodes[2].getnewaddress():unspent["amount"]+1}])
decoded_psbt = self.nodes[0].decodepsbt(psbtx_info["psbt"]) decoded_psbt = self.nodes[0].decodepsbt(psbtx_info["psbt"])
for tx_in, psbt_in in zip(decoded_psbt["tx"]["vin"], decoded_psbt["inputs"]): for psbt_in in decoded_psbt["inputs"]:
assert_equal(tx_in["sequence"], MAX_BIP125_RBF_SEQUENCE) assert_equal(psbt_in["sequence"], MAX_BIP125_RBF_SEQUENCE)
assert "bip32_derivs" in psbt_in assert "bip32_derivs" in psbt_in
assert_equal(decoded_psbt["tx"]["locktime"], 0) assert_equal(decoded_psbt["fallback_locktime"], 0)
# Same construction without optional arguments, for a node with -walletrbf=0 # Same construction without optional arguments, for a node with -walletrbf=0
unspent1 = self.nodes[1].listunspent()[0] unspent1 = self.nodes[1].listunspent()[0]
psbtx_info = self.nodes[1].walletcreatefundedpsbt([{"txid":unspent1["txid"], "vout":unspent1["vout"]}], [{self.nodes[2].getnewaddress():unspent1["amount"]+1}], block_height, {"add_inputs": True}) psbtx_info = self.nodes[1].walletcreatefundedpsbt([{"txid":unspent1["txid"], "vout":unspent1["vout"]}], [{self.nodes[2].getnewaddress():unspent1["amount"]+1}], block_height, {"add_inputs": True})
decoded_psbt = self.nodes[1].decodepsbt(psbtx_info["psbt"]) decoded_psbt = self.nodes[1].decodepsbt(psbtx_info["psbt"])
for tx_in, psbt_in in zip(decoded_psbt["tx"]["vin"], decoded_psbt["inputs"]): for psbt_in in decoded_psbt["inputs"]:
assert_greater_than(tx_in["sequence"], MAX_BIP125_RBF_SEQUENCE) assert_greater_than(psbt_in["sequence"], MAX_BIP125_RBF_SEQUENCE)
assert "bip32_derivs" in psbt_in assert "bip32_derivs" in psbt_in
# Make sure change address wallet does not have P2SH innerscript access to results in success # Make sure change address wallet does not have P2SH innerscript access to results in success
@ -614,7 +617,7 @@ class PSBTTest(BitcoinTestFramework):
# Creator Tests # Creator Tests
for creator in creators: for creator in creators:
created_tx = self.nodes[0].createpsbt(inputs=creator['inputs'], outputs=creator['outputs'], replaceable=False) created_tx = self.nodes[0].createpsbt(inputs=creator['inputs'], outputs=creator['outputs'], replaceable=False, psbt_version=creator['version'])
assert_equal(created_tx, creator['result']) assert_equal(created_tx, creator['result'])
# Signer tests # Signer tests
@ -668,54 +671,66 @@ class PSBTTest(BitcoinTestFramework):
utxo1, utxo2, utxo3 = self.create_outpoints(self.nodes[1], outputs=[{addr1: 11}, {addr2: 11}, {addr3: 11}]) utxo1, utxo2, utxo3 = self.create_outpoints(self.nodes[1], outputs=[{addr1: 11}, {addr2: 11}, {addr3: 11}])
self.sync_all() self.sync_all()
psbt_v2_required_keys = ["previous_vout", "sequence", "previous_txid"]
def test_psbt_input_keys(psbt_input, keys): def test_psbt_input_keys(psbt_input, keys):
"""Check that the psbt input has only the expected keys.""" """Check that the psbt input has only the expected keys."""
keys.extend(["previous_vout", "sequence", "previous_txid"])
assert_equal(set(keys), set(psbt_input.keys())) assert_equal(set(keys), set(psbt_input.keys()))
# Create a PSBT. None of the inputs are filled initially # Create a PSBT. None of the inputs are filled initially
psbt = self.nodes[1].createpsbt([utxo1, utxo2, utxo3], {self.nodes[0].getnewaddress():32.999}) psbt = self.nodes[1].createpsbt([utxo1, utxo2, utxo3], {self.nodes[0].getnewaddress():32.999})
decoded = self.nodes[1].decodepsbt(psbt) decoded = self.nodes[1].decodepsbt(psbt)
test_psbt_input_keys(decoded['inputs'][0], []) test_psbt_input_keys(decoded['inputs'][0], psbt_v2_required_keys)
test_psbt_input_keys(decoded['inputs'][1], []) test_psbt_input_keys(decoded['inputs'][1], psbt_v2_required_keys)
test_psbt_input_keys(decoded['inputs'][2], []) test_psbt_input_keys(decoded['inputs'][2], psbt_v2_required_keys)
# Update a PSBT with UTXOs from the node # Update a PSBT with UTXOs from the node
# Bech32 inputs should be filled with witness UTXO. Other inputs should not be filled because they are non-witness # Bech32 inputs should be filled with witness UTXO. Other inputs should not be filled because they are non-witness
updated = self.nodes[1].utxoupdatepsbt(psbt) updated = self.nodes[1].utxoupdatepsbt(psbt)
decoded = self.nodes[1].decodepsbt(updated) decoded = self.nodes[1].decodepsbt(updated)
test_psbt_input_keys(decoded['inputs'][0], ['witness_utxo', 'non_witness_utxo']) test_psbt_input_keys(decoded['inputs'][0], psbt_v2_required_keys + ['witness_utxo', 'non_witness_utxo'])
test_psbt_input_keys(decoded['inputs'][1], ['non_witness_utxo']) test_psbt_input_keys(decoded['inputs'][1], psbt_v2_required_keys + ['non_witness_utxo'])
test_psbt_input_keys(decoded['inputs'][2], ['non_witness_utxo']) test_psbt_input_keys(decoded['inputs'][2], psbt_v2_required_keys + ['non_witness_utxo'])
# Try again, now while providing descriptors, making P2SH-segwit work, and causing bip32_derivs and redeem_script to be filled in # Try again, now while providing descriptors, making P2SH-segwit work, and causing bip32_derivs and redeem_script to be filled in
descs = [self.nodes[1].getaddressinfo(addr)['desc'] for addr in [addr1,addr2,addr3]] descs = [self.nodes[1].getaddressinfo(addr)['desc'] for addr in [addr1,addr2,addr3]]
updated = self.nodes[1].utxoupdatepsbt(psbt=psbt, descriptors=descs) updated = self.nodes[1].utxoupdatepsbt(psbt=psbt, descriptors=descs)
decoded = self.nodes[1].decodepsbt(updated) decoded = self.nodes[1].decodepsbt(updated)
test_psbt_input_keys(decoded['inputs'][0], ['witness_utxo', 'non_witness_utxo', 'bip32_derivs']) test_psbt_input_keys(decoded['inputs'][0], psbt_v2_required_keys + ['witness_utxo', 'non_witness_utxo', 'bip32_derivs'])
test_psbt_input_keys(decoded['inputs'][1], ['non_witness_utxo', 'bip32_derivs']) test_psbt_input_keys(decoded['inputs'][1], psbt_v2_required_keys + ['non_witness_utxo', 'bip32_derivs'])
test_psbt_input_keys(decoded['inputs'][2], ['non_witness_utxo','witness_utxo', 'bip32_derivs', 'redeem_script']) test_psbt_input_keys(decoded['inputs'][2], psbt_v2_required_keys + ['non_witness_utxo', 'witness_utxo', 'bip32_derivs', 'redeem_script'])
# Cannot join PSBTv2s
psbt1 = self.nodes[1].createpsbt(inputs=[utxo1], outputs={self.nodes[0].getnewaddress():Decimal('10.999')}, psbt_version=0)
psbt2 = self.nodes[1].createpsbt(inputs=[utxo1], outputs={self.nodes[0].getnewaddress():Decimal('10.999')}, psbt_version=2)
assert_raises_rpc_error(-8, "joinpsbts only operates on version 0 PSBTs", self.nodes[1].joinpsbts, [psbt1, psbt2])
# Two PSBTs with a common input should not be joinable # Two PSBTs with a common input should not be joinable
psbt1 = self.nodes[1].createpsbt([utxo1], {self.nodes[0].getnewaddress():Decimal('10.999')}) psbt2 = self.nodes[1].createpsbt([utxo1], {self.nodes[0].getnewaddress():Decimal('10.999')}, psbt_version=0)
assert_raises_rpc_error(-8, "exists in multiple PSBTs", self.nodes[1].joinpsbts, [psbt1, updated]) assert_raises_rpc_error(-8, "exists in multiple PSBTs", self.nodes[1].joinpsbts, [psbt1, psbt2])
# Join two distinct PSBTs # Join two distinct PSBTs
psbt1 = self.nodes[1].createpsbt(inputs=[utxo1, utxo2, utxo3], outputs={self.nodes[0].getnewaddress():32.999}, psbt_version=0)
addr4 = self.nodes[1].getnewaddress("", "p2sh-segwit") addr4 = self.nodes[1].getnewaddress("", "p2sh-segwit")
utxo4 = self.create_outpoints(self.nodes[0], outputs=[{addr4: 5}])[0] utxo4 = self.create_outpoints(self.nodes[0], outputs=[{addr4: 5}])[0]
self.generate(self.nodes[0], 6) self.generate(self.nodes[0], 6)
psbt2 = self.nodes[1].createpsbt([utxo4], {self.nodes[0].getnewaddress():Decimal('4.999')}) psbt2 = self.nodes[1].createpsbt([utxo4], {self.nodes[0].getnewaddress():Decimal('4.999')}, psbt_version=0)
psbt2 = self.nodes[1].walletprocesspsbt(psbt2)['psbt'] psbt2 = self.nodes[1].walletprocesspsbt(psbt2)['psbt']
psbt2_decoded = self.nodes[0].decodepsbt(psbt2) psbt2_decoded = self.nodes[0].decodepsbt(psbt2)
assert "final_scriptwitness" in psbt2_decoded['inputs'][0] and "final_scriptSig" in psbt2_decoded['inputs'][0] assert "final_scriptwitness" in psbt2_decoded['inputs'][0] and "final_scriptSig" in psbt2_decoded['inputs'][0]
joined = self.nodes[0].joinpsbts([psbt, psbt2]) joined = self.nodes[0].joinpsbts([psbt1, psbt2])
joined_decoded = self.nodes[0].decodepsbt(joined) 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] assert_equal(len(joined_decoded['inputs']), 4)
assert_equal(len(joined_decoded['outputs']), 2)
assert "final_scriptwitness" not in joined_decoded['inputs'][3]
assert "final_scriptSig" not in joined_decoded['inputs'][3]
# Check that joining shuffles the inputs and outputs # Check that joining shuffles the inputs and outputs
# 10 attempts should be enough to get a shuffled join # 10 attempts should be enough to get a shuffled join
shuffled = False shuffled = False
for _ in range(10): for _ in range(10):
shuffled_joined = self.nodes[0].joinpsbts([psbt, psbt2]) shuffled_joined = self.nodes[0].joinpsbts([psbt1, psbt2])
shuffled |= joined != shuffled_joined shuffled |= joined != shuffled_joined
if shuffled: if shuffled:
break break
@ -802,11 +817,9 @@ class PSBTTest(BitcoinTestFramework):
final = signed['hex'] final = signed['hex']
dec = self.nodes[0].decodepsbt(signed["psbt"]) dec = self.nodes[0].decodepsbt(signed["psbt"])
for i, txin in enumerate(dec["tx"]["vin"]): for psbt_in in dec["inputs"]:
if txin["txid"] == ext_utxo["txid"] and txin["vout"] == ext_utxo["vout"]: if psbt_in["previous_txid"] == ext_utxo["txid"] and psbt_in["previous_vout"] == ext_utxo["vout"]:
input_idx = i
break break
psbt_in = dec["inputs"][input_idx]
scriptsig_hex = psbt_in["final_scriptSig"]["hex"] if "final_scriptSig" in psbt_in else "" scriptsig_hex = psbt_in["final_scriptSig"]["hex"] if "final_scriptSig" in psbt_in else ""
witness_stack_hex = psbt_in["final_scriptwitness"] if "final_scriptwitness" in psbt_in else None witness_stack_hex = psbt_in["final_scriptwitness"] if "final_scriptwitness" in psbt_in else None
input_weight = calculate_input_weight(scriptsig_hex, witness_stack_hex) input_weight = calculate_input_weight(scriptsig_hex, witness_stack_hex)

View file

@ -633,14 +633,14 @@ def test_watchonly_psbt(self, peer_node, rbf_node, dest_address):
{"fee_rate": 1, "add_inputs": False}, True)['psbt'] {"fee_rate": 1, "add_inputs": False}, True)['psbt']
psbt_signed = signer.walletprocesspsbt(psbt=psbt, sign=True, sighashtype="ALL", bip32derivs=True) psbt_signed = signer.walletprocesspsbt(psbt=psbt, sign=True, sighashtype="ALL", bip32derivs=True)
original_txid = watcher.sendrawtransaction(psbt_signed["hex"]) original_txid = watcher.sendrawtransaction(psbt_signed["hex"])
assert_equal(len(watcher.decodepsbt(psbt)["tx"]["vin"]), 1) assert_equal(len(watcher.decodepsbt(psbt)["inputs"]), 1)
# bumpfee can't be used on watchonly wallets # bumpfee can't be used on watchonly wallets
assert_raises_rpc_error(-4, "bumpfee is not available with wallets that have private keys disabled. Use psbtbumpfee instead.", watcher.bumpfee, original_txid) assert_raises_rpc_error(-4, "bumpfee is not available with wallets that have private keys disabled. Use psbtbumpfee instead.", watcher.bumpfee, original_txid)
# Bump fee, obnoxiously high to add additional watchonly input # Bump fee, obnoxiously high to add additional watchonly input
bumped_psbt = watcher.psbtbumpfee(original_txid, fee_rate=HIGH) bumped_psbt = watcher.psbtbumpfee(original_txid, fee_rate=HIGH)
assert_greater_than(len(watcher.decodepsbt(bumped_psbt['psbt'])["tx"]["vin"]), 1) assert_greater_than(len(watcher.decodepsbt(bumped_psbt['psbt'])["inputs"]), 1)
assert "txid" not in bumped_psbt assert "txid" not in bumped_psbt
assert_equal(bumped_psbt["origfee"], -watcher.gettransaction(original_txid)["fee"]) assert_equal(bumped_psbt["origfee"], -watcher.gettransaction(original_txid)["fee"])
assert not watcher.finalizepsbt(bumped_psbt["psbt"])["complete"] assert not watcher.finalizepsbt(bumped_psbt["psbt"])["complete"]

View file

@ -1171,10 +1171,10 @@ class RawTransactionsTest(BitcoinTestFramework):
tx = wallet.send(outputs=[{addr1: 8}], **options) tx = wallet.send(outputs=[{addr1: 8}], **options)
assert tx["complete"] assert tx["complete"]
# Check that only the preset inputs were added to the tx # Check that only the preset inputs were added to the tx
decoded_psbt_inputs = self.nodes[0].decodepsbt(tx["psbt"])['tx']['vin'] decoded_psbt_inputs = self.nodes[0].decodepsbt(tx["psbt"])["inputs"]
assert_equal(len(decoded_psbt_inputs), 2) assert_equal(len(decoded_psbt_inputs), 2)
for input in decoded_psbt_inputs: for input in decoded_psbt_inputs:
assert_equal(input["txid"], source_tx["txid"]) assert_equal(input["previous_txid"], source_tx["txid"])
# Case (5), assert that inputs are added to the tx by explicitly setting add_inputs=true # Case (5), assert that inputs are added to the tx by explicitly setting add_inputs=true
options = {"add_inputs": True, "add_to_wallet": True} options = {"add_inputs": True, "add_to_wallet": True}
@ -1213,10 +1213,10 @@ class RawTransactionsTest(BitcoinTestFramework):
}) })
psbt_tx = wallet.walletcreatefundedpsbt(outputs=[{addr1: 8}], inputs=inputs, **options) psbt_tx = wallet.walletcreatefundedpsbt(outputs=[{addr1: 8}], inputs=inputs, **options)
# Check that only the preset inputs were added to the tx # Check that only the preset inputs were added to the tx
decoded_psbt_inputs = self.nodes[0].decodepsbt(psbt_tx["psbt"])['tx']['vin'] decoded_psbt_inputs = self.nodes[0].decodepsbt(psbt_tx["psbt"])["inputs"]
assert_equal(len(decoded_psbt_inputs), 2) assert_equal(len(decoded_psbt_inputs), 2)
for input in decoded_psbt_inputs: for input in decoded_psbt_inputs:
assert_equal(input["txid"], source_tx["txid"]) assert_equal(input["previous_txid"], source_tx["txid"])
# Case (5), 'walletcreatefundedpsbt' command # Case (5), 'walletcreatefundedpsbt' command
# Explicit add_inputs=true, no preset inputs # Explicit add_inputs=true, no preset inputs

View file

@ -35,13 +35,13 @@ class WalletMultisigDescriptorPSBTTest(BitcoinTestFramework):
@staticmethod @staticmethod
def _check_psbt(psbt, to, value, multisig): def _check_psbt(psbt, to, value, multisig):
"""Helper function for any of the N participants to check the psbt with decodepsbt and verify it is OK before signing.""" """Helper function for any of the N participants to check the psbt with decodepsbt and verify it is OK before signing."""
tx = multisig.decodepsbt(psbt)["tx"] decoded = multisig.decodepsbt(psbt)
amount = 0 amount = 0
for vout in tx["vout"]: for psbt_out in decoded["outputs"]:
address = vout["scriptPubKey"]["address"] address = psbt_out["script"]["address"]
assert_equal(multisig.getaddressinfo(address)["ischange"], address != to) assert_equal(multisig.getaddressinfo(address)["ischange"], address != to)
if address == to: if address == to:
amount += vout["value"] amount += psbt_out["amount"]
assert_approx(amount, float(value), vspan=0.001) assert_approx(amount, float(value), vspan=0.001)
def participants_create_multisigs(self, external_xpubs, internal_xpubs): def participants_create_multisigs(self, external_xpubs, internal_xpubs):

View file

@ -380,10 +380,10 @@ class WalletSendTest(BitcoinTestFramework):
assert res["complete"] assert res["complete"]
res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, add_to_wallet=False, change_address=change_address, change_position=0) res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, add_to_wallet=False, change_address=change_address, change_position=0)
assert res["complete"] assert res["complete"]
assert_equal(self.nodes[0].decodepsbt(res["psbt"])["tx"]["vout"][0]["scriptPubKey"]["address"], change_address) assert_equal(self.nodes[0].decodepsbt(res["psbt"])["outputs"][0]["script"]["address"], change_address)
res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, add_to_wallet=False, change_type="legacy", change_position=0) res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, add_to_wallet=False, change_type="legacy", change_position=0)
assert res["complete"] assert res["complete"]
change_address = self.nodes[0].decodepsbt(res["psbt"])["tx"]["vout"][0]["scriptPubKey"]["address"] change_address = self.nodes[0].decodepsbt(res["psbt"])["outputs"][0]["script"]["address"]
assert change_address[0] == "m" or change_address[0] == "n" assert change_address[0] == "m" or change_address[0] == "n"
self.log.info("Set lock time...") self.log.info("Set lock time...")
@ -483,11 +483,9 @@ class WalletSendTest(BitcoinTestFramework):
assert signed["complete"] assert signed["complete"]
dec = self.nodes[0].decodepsbt(signed["psbt"]) dec = self.nodes[0].decodepsbt(signed["psbt"])
for i, txin in enumerate(dec["tx"]["vin"]): for psbt_in in dec["inputs"]:
if txin["txid"] == ext_utxo["txid"] and txin["vout"] == ext_utxo["vout"]: if psbt_in["previous_txid"] == ext_utxo["txid"] and psbt_in["previous_vout"] == ext_utxo["vout"]:
input_idx = i
break break
psbt_in = dec["inputs"][input_idx]
scriptsig_hex = psbt_in["final_scriptSig"]["hex"] if "final_scriptSig" in psbt_in else "" scriptsig_hex = psbt_in["final_scriptSig"]["hex"] if "final_scriptSig" in psbt_in else ""
witness_stack_hex = psbt_in["final_scriptwitness"] if "final_scriptwitness" in psbt_in else None witness_stack_hex = psbt_in["final_scriptwitness"] if "final_scriptwitness" in psbt_in else None
input_weight = calculate_input_weight(scriptsig_hex, witness_stack_hex) input_weight = calculate_input_weight(scriptsig_hex, witness_stack_hex)

View file

@ -306,9 +306,9 @@ class SendallTest(BitcoinTestFramework):
decoded = self.nodes[0].decodepsbt(psbt) decoded = self.nodes[0].decodepsbt(psbt)
assert_equal(len(decoded["inputs"]), 1) assert_equal(len(decoded["inputs"]), 1)
assert_equal(len(decoded["outputs"]), 1) assert_equal(len(decoded["outputs"]), 1)
assert_equal(decoded["tx"]["vin"][0]["txid"], utxo["txid"]) assert_equal(decoded["inputs"][0]["previous_txid"], utxo["txid"])
assert_equal(decoded["tx"]["vin"][0]["vout"], utxo["vout"]) assert_equal(decoded["inputs"][0]["previous_vout"], utxo["vout"])
assert_equal(decoded["tx"]["vout"][0]["scriptPubKey"]["address"], self.remainder_target) assert_equal(decoded["outputs"][0]["script"]["address"], self.remainder_target)
@cleanup @cleanup
def sendall_with_minconf(self): def sendall_with_minconf(self):