diff options
author | Andrew Chow <github@achow101.com> | 2023-09-12 12:18:52 -0400 |
---|---|---|
committer | Andrew Chow <github@achow101.com> | 2023-09-12 12:28:13 -0400 |
commit | 8f9c74cb11d2016c84eea037533c1a131745fdc8 (patch) | |
tree | cedf7acfca007d83e88b34a9d9cb6008eaebaa1e | |
parent | 7649431637441d0e236f33d67eb196bfc33a13a7 (diff) | |
parent | 2e249b922762f19d6ae61edaad062f31bc2849f3 (diff) |
Merge bitcoin/bitcoin#28414: wallet rpc: return final tx hex from walletprocesspsbt if complete
2e249b922762f19d6ae61edaad062f31bc2849f3 doc: add release note for PR #28414 (Matthew Zipkin)
4614332fc4514f63fcbe9e6de507f7bb9b7e87e9 test: remove unnecessary finalizepsbt rpc calls (ismaelsadeeq)
e3d484b603abff69c6ebfca5cfb78cf82743d090 wallet rpc: return final tx hex from walletprocesspsbt if complete (Matthew Zipkin)
Pull request description:
See https://github.com/bitcoin/bitcoin/pull/28363#discussion_r1315753887
`walletprocesspsbt` currently returns a base64-encoded PSBT and a boolean indicating if the tx is "complete". If it is complete, the base64 PSBT can be finalized with `finalizepsbt` which returns the hex-encoded transaction suitable for `sendrawtransaction`.
With this patch, `walletprocesspsbt` return object will ALSO include the broadcast-able hex string if the tx is already final. This saves users the extra step of calling `finalizepsbt` assuming they have already inspected and approve the transaction from earlier steps.
ACKs for top commit:
ismaelsadeeq:
re ACK 2e249b922762f19d6ae61edaad062f31bc2849f3
BrandonOdiwuor:
re ACK 2e249b9
Randy808:
Tested ACK 2e249b922762f19d6ae61edaad062f31bc2849f3
achow101:
ACK 2e249b922762f19d6ae61edaad062f31bc2849f3
ishaanam:
ACK 2e249b922762f19d6ae61edaad062f31bc2849f3
Tree-SHA512: 229c1103265a9b4248f080935a7ad5607c3be3f9a096a9ab6554093b2cd8aa8b4d1fa55b1b97d3925ba208dbc3ccba4e4d37c40e1491db0d27ba3d9fe98f931e
-rw-r--r-- | doc/release-notes-28414.md | 5 | ||||
-rw-r--r-- | src/wallet/rpc/spend.cpp | 9 | ||||
-rwxr-xr-x | test/functional/rpc_psbt.py | 63 | ||||
-rwxr-xr-x | test/functional/wallet_bumpfee.py | 11 | ||||
-rwxr-xr-x | test/functional/wallet_fundrawtransaction.py | 3 | ||||
-rwxr-xr-x | test/functional/wallet_multisig_descriptor_psbt.py | 3 | ||||
-rwxr-xr-x | test/functional/wallet_send.py | 5 | ||||
-rwxr-xr-x | test/functional/wallet_signer.py | 3 |
8 files changed, 56 insertions, 46 deletions
diff --git a/doc/release-notes-28414.md b/doc/release-notes-28414.md new file mode 100644 index 0000000000..7fca11f822 --- /dev/null +++ b/doc/release-notes-28414.md @@ -0,0 +1,5 @@ +RPC Wallet +---------- + +- RPC `walletprocesspsbt` return object now includes field `hex` (if the transaction +is complete) containing the serialized transaction suitable for RPC `sendrawtransaction`. (#28414) diff --git a/src/wallet/rpc/spend.cpp b/src/wallet/rpc/spend.cpp index 0c2be26ddf..c4206e9897 100644 --- a/src/wallet/rpc/spend.cpp +++ b/src/wallet/rpc/spend.cpp @@ -1566,6 +1566,7 @@ RPCHelpMan walletprocesspsbt() { {RPCResult::Type::STR, "psbt", "The base64-encoded partially signed transaction"}, {RPCResult::Type::BOOL, "complete", "If the transaction has a complete set of signatures"}, + {RPCResult::Type::STR_HEX, "hex", /*optional=*/true, "The hex-encoded network transaction if complete"}, } }, RPCExamples{ @@ -1609,6 +1610,14 @@ RPCHelpMan walletprocesspsbt() ssTx << psbtx; result.pushKV("psbt", EncodeBase64(ssTx.str())); result.pushKV("complete", complete); + if (complete) { + CMutableTransaction mtx; + // Returns true if complete, which we already think it is. + CHECK_NONFATAL(FinalizeAndExtractPSBT(psbtx, mtx)); + CDataStream ssTx_final(SER_NETWORK, PROTOCOL_VERSION); + ssTx_final << mtx; + result.pushKV("hex", HexStr(ssTx_final)); + } return result; }, diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py index b574a370d6..c01dc64df0 100755 --- a/test/functional/rpc_psbt.py +++ b/test/functional/rpc_psbt.py @@ -106,11 +106,11 @@ class PSBTTest(BitcoinTestFramework): assert "non_witness_utxo" in mining_node.decodepsbt(psbt_new.to_base64())["inputs"][0] # Have the offline node sign the PSBT (which will remove the non-witness UTXO) - signed_psbt = offline_node.walletprocesspsbt(psbt_new.to_base64())["psbt"] - assert not "non_witness_utxo" in mining_node.decodepsbt(signed_psbt)["inputs"][0] + signed_psbt = offline_node.walletprocesspsbt(psbt_new.to_base64()) + assert not "non_witness_utxo" in mining_node.decodepsbt(signed_psbt["psbt"])["inputs"][0] # Make sure we can mine the resulting transaction - txid = mining_node.sendrawtransaction(mining_node.finalizepsbt(signed_psbt)["hex"]) + txid = mining_node.sendrawtransaction(signed_psbt["hex"]) self.generate(mining_node, nblocks=1, sync_fun=lambda: self.sync_all([online_node, mining_node])) assert_equal(online_node.gettxout(txid,0)["confirmations"], 1) @@ -142,9 +142,8 @@ class PSBTTest(BitcoinTestFramework): utxo1 = tx1_inputs[0] assert_equal(unconfirmed_txid, utxo1['txid']) - signed_tx1 = wallet.walletprocesspsbt(psbtx1)['psbt'] - final_tx1 = wallet.finalizepsbt(signed_tx1)['hex'] - txid1 = self.nodes[0].sendrawtransaction(final_tx1) + signed_tx1 = wallet.walletprocesspsbt(psbtx1) + txid1 = self.nodes[0].sendrawtransaction(signed_tx1['hex']) mempool = self.nodes[0].getrawmempool() assert txid1 in mempool @@ -157,9 +156,8 @@ class PSBTTest(BitcoinTestFramework): 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'] - signed_invalid = wallet.walletprocesspsbt(psbt_invalid)['psbt'] - final_invalid = wallet.finalizepsbt(signed_invalid)['hex'] - assert_raises_rpc_error(-26, "bad-txns-spends-conflicting-tx", self.nodes[0].sendrawtransaction, final_invalid) + 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'] @@ -169,9 +167,8 @@ class PSBTTest(BitcoinTestFramework): if vin['txid'] != unconfirmed_txid: assert_greater_than_or_equal(self.nodes[0].gettxout(vin['txid'], vin['vout'])['confirmations'], 2) - signed_tx2 = wallet.walletprocesspsbt(psbtx2)['psbt'] - final_tx2 = wallet.finalizepsbt(signed_tx2)['hex'] - txid2 = self.nodes[0].sendrawtransaction(final_tx2) + signed_tx2 = wallet.walletprocesspsbt(psbtx2) + txid2 = self.nodes[0].sendrawtransaction(signed_tx2['hex']) mempool = self.nodes[0].getrawmempool() assert txid1 not in mempool @@ -217,12 +214,21 @@ class PSBTTest(BitcoinTestFramework): self.nodes[0].walletpassphrase(passphrase="password", timeout=1000000) - # Sign the transaction and send - signed_tx = self.nodes[0].walletprocesspsbt(psbt=psbtx, finalize=False)['psbt'] - finalized_tx = self.nodes[0].walletprocesspsbt(psbt=psbtx, finalize=True)['psbt'] - assert signed_tx != finalized_tx - final_tx = self.nodes[0].finalizepsbt(signed_tx)['hex'] - self.nodes[0].sendrawtransaction(final_tx) + # Sign the transaction but don't finalize + processed_psbt = self.nodes[0].walletprocesspsbt(psbt=psbtx, finalize=False) + assert "hex" not in processed_psbt + signed_psbt = processed_psbt['psbt'] + + # Finalize and send + finalized_hex = self.nodes[0].finalizepsbt(signed_psbt)['hex'] + self.nodes[0].sendrawtransaction(finalized_hex) + + # Alternative method: sign AND finalize in one command + processed_finalized_psbt = self.nodes[0].walletprocesspsbt(psbt=psbtx, finalize=True) + finalized_psbt = processed_finalized_psbt['psbt'] + finalized_psbt_hex = processed_finalized_psbt['hex'] + assert signed_psbt != finalized_psbt + assert finalized_psbt_hex == finalized_hex # Manually selected inputs can be locked: assert_equal(len(self.nodes[0].listlockunspent()), 0) @@ -296,7 +302,7 @@ class PSBTTest(BitcoinTestFramework): # Check decodepsbt fee calculation (input values shall only be counted once per UTXO) assert_equal(decoded['fee'], created_psbt['fee']) assert_equal(walletprocesspsbt_out['complete'], True) - self.nodes[1].sendrawtransaction(self.nodes[1].finalizepsbt(walletprocesspsbt_out['psbt'])['hex']) + self.nodes[1].sendrawtransaction(walletprocesspsbt_out['hex']) self.log.info("Test walletcreatefundedpsbt fee rate of 10000 sat/vB and 0.1 BTC/kvB produces a total fee at or slightly below -maxtxfee (~0.05290000)") res1 = self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"fee_rate": 10000, "add_inputs": True}) @@ -387,7 +393,7 @@ class PSBTTest(BitcoinTestFramework): # partially sign with node 2. This should be complete and sendable walletprocesspsbt_out = self.nodes[2].walletprocesspsbt(psbtx) assert_equal(walletprocesspsbt_out['complete'], True) - self.nodes[2].sendrawtransaction(self.nodes[2].finalizepsbt(walletprocesspsbt_out['psbt'])['hex']) + self.nodes[2].sendrawtransaction(walletprocesspsbt_out['hex']) # check that walletprocesspsbt fails to decode a non-psbt rawtx = self.nodes[1].createrawtransaction([{"txid":txid,"vout":p2wpkh_pos}], {self.nodes[1].getnewaddress():9.99}) @@ -739,14 +745,13 @@ class PSBTTest(BitcoinTestFramework): assert not signed['complete'] signed = self.nodes[0].walletprocesspsbt(signed['psbt']) assert signed['complete'] - self.nodes[0].finalizepsbt(signed['psbt']) psbt = wallet.walletcreatefundedpsbt([ext_utxo], {self.nodes[0].getnewaddress(): 15}, 0, {"add_inputs": True, "solving_data":{"descriptors": [desc]}}) signed = wallet.walletprocesspsbt(psbt['psbt']) assert not signed['complete'] signed = self.nodes[0].walletprocesspsbt(signed['psbt']) assert signed['complete'] - final = self.nodes[0].finalizepsbt(signed['psbt'], False) + final = signed['hex'] dec = self.nodes[0].decodepsbt(signed["psbt"]) for i, txin in enumerate(dec["tx"]["vin"]): @@ -781,8 +786,8 @@ class PSBTTest(BitcoinTestFramework): ) signed = wallet.walletprocesspsbt(psbt["psbt"]) signed = self.nodes[0].walletprocesspsbt(signed["psbt"]) - final = self.nodes[0].finalizepsbt(signed["psbt"]) - assert self.nodes[0].testmempoolaccept([final["hex"]])[0]["allowed"] + final = signed["hex"] + assert self.nodes[0].testmempoolaccept([final])[0]["allowed"] # Reducing the weight should have a lower fee psbt2 = wallet.walletcreatefundedpsbt( inputs=[{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": low_input_weight}], @@ -837,8 +842,8 @@ class PSBTTest(BitcoinTestFramework): self.nodes[0].importprivkey(privkey) psbt = watchonly.sendall([wallet.getnewaddress()])["psbt"] - psbt = self.nodes[0].walletprocesspsbt(psbt)["psbt"] - self.nodes[0].sendrawtransaction(self.nodes[0].finalizepsbt(psbt)["hex"]) + signed_tx = self.nodes[0].walletprocesspsbt(psbt) + self.nodes[0].sendrawtransaction(signed_tx["hex"]) # Same test but for taproot if self.options.descriptors: @@ -853,8 +858,8 @@ class PSBTTest(BitcoinTestFramework): self.nodes[0].importdescriptors([{"desc": descsum_create("tr({})".format(privkey)), "timestamp":"now"}]) psbt = watchonly.sendall([wallet.getnewaddress(), addr])["psbt"] - psbt = self.nodes[0].walletprocesspsbt(psbt)["psbt"] - txid = self.nodes[0].sendrawtransaction(self.nodes[0].finalizepsbt(psbt)["hex"]) + processed_psbt = self.nodes[0].walletprocesspsbt(psbt) + txid = self.nodes[0].sendrawtransaction(processed_psbt["hex"]) vout = find_vout_for_address(self.nodes[0], txid, addr) # Make sure tap tree is in psbt @@ -871,7 +876,7 @@ class PSBTTest(BitcoinTestFramework): vout = find_vout_for_address(self.nodes[0], txid, addr) psbt = self.nodes[0].createpsbt([{"txid": txid, "vout": vout}], [{self.nodes[0].getnewaddress(): 0.9999}]) signed = self.nodes[0].walletprocesspsbt(psbt) - rawtx = self.nodes[0].finalizepsbt(signed["psbt"])["hex"] + rawtx = signed["hex"] self.nodes[0].sendrawtransaction(rawtx) self.generate(self.nodes[0], 1) diff --git a/test/functional/wallet_bumpfee.py b/test/functional/wallet_bumpfee.py index e69c1829ca..c66f279e88 100755 --- a/test/functional/wallet_bumpfee.py +++ b/test/functional/wallet_bumpfee.py @@ -403,8 +403,7 @@ def test_notmine_bumpfee(self, rbf_node, peer_node, dest_address): def finish_psbtbumpfee(psbt): psbt = rbf_node.walletprocesspsbt(psbt) psbt = peer_node.walletprocesspsbt(psbt["psbt"]) - final = rbf_node.finalizepsbt(psbt["psbt"]) - res = rbf_node.testmempoolaccept([final["hex"]]) + res = rbf_node.testmempoolaccept([psbt["hex"]]) assert res[0]["allowed"] assert_greater_than(res[0]["fees"]["base"], old_fee) @@ -638,8 +637,7 @@ def test_watchonly_psbt(self, peer_node, rbf_node, dest_address): psbt = watcher.walletcreatefundedpsbt([watcher.listunspent()[0]], {dest_address: 0.0005}, 0, {"fee_rate": 1, "add_inputs": False}, True)['psbt'] psbt_signed = signer.walletprocesspsbt(psbt=psbt, sign=True, sighashtype="ALL", bip32derivs=True) - psbt_final = watcher.finalizepsbt(psbt_signed["psbt"]) - original_txid = watcher.sendrawtransaction(psbt_final["hex"]) + original_txid = watcher.sendrawtransaction(psbt_signed["hex"]) assert_equal(len(watcher.decodepsbt(psbt)["tx"]["vin"]), 1) # bumpfee can't be used on watchonly wallets @@ -654,11 +652,10 @@ def test_watchonly_psbt(self, peer_node, rbf_node, dest_address): # Sign bumped transaction bumped_psbt_signed = signer.walletprocesspsbt(psbt=bumped_psbt["psbt"], sign=True, sighashtype="ALL", bip32derivs=True) - bumped_psbt_final = watcher.finalizepsbt(bumped_psbt_signed["psbt"]) - assert bumped_psbt_final["complete"] + assert bumped_psbt_signed["complete"] # Broadcast bumped transaction - bumped_txid = watcher.sendrawtransaction(bumped_psbt_final["hex"]) + bumped_txid = watcher.sendrawtransaction(bumped_psbt_signed["hex"]) assert bumped_txid in rbf_node.getrawmempool() assert original_txid not in rbf_node.getrawmempool() diff --git a/test/functional/wallet_fundrawtransaction.py b/test/functional/wallet_fundrawtransaction.py index fa4f009f34..b1829f42af 100755 --- a/test/functional/wallet_fundrawtransaction.py +++ b/test/functional/wallet_fundrawtransaction.py @@ -556,8 +556,7 @@ class RawTransactionsTest(BitcoinTestFramework): funded_psbt = wmulti.walletcreatefundedpsbt(inputs=inputs, outputs=outputs, changeAddress=w2.getrawchangeaddress())['psbt'] signed_psbt = w2.walletprocesspsbt(funded_psbt) - final_psbt = w2.finalizepsbt(signed_psbt['psbt']) - self.nodes[2].sendrawtransaction(final_psbt['hex']) + self.nodes[2].sendrawtransaction(signed_psbt['hex']) self.generate(self.nodes[2], 1) # Make sure funds are received at node1. diff --git a/test/functional/wallet_multisig_descriptor_psbt.py b/test/functional/wallet_multisig_descriptor_psbt.py index 28bee1911e..68bf45f7e3 100755 --- a/test/functional/wallet_multisig_descriptor_psbt.py +++ b/test/functional/wallet_multisig_descriptor_psbt.py @@ -150,8 +150,7 @@ class WalletMultisigDescriptorPSBTTest(BitcoinTestFramework): signing_wallet = participants["signers"][m] psbt = signing_wallet.walletprocesspsbt(psbt["psbt"]) assert_equal(psbt["complete"], m == self.M - 1) - finalized = coordinator_wallet.finalizepsbt(psbt["psbt"]) - coordinator_wallet.sendrawtransaction(finalized["hex"]) + coordinator_wallet.sendrawtransaction(psbt["hex"]) self.log.info("Check that balances are correct after the transaction has been included in a block.") self.generate(self.nodes[0], 1) diff --git a/test/functional/wallet_send.py b/test/functional/wallet_send.py index 4728f53be7..6ce2a56bfc 100755 --- a/test/functional/wallet_send.py +++ b/test/functional/wallet_send.py @@ -530,13 +530,11 @@ class WalletSendTest(BitcoinTestFramework): signed = ext_wallet.walletprocesspsbt(res["psbt"]) signed = ext_fund.walletprocesspsbt(res["psbt"]) assert signed["complete"] - self.nodes[0].finalizepsbt(signed["psbt"]) res = self.test_send(from_wallet=ext_wallet, to_wallet=self.nodes[0], amount=15, inputs=[ext_utxo], add_inputs=True, psbt=True, include_watching=True, solving_data={"descriptors": [desc]}) signed = ext_wallet.walletprocesspsbt(res["psbt"]) signed = ext_fund.walletprocesspsbt(res["psbt"]) assert signed["complete"] - self.nodes[0].finalizepsbt(signed["psbt"]) dec = self.nodes[0].decodepsbt(signed["psbt"]) for i, txin in enumerate(dec["tx"]["vin"]): @@ -574,8 +572,7 @@ class WalletSendTest(BitcoinTestFramework): signed = ext_wallet.walletprocesspsbt(res["psbt"]) signed = ext_fund.walletprocesspsbt(res["psbt"]) assert signed["complete"] - tx = self.nodes[0].finalizepsbt(signed["psbt"]) - testres = self.nodes[0].testmempoolaccept([tx["hex"]])[0] + testres = self.nodes[0].testmempoolaccept([signed["hex"]])[0] assert_equal(testres["allowed"], True) assert_fee_amount(testres["fees"]["base"], testres["vsize"], Decimal(0.0001)) diff --git a/test/functional/wallet_signer.py b/test/functional/wallet_signer.py index 2735ec1706..32a1887153 100755 --- a/test/functional/wallet_signer.py +++ b/test/functional/wallet_signer.py @@ -169,8 +169,7 @@ class WalletSignerTest(BitcoinTestFramework): dest = self.nodes[0].getnewaddress(address_type='bech32') mock_psbt = mock_wallet.walletcreatefundedpsbt([], {dest:0.5}, 0, {'replaceable': True}, True)['psbt'] mock_psbt_signed = mock_wallet.walletprocesspsbt(psbt=mock_psbt, sign=True, sighashtype="ALL", bip32derivs=True) - mock_psbt_final = mock_wallet.finalizepsbt(mock_psbt_signed["psbt"]) - mock_tx = mock_psbt_final["hex"] + mock_tx = mock_psbt_signed["hex"] assert mock_wallet.testmempoolaccept([mock_tx])[0]["allowed"] # # Create a new wallet and populate with specific public keys, in order |