diff --git a/src/psbt.h b/src/psbt.h index c5bbff891f6..9e514f2eaa8 100644 --- a/src/psbt.h +++ b/src/psbt.h @@ -92,7 +92,7 @@ static constexpr uint8_t PSBT_SEPARATOR = 0x00; const std::streamsize MAX_FILE_SIZE_PSBT = 100000000; // 100 MB // 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 */ struct PSBTProprietary diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 8d3e551d797..a28543fb3bb 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -208,6 +208,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "walletcreatefundedpsbt", 3, "max_tx_weight"}, { "walletcreatefundedpsbt", 4, "bip32derivs" }, { "walletcreatefundedpsbt", 5, "version" }, + { "walletcreatefundedpsbt", 6, "psbt_version" }, { "walletprocesspsbt", 0, "psbt", ParamFormat::STRING }, { "walletprocesspsbt", 1, "sign" }, { "walletprocesspsbt", 2, "sighashtype", ParamFormat::STRING }, @@ -223,12 +224,14 @@ static const CRPCConvertParam vRPCConvertParams[] = { "createpsbt", 2, "locktime" }, { "createpsbt", 3, "replaceable" }, { "createpsbt", 4, "version" }, + { "createpsbt", 5, "psbt_version" }, { "combinepsbt", 0, "txs"}, { "joinpsbts", 0, "txs"}, { "finalizepsbt", 0, "psbt", ParamFormat::STRING }, { "finalizepsbt", 1, "extract"}, { "converttopsbt", 1, "permitsigdata"}, { "converttopsbt", 2, "iswitness"}, + { "converttopsbt", 3, "psbt_version"}, { "gettxout", 1, "n" }, { "gettxout", 2, "include_mempool" }, { "gettxoutproof", 0, "txids" }, @@ -328,6 +331,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "psbtbumpfee", 1, "replaceable"}, { "psbtbumpfee", 1, "outputs"}, { "psbtbumpfee", 1, "original_change_index"}, + { "psbtbumpfee", 1, "psbt_version"}, { "logging", 0, "include" }, { "logging", 1, "exclude" }, { "disconnectnode", 1, "nodeid" }, diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 539c24cd06b..396768edb71 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -1685,7 +1685,12 @@ static RPCMethod createpsbt() "Implements the Creator role.\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", - CreateTxDoc(), + Cat>( + CreateTxDoc(), + { + {"psbt_version", RPCArg::Type::NUM, RPCArg::Default{2}, "The PSBT version number to use."}, + } + ), RPCResult{ RPCResult::Type::STR, "", "The resulting raw transaction (base64-encoded string)" }, @@ -1694,7 +1699,6 @@ static RPCMethod createpsbt() }, [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue { - std::optional rbf; if (!request.params[3].isNull()) { rbf = request.params[3].get_bool(); @@ -1702,7 +1706,14 @@ static RPCMethod createpsbt() CMutableTransaction rawTx = ConstructTransaction(request.params[0], request.params[1], request.params[2], rbf, self.Arg("version")); // Make a blank psbt - PartiallySignedTransaction psbtx(rawTx); + uint32_t psbt_version = 2; + if (!request.params[5].isNull()) { + psbt_version = request.params[5].getInt(); + } + if (psbt_version != 2 && psbt_version != 0) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "The PSBT version can only be 2 or 0"); + } + PartiallySignedTransaction psbtx(rawTx, psbt_version); // Serialize the PSBT DataStream ssTx{}; @@ -1730,6 +1741,7 @@ static RPCMethod converttopsbt() "This boolean should reflect whether the transaction has inputs\n" "(e.g. fully valid, or on-chain transactions), if known by the caller." }, + {"psbt_version", RPCArg::Type::NUM, RPCArg::Default{2}, "The PSBT version number to use."}, }, RPCResult{ RPCResult::Type::STR, "", "The resulting raw transaction (base64-encoded string)" @@ -1763,7 +1775,14 @@ static RPCMethod converttopsbt() } // Make a blank psbt - PartiallySignedTransaction psbtx(tx); + uint32_t psbt_version = 2; + if (!request.params[3].isNull()) { + psbt_version = request.params[3].getInt(); + } + if (psbt_version != 2 && psbt_version != 0) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "The PSBT version can only be 2 or 0"); + } + PartiallySignedTransaction psbtx(tx, psbt_version); // Serialize the PSBT DataStream ssTx{}; diff --git a/src/wallet/rpc/spend.cpp b/src/wallet/rpc/spend.cpp index ef59c1f8b71..b6cdc8600f3 100644 --- a/src/wallet/rpc/spend.cpp +++ b/src/wallet/rpc/spend.cpp @@ -109,7 +109,7 @@ static UniValue FinishTransaction(const std::shared_ptr pwallet, const } // Make a blank psbt - PartiallySignedTransaction psbtx(rawTx); + PartiallySignedTransaction psbtx(rawTx, /*version=*/2); // First fill transaction with our data without signing, // so external signers are not asked to sign more than once. @@ -979,6 +979,7 @@ static RPCMethod bumpfee_helper(std::string method_name) { {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The txid to be bumped"}, {"options", RPCArg::Type::OBJ_NAMED_PARAMS, RPCArg::Optional::OMITTED, "", + Cat( { {"conf_target", RPCArg::Type::NUM, RPCArg::DefaultHint{"wallet -txconfirmtarget"}, "Confirmation target in blocks\n"}, {"fee_rate", RPCArg::Type::AMOUNT, RPCArg::DefaultHint{"not set, fall back to wallet fee estimation"}, @@ -1007,6 +1008,8 @@ static RPCMethod bumpfee_helper(std::string method_name) "original change output. The change output’s amount can increase if bumping the transaction " "adds new inputs, otherwise it will decrease. Cannot be used in combination with the 'outputs' option."}, }, + want_psbt ? std::vector{{"psbt_version", RPCArg::Type::NUM, RPCArg::Default(2), "The PSBT version number to use."}} : std::vector() + ), RPCArgOptions{.oneline_description="options"}}, }, RPCResult{ @@ -1045,6 +1048,8 @@ static RPCMethod bumpfee_helper(std::string method_name) std::optional original_change_index; + uint32_t psbt_version = 2; + if (!request.params[1].isNull()) { UniValue options = request.params[1]; RPCTypeCheckObj(options, @@ -1056,6 +1061,7 @@ static RPCMethod bumpfee_helper(std::string method_name) {"estimate_mode", UniValueType(UniValue::VSTR)}, {"outputs", UniValueType()}, // will be checked by AddOutputs() {"original_change_index", UniValueType(UniValue::VNUM)}, + {"psbt_version", UniValueType(UniValue::VNUM)}, }, true, true); @@ -1083,6 +1089,13 @@ static RPCMethod bumpfee_helper(std::string method_name) if (options.exists("original_change_index")) { original_change_index = options["original_change_index"].getInt(); } + + if (options.exists("psbt_version")) { + psbt_version = options["psbt_version"].getInt(); + } + if (psbt_version != 2 && psbt_version != 0) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "The PSBT version can only be 2 or 0"); + } } // Make sure the results are valid at least up to the most recent block @@ -1136,7 +1149,7 @@ static RPCMethod bumpfee_helper(std::string method_name) result.pushKV("txid", txid.GetHex()); } else { - PartiallySignedTransaction psbtx(mtx); + PartiallySignedTransaction psbtx(mtx, psbt_version); bool complete = false; const auto err{pwallet->FillPSBT(psbtx, {.sign = false, .bip32_derivs = true}, complete)}; CHECK_NONFATAL(!err); @@ -1723,6 +1736,7 @@ RPCMethod walletcreatefundedpsbt() RPCArgOptions{.oneline_description="options"}}, {"bip32derivs", RPCArg::Type::BOOL, RPCArg::Default{true}, "Include BIP 32 derivation paths for public keys if we know them"}, {"version", RPCArg::Type::NUM, RPCArg::Default{DEFAULT_WALLET_TX_VERSION}, "Transaction version"}, + {"psbt_version", RPCArg::Type::NUM, RPCArg::Default(2), "The PSBT version number to use."}, }, RPCResult{ RPCResult::Type::OBJ, "", "", @@ -1772,7 +1786,15 @@ RPCMethod walletcreatefundedpsbt() auto txr = FundTransaction(wallet, rawTx, recipients, options, coin_control, /*override_min_fee=*/true); // Make a blank psbt - PartiallySignedTransaction psbtx(CMutableTransaction(*txr.tx)); + uint32_t psbt_version = 2; + if (!request.params[6].isNull()) { + psbt_version = request.params[6].getInt(); + } + 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 bool bip32derivs = request.params[4].isNull() ? true : request.params[4].get_bool(); diff --git a/test/functional/data/rpc_psbt.json b/test/functional/data/rpc_psbt.json index fd46312af4d..8c049b8c7a4 100644 --- a/test/functional/data/rpc_psbt.json +++ b/test/functional/data/rpc_psbt.json @@ -180,6 +180,7 @@ "bcrt1qqzh2ngh97ru8dfvgma25d6r595wcwqy0cee4cc": 1 } ], + "version": 0, "result" : "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAAAAAA=" } ], diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py index cd47f61ada6..3c3e72887bd 100755 --- a/test/functional/rpc_psbt.py +++ b/test/functional/rpc_psbt.py @@ -37,6 +37,7 @@ from test_framework.psbt import ( PSBT_IN_WITNESS_UTXO, PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS, PSBT_OUT_TAP_TREE, + PSBT_OUT_SCRIPT, ) from test_framework.script import CScript, OP_TRUE, SIGHASH_ALL, SIGHASH_ANYONECANPAY from test_framework.script_util import MIN_STANDARD_TX_NONWITNESS_SIZE @@ -110,9 +111,7 @@ class PSBTTest(BitcoinTestFramework): # Modify the raw transaction by changing the output address, so the signature is no longer valid signed_psbt_obj = PSBT.from_base64(signed_psbt) - substitute_addr = wallet.getnewaddress() - 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) + signed_psbt_obj.o[0].map[PSBT_OUT_SCRIPT] = CScript([OP_TRUE]) # Check that the walletprocesspsbt call succeeds but also recognizes that the transaction is not complete signed_psbt_incomplete = wallet.walletprocesspsbt(psbt=signed_psbt_obj.to_base64(), finalize=False) @@ -185,11 +184,11 @@ class PSBTTest(BitcoinTestFramework): psbtx1 = wallet.walletcreatefundedpsbt([], {target_address: 0.1}, 0, {'fee_rate': 1, 'maxconf': 0})['psbt'] # 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) utxo1 = tx1_inputs[0] - assert_equal(unconfirmed_txid, utxo1['txid']) + assert_equal(unconfirmed_txid, utxo1['previous_txid']) signed_tx1 = wallet.walletprocesspsbt(psbtx1) txid1 = self.nodes[0].sendrawtransaction(signed_tx1['hex']) @@ -198,23 +197,23 @@ class PSBTTest(BitcoinTestFramework): assert txid1 in mempool 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") - 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") - 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) 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") - psbtx2 = wallet.walletcreatefundedpsbt([{'txid': utxo1['txid'], 'vout': utxo1['vout']}], {target_address: 1}, 0, {'add_inputs': True, 'minconf': 2, 'fee_rate': 10})['psbt'] - tx2_inputs = self.nodes[0].decodepsbt(psbtx2)['tx']['vin'] + 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)['inputs'] assert_greater_than_or_equal(len(tx2_inputs), 2) for vin in tx2_inputs: - if vin['txid'] != unconfirmed_txid: - assert_greater_than_or_equal(self.nodes[0].gettxout(vin['txid'], vin['vout'])['confirmations'], 2) + if vin['previous_txid'] != unconfirmed_txid: + assert_greater_than_or_equal(self.nodes[0].gettxout(vin['previous_txid'], vin['previous_vout'])['confirmations'], 2) signed_tx2 = wallet.walletprocesspsbt(psbtx2) txid2 = self.nodes[0].sendrawtransaction(signed_tx2['hex']) @@ -387,7 +386,7 @@ class PSBTTest(BitcoinTestFramework): # 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"]) 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 test_psbt_named_parameter_handling(self): """Test that PSBT Base64 parameters with '=' padding are handled correctly in -named mode""" @@ -429,17 +428,45 @@ class PSBTTest(BitcoinTestFramework): def test_psbt_roundtrip(self): self.log.info("Test that PSBTs roundtrip when RPC does nothing") utxo = self.nodes[0].listunspent()[0] - psbt = self.nodes[0].walletcreatefundedpsbt(inputs=[utxo], outputs=[{self.nodes[0].getnewaddress(): utxo["amount"] / 2}])["psbt"] + for ver in [0, 2]: + psbt = self.nodes[0].walletcreatefundedpsbt(inputs=[utxo], outputs=[{self.nodes[0].getnewaddress(): utxo["amount"] / 2}], psbt_version=ver)["psbt"] - rt_psbts = [ - self.nodes[0].combinepsbt([psbt, psbt]), - self.nodes[0].finalizepsbt(psbt)["psbt"], - self.nodes[0].utxoupdatepsbt(psbt), - self.nodes[0].descriptorprocesspsbt(psbt, [])["psbt"], - self.nodes[0].walletprocesspsbt(psbt, sign=False)["psbt"], - ] - for p in rt_psbts: - assert_equal(psbt, p) + rt_psbts = [ + self.nodes[0].combinepsbt([psbt, psbt]), + self.nodes[0].finalizepsbt(psbt)["psbt"], + self.nodes[0].utxoupdatepsbt(psbt), + self.nodes[0].descriptorprocesspsbt(psbt, [])["psbt"], + self.nodes[0].walletprocesspsbt(psbt, sign=False)["psbt"], + ] + for p in rt_psbts: + assert_equal(psbt, p) + + def test_psbt_version(self): + tobump = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1) + utxo = self.nodes[0].listunspent()[0] + outputs = [{self.nodes[0].getnewaddress(): utxo["amount"] / 2}] + rawtx = self.nodes[0].createrawtransaction(inputs=[utxo], outputs=outputs) + for ver in [0, 2]: + psbt = self.nodes[0].createpsbt(inputs=[utxo], outputs=outputs, psbt_version=ver) + dec = self.nodes[0].decodepsbt(psbt) + assert_equal(ver, dec["psbt_version"]) + + psbt = self.nodes[0].walletcreatefundedpsbt(inputs=[utxo], outputs=outputs, psbt_version=ver) + dec = self.nodes[0].decodepsbt(psbt["psbt"]) + assert_equal(ver, dec["psbt_version"]) + + psbt = self.nodes[0].converttopsbt(hexstring=rawtx, psbt_version=ver) + dec = self.nodes[0].decodepsbt(psbt) + assert_equal(ver, dec["psbt_version"]) + + psbt = self.nodes[0].psbtbumpfee(txid=tobump, psbt_version=ver) + dec = self.nodes[0].decodepsbt(psbt["psbt"]) + assert_equal(ver, dec["psbt_version"]) + + assert_raises_rpc_error(-8, "The PSBT version can only be 2 or 0", self.nodes[0].createpsbt, inputs=[utxo], outputs=outputs, psbt_version=1) + assert_raises_rpc_error(-8, "The PSBT version can only be 2 or 0", self.nodes[0].walletcreatefundedpsbt, inputs=[utxo], outputs=outputs, psbt_version=1) + assert_raises_rpc_error(-8, "The PSBT version can only be 2 or 0", self.nodes[0].converttopsbt, hexstring=rawtx, psbt_version=1) + assert_raises_rpc_error(-8, "The PSBT version can only be 2 or 0", self.nodes[0].psbtbumpfee, txid=tobump, psbt_version=1) def run_test(self): # Create and fund a raw tx for sending 10 BTC @@ -481,7 +508,9 @@ class PSBTTest(BitcoinTestFramework): 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"] - 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. assert_greater_than_or_equal(max_tx_weight_sufficient, weight) @@ -492,7 +521,7 @@ class PSBTTest(BitcoinTestFramework): 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'] - 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 self.nodes[0].walletcreatefundedpsbt(None, {self.nodes[2].getnewaddress():10}) @@ -735,12 +764,14 @@ class PSBTTest(BitcoinTestFramework): # Update psbts, should only have data for one input and not the other psbt1 = self.nodes[1].walletprocesspsbt(psbt_orig, False, "ALL")['psbt'] 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 assert "bip32_derivs" in psbt1_decoded['inputs'][0] psbt2 = self.nodes[2].walletprocesspsbt(psbt_orig, False, "ALL", False)['psbt'] 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 assert "bip32_derivs" not in psbt2_decoded['inputs'][1] @@ -762,33 +793,33 @@ class PSBTTest(BitcoinTestFramework): 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) decoded_psbt = self.nodes[0].decodepsbt(psbtx_info["psbt"]) - for tx_in, psbt_in in zip(decoded_psbt["tx"]["vin"], decoded_psbt["inputs"]): - assert_greater_than(tx_in["sequence"], MAX_BIP125_RBF_SEQUENCE) + for psbt_in in decoded_psbt["inputs"]: + assert_greater_than(psbt_in["sequence"], MAX_BIP125_RBF_SEQUENCE) 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 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"]) - for tx_in, psbt_in in zip(decoded_psbt["tx"]["vin"], decoded_psbt["inputs"]): - assert_equal(tx_in["sequence"], MAX_BIP125_RBF_SEQUENCE) + for psbt_in in decoded_psbt["inputs"]: + assert_equal(psbt_in["sequence"], MAX_BIP125_RBF_SEQUENCE) 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 psbtx_info = self.nodes[0].walletcreatefundedpsbt([], [{self.nodes[2].getnewaddress():unspent["amount"]+1}]) decoded_psbt = self.nodes[0].decodepsbt(psbtx_info["psbt"]) - for tx_in, psbt_in in zip(decoded_psbt["tx"]["vin"], decoded_psbt["inputs"]): - assert_equal(tx_in["sequence"], MAX_BIP125_RBF_SEQUENCE) + for psbt_in in decoded_psbt["inputs"]: + assert_equal(psbt_in["sequence"], MAX_BIP125_RBF_SEQUENCE) 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 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}) decoded_psbt = self.nodes[1].decodepsbt(psbtx_info["psbt"]) - for tx_in, psbt_in in zip(decoded_psbt["tx"]["vin"], decoded_psbt["inputs"]): - assert_greater_than(tx_in["sequence"], MAX_BIP125_RBF_SEQUENCE) + for psbt_in in decoded_psbt["inputs"]: + assert_greater_than(psbt_in["sequence"], MAX_BIP125_RBF_SEQUENCE) assert "bip32_derivs" in psbt_in # Make sure change address wallet does not have P2SH innerscript access to results in success @@ -869,7 +900,7 @@ class PSBTTest(BitcoinTestFramework): # Creator Tests 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']) # Signer tests @@ -923,46 +954,55 @@ class PSBTTest(BitcoinTestFramework): utxo1, utxo2, utxo3 = self.create_outpoints(self.nodes[1], outputs=[{addr1: 11}, {addr2: 11}, {addr3: 11}]) self.sync_all() + psbt_v2_required_keys = ["previous_txid", "previous_vout", "sequence"] + def test_psbt_input_keys(psbt_input, keys): """Check that the psbt input has only the expected keys.""" + keys.extend(["previous_txid", "previous_vout", "sequence"]) assert_equal(set(keys), set(psbt_input.keys())) # Create a PSBT. None of the inputs are filled initially psbt = self.nodes[1].createpsbt([utxo1, utxo2, utxo3], {self.nodes[0].getnewaddress():32.999}) decoded = self.nodes[1].decodepsbt(psbt) - test_psbt_input_keys(decoded['inputs'][0], []) - test_psbt_input_keys(decoded['inputs'][1], []) - test_psbt_input_keys(decoded['inputs'][2], []) + test_psbt_input_keys(decoded['inputs'][0], psbt_v2_required_keys) + test_psbt_input_keys(decoded['inputs'][1], psbt_v2_required_keys) + test_psbt_input_keys(decoded['inputs'][2], psbt_v2_required_keys) # 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 updated = self.nodes[1].utxoupdatepsbt(psbt) decoded = self.nodes[1].decodepsbt(updated) - test_psbt_input_keys(decoded['inputs'][0], ['witness_utxo', 'non_witness_utxo']) - test_psbt_input_keys(decoded['inputs'][1], ['non_witness_utxo']) - test_psbt_input_keys(decoded['inputs'][2], ['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], psbt_v2_required_keys + ['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 descs = [self.nodes[1].getaddressinfo(addr)['desc'] for addr in [addr1,addr2,addr3]] updated = self.nodes[1].utxoupdatepsbt(psbt=psbt, descriptors=descs) 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'][1], ['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'][0], psbt_v2_required_keys + ['witness_utxo', '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], 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 - psbt1 = self.nodes[1].createpsbt([utxo1], {self.nodes[0].getnewaddress():Decimal('10.999')}) - assert_raises_rpc_error(-8, "exists in multiple PSBTs", self.nodes[1].joinpsbts, [psbt1, updated]) + 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, psbt2]) # 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") utxo4 = self.create_outpoints(self.nodes[0], outputs=[{addr4: 5}])[0] 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_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 = self.nodes[0].joinpsbts([psbt1, psbt2]) joined_decoded = self.nodes[0].decodepsbt(joined) assert_equal(len(joined_decoded['inputs']), 4) assert_equal(len(joined_decoded['outputs']), 2) @@ -973,7 +1013,7 @@ class PSBTTest(BitcoinTestFramework): # 10 attempts should be enough to get a shuffled join shuffled = False for _ in range(10): - shuffled_joined = self.nodes[0].joinpsbts([psbt, psbt2]) + shuffled_joined = self.nodes[0].joinpsbts([psbt1, psbt2]) shuffled |= joined != shuffled_joined if shuffled: break @@ -1071,11 +1111,9 @@ class PSBTTest(BitcoinTestFramework): final = signed['hex'] dec = self.nodes[0].decodepsbt(signed["psbt"]) - for i, txin in enumerate(dec["tx"]["vin"]): - if txin["txid"] == ext_utxo["txid"] and txin["vout"] == ext_utxo["vout"]: - input_idx = i + for psbt_in in dec["inputs"]: + if psbt_in["previous_txid"] == ext_utxo["txid"] and psbt_in["previous_vout"] == ext_utxo["vout"]: break - psbt_in = dec["inputs"][input_idx] 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 input_weight = calculate_input_weight(scriptsig_hex, witness_stack_hex) @@ -1195,7 +1233,7 @@ class PSBTTest(BitcoinTestFramework): assert_equal(comb_psbt, psbt) self.log.info("Test walletprocesspsbt raises if an invalid sighashtype is passed") - assert_raises_rpc_error(-8, "'all' is not a valid sighash parameter.", self.nodes[0].walletprocesspsbt, psbt, sighashtype="all") + assert_raises_rpc_error(-8, "'all' is not a valid sighash parameter.", self.nodes[0].walletprocesspsbt, psbt=psbt, sighashtype="all") self.log.info("Test decoding PSBT with per-input preimage types") # note that the decodepsbt RPC doesn't check whether preimages and hashes match @@ -1303,13 +1341,14 @@ class PSBTTest(BitcoinTestFramework): self.nodes[2].sendrawtransaction(processed_psbt['hex']) self.log.info("Test descriptorprocesspsbt raises if an invalid sighashtype is passed") - assert_raises_rpc_error(-8, "'all' is not a valid sighash parameter.", self.nodes[2].descriptorprocesspsbt, psbt, [descriptor], sighashtype="all") + assert_raises_rpc_error(-8, "'all' is not a valid sighash parameter.", self.nodes[2].descriptorprocesspsbt, psbt=psbt, descriptors=[descriptor], sighashtype="all") if not self.options.usecli: self.test_sighash_mismatch() self.test_sighash_adding() self.test_psbt_named_parameter_handling() self.test_psbt_roundtrip() + self.test_psbt_version() if __name__ == '__main__': PSBTTest(__file__).main() diff --git a/test/functional/wallet_bumpfee.py b/test/functional/wallet_bumpfee.py index 80735e01dd1..961f1a1494f 100755 --- a/test/functional/wallet_bumpfee.py +++ b/test/functional/wallet_bumpfee.py @@ -608,14 +608,14 @@ def test_watchonly_psbt(self, peer_node, rbf_node, dest_address): {"fee_rate": 1, "add_inputs": False}, True)['psbt'] psbt_signed = signer.walletprocesspsbt(psbt=psbt, sign=True, sighashtype="ALL", bip32derivs=True) 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 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 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_equal(bumped_psbt["origfee"], -watcher.gettransaction(original_txid)["fee"]) assert not watcher.finalizepsbt(bumped_psbt["psbt"])["complete"] diff --git a/test/functional/wallet_fundrawtransaction.py b/test/functional/wallet_fundrawtransaction.py index a00e7e682dd..022270293cf 100755 --- a/test/functional/wallet_fundrawtransaction.py +++ b/test/functional/wallet_fundrawtransaction.py @@ -1176,10 +1176,10 @@ class RawTransactionsTest(BitcoinTestFramework): tx = wallet.send(outputs=[{addr1: 8}], **options) assert tx["complete"] # 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) 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 options = {"add_inputs": True, "add_to_wallet": True} @@ -1218,10 +1218,10 @@ class RawTransactionsTest(BitcoinTestFramework): }) psbt_tx = wallet.walletcreatefundedpsbt(outputs=[{addr1: 8}], inputs=inputs, **options) # 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) 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 # Explicit add_inputs=true, no preset inputs diff --git a/test/functional/wallet_multisig_descriptor_psbt.py b/test/functional/wallet_multisig_descriptor_psbt.py index a3236a2249a..85051c34ab8 100755 --- a/test/functional/wallet_multisig_descriptor_psbt.py +++ b/test/functional/wallet_multisig_descriptor_psbt.py @@ -36,13 +36,13 @@ class WalletMultisigDescriptorPSBTTest(BitcoinTestFramework): @staticmethod 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.""" - tx = multisig.decodepsbt(psbt)["tx"] + decoded = multisig.decodepsbt(psbt) amount = 0 - for vout in tx["vout"]: - address = vout["scriptPubKey"]["address"] + for psbt_out in decoded["outputs"]: + address = psbt_out["script"]["address"] assert_equal(multisig.getaddressinfo(address)["ischange"], address != to) if address == to: - amount += vout["value"] + amount += psbt_out["amount"] assert_approx(amount, float(value), vspan=0.001) def participants_create_multisigs(self, xpubs): diff --git a/test/functional/wallet_send.py b/test/functional/wallet_send.py index 213c475634d..ba98699039d 100755 --- a/test/functional/wallet_send.py +++ b/test/functional/wallet_send.py @@ -395,10 +395,10 @@ class WalletSendTest(BitcoinTestFramework): 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) 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) 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] in ("m", "n") self.log.info("Set lock time...") @@ -498,11 +498,9 @@ class WalletSendTest(BitcoinTestFramework): assert signed["complete"] dec = self.nodes[0].decodepsbt(signed["psbt"]) - for i, txin in enumerate(dec["tx"]["vin"]): - if txin["txid"] == ext_utxo["txid"] and txin["vout"] == ext_utxo["vout"]: - input_idx = i + for psbt_in in dec["inputs"]: + if psbt_in["previous_txid"] == ext_utxo["txid"] and psbt_in["previous_vout"] == ext_utxo["vout"]: break - psbt_in = dec["inputs"][input_idx] 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 input_weight = calculate_input_weight(scriptsig_hex, witness_stack_hex) diff --git a/test/functional/wallet_sendall.py b/test/functional/wallet_sendall.py index 1977a1e227b..0524b5c82ed 100755 --- a/test/functional/wallet_sendall.py +++ b/test/functional/wallet_sendall.py @@ -308,9 +308,9 @@ class SendallTest(BitcoinTestFramework): decoded = self.nodes[0].decodepsbt(psbt) assert_equal(len(decoded["inputs"]), 1) assert_equal(len(decoded["outputs"]), 1) - assert_equal(decoded["tx"]["vin"][0]["txid"], utxo["txid"]) - assert_equal(decoded["tx"]["vin"][0]["vout"], utxo["vout"]) - assert_equal(decoded["tx"]["vout"][0]["scriptPubKey"]["address"], self.remainder_target) + assert_equal(decoded["inputs"][0]["previous_txid"], utxo["txid"]) + assert_equal(decoded["inputs"][0]["previous_vout"], utxo["vout"]) + assert_equal(decoded["outputs"][0]["script"]["address"], self.remainder_target) @cleanup def sendall_with_minconf(self): diff --git a/test/functional/wallet_v3_txs.py b/test/functional/wallet_v3_txs.py index 446dc086a3c..18fa0d546b9 100755 --- a/test/functional/wallet_v3_txs.py +++ b/test/functional/wallet_v3_txs.py @@ -448,7 +448,7 @@ class WalletV3Test(BitcoinTestFramework): outputs = {self.alice.getnewaddress() : 10} psbt = self.charlie.createpsbt(inputs=[], outputs=outputs, version=3) - assert_equal(self.charlie.decodepsbt(psbt)["tx"]["version"], 3) + assert_equal(self.charlie.decodepsbt(psbt)["tx_version"], 3) @cleanup def send_v3(self): @@ -526,7 +526,7 @@ class WalletV3Test(BitcoinTestFramework): outputs = {self.alice.getnewaddress() : 10} psbt = self.charlie.walletcreatefundedpsbt(inputs=[], outputs=outputs, version=3)["psbt"] - assert_equal(self.charlie.decodepsbt(psbt)["tx"]["version"], 3) + assert_equal(self.charlie.decodepsbt(psbt)["tx_version"], 3) @cleanup def sendall_truc_weight_limit(self):