From 11d6459b6e101f05f36e13799c400bef82d2fc21 Mon Sep 17 00:00:00 2001 From: t-bast Date: Wed, 10 Mar 2021 15:37:18 +0100 Subject: rpc: include_unsafe option for fundrawtransaction Allow RPC users to opt-in to unsafe inputs when funding a raw transaction. Applications that need to manage a complex RBF flow (such as lightning nodes using anchor outputs) are very limited if they can only use safe inputs. Fixes #21299 --- test/functional/rpc_fundrawtransaction.py | 35 +++++++++++++++++++++++++++++++ test/functional/rpc_psbt.py | 8 +++++++ test/functional/wallet_send.py | 27 +++++++++++++++++++----- 3 files changed, 65 insertions(+), 5 deletions(-) (limited to 'test/functional') diff --git a/test/functional/rpc_fundrawtransaction.py b/test/functional/rpc_fundrawtransaction.py index 5129ecb895..a9ebe5741e 100755 --- a/test/functional/rpc_fundrawtransaction.py +++ b/test/functional/rpc_fundrawtransaction.py @@ -96,6 +96,7 @@ class RawTransactionsTest(BitcoinTestFramework): self.test_option_subtract_fee_from_outputs() self.test_subtract_fee_with_presets() self.test_transaction_too_large() + self.test_include_unsafe() def test_change_position(self): """Ensure setting changePosition in fundraw with an exact match is handled properly.""" @@ -928,6 +929,40 @@ class RawTransactionsTest(BitcoinTestFramework): self.nodes[0].generate(10) assert_raises_rpc_error(-4, "Transaction too large", recipient.fundrawtransaction, rawtx) + def test_include_unsafe(self): + self.log.info("Test fundrawtxn with unsafe inputs") + + self.nodes[0].createwallet("unsafe") + wallet = self.nodes[0].get_wallet_rpc("unsafe") + + # We receive unconfirmed funds from external keys (unsafe outputs). + addr = wallet.getnewaddress() + txid1 = self.nodes[2].sendtoaddress(addr, 6) + txid2 = self.nodes[2].sendtoaddress(addr, 4) + self.sync_all() + vout1 = find_vout_for_address(wallet, txid1, addr) + vout2 = find_vout_for_address(wallet, txid2, addr) + + # Unsafe inputs are ignored by default. + rawtx = wallet.createrawtransaction([], [{self.nodes[2].getnewaddress(): 5}]) + assert_raises_rpc_error(-4, "Insufficient funds", wallet.fundrawtransaction, rawtx) + + # But we can opt-in to use them for funding. + fundedtx = wallet.fundrawtransaction(rawtx, {"include_unsafe": True}) + tx_dec = wallet.decoderawtransaction(fundedtx['hex']) + assert any([txin['txid'] == txid1 and txin['vout'] == vout1 for txin in tx_dec['vin']]) + signedtx = wallet.signrawtransactionwithwallet(fundedtx['hex']) + wallet.sendrawtransaction(signedtx['hex']) + + # And we can also use them once they're confirmed. + self.nodes[0].generate(1) + rawtx = wallet.createrawtransaction([], [{self.nodes[2].getnewaddress(): 3}]) + fundedtx = wallet.fundrawtransaction(rawtx, {"include_unsafe": True}) + tx_dec = wallet.decoderawtransaction(fundedtx['hex']) + assert any([txin['txid'] == txid2 and txin['vout'] == vout2 for txin in tx_dec['vin']]) + signedtx = wallet.signrawtransactionwithwallet(fundedtx['hex']) + wallet.sendrawtransaction(signedtx['hex']) + if __name__ == '__main__': RawTransactionsTest().main() diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py index 079a3bd3ba..e811628a5d 100755 --- a/test/functional/rpc_psbt.py +++ b/test/functional/rpc_psbt.py @@ -394,6 +394,14 @@ class PSBTTest(BitcoinTestFramework): # We don't care about the decode result, but decoding must succeed. self.nodes[0].decodepsbt(double_processed_psbt["psbt"]) + # Make sure unsafe inputs are included if specified + self.nodes[2].createwallet(wallet_name="unsafe") + wunsafe = self.nodes[2].get_wallet_rpc("unsafe") + self.nodes[0].sendtoaddress(wunsafe.getnewaddress(), 2) + self.sync_mempools() + assert_raises_rpc_error(-4, "Insufficient funds", wunsafe.walletcreatefundedpsbt, [], [{self.nodes[0].getnewaddress(): 1}]) + wunsafe.walletcreatefundedpsbt([], [{self.nodes[0].getnewaddress(): 1}], 0, {"include_unsafe": True}) + # BIP 174 Test Vectors # Check that unknown values are just passed through diff --git a/test/functional/wallet_send.py b/test/functional/wallet_send.py index 53553dcd80..44a17ef650 100755 --- a/test/functional/wallet_send.py +++ b/test/functional/wallet_send.py @@ -33,12 +33,15 @@ class WalletSendTest(BitcoinTestFramework): def test_send(self, from_wallet, to_wallet=None, amount=None, data=None, arg_conf_target=None, arg_estimate_mode=None, arg_fee_rate=None, conf_target=None, estimate_mode=None, fee_rate=None, add_to_wallet=None, psbt=None, - inputs=None, add_inputs=None, change_address=None, change_position=None, change_type=None, + inputs=None, add_inputs=None, include_unsafe=None, change_address=None, change_position=None, change_type=None, include_watching=None, locktime=None, lock_unspents=None, replaceable=None, subtract_fee_from_outputs=None, expect_error=None): assert (amount is None) != (data is None) - from_balance_before = from_wallet.getbalance() + from_balance_before = from_wallet.getbalances()["mine"]["trusted"] + if include_unsafe: + from_balance_before += from_wallet.getbalances()["mine"]["untrusted_pending"] + if to_wallet is None: assert amount is None else: @@ -71,6 +74,8 @@ class WalletSendTest(BitcoinTestFramework): options["inputs"] = inputs if add_inputs is not None: options["add_inputs"] = add_inputs + if include_unsafe is not None: + options["include_unsafe"] = include_unsafe if change_address is not None: options["change_address"] = change_address if change_position is not None: @@ -133,6 +138,10 @@ class WalletSendTest(BitcoinTestFramework): assert not "txid" in res assert "psbt" in res + from_balance = from_wallet.getbalances()["mine"]["trusted"] + if include_unsafe: + from_balance += from_wallet.getbalances()["mine"]["untrusted_pending"] + if add_to_wallet and not include_watching: # Ensure transaction exists in the wallet: tx = from_wallet.gettransaction(res["txid"]) @@ -143,13 +152,13 @@ class WalletSendTest(BitcoinTestFramework): assert tx if amount: if subtract_fee_from_outputs: - assert_equal(from_balance_before - from_wallet.getbalance(), amount) + assert_equal(from_balance_before - from_balance, amount) else: - assert_greater_than(from_balance_before - from_wallet.getbalance(), amount) + assert_greater_than(from_balance_before - from_balance, amount) else: assert next((out for out in tx["vout"] if out["scriptPubKey"]["asm"] == "OP_RETURN 35"), None) else: - assert_equal(from_balance_before, from_wallet.getbalance()) + assert_equal(from_balance_before, from_balance) if to_wallet: self.sync_mempools() @@ -440,6 +449,14 @@ class WalletSendTest(BitcoinTestFramework): self.log.info("Subtract fee from output") self.test_send(from_wallet=w0, to_wallet=w1, amount=1, subtract_fee_from_outputs=[0]) + self.log.info("Include unsafe inputs") + self.nodes[1].createwallet(wallet_name="w5") + w5 = self.nodes[1].get_wallet_rpc("w5") + self.test_send(from_wallet=w0, to_wallet=w5, amount=2) + self.test_send(from_wallet=w5, to_wallet=w0, amount=1, expect_error=(-4, "Insufficient funds")) + res = self.test_send(from_wallet=w5, to_wallet=w0, amount=1, include_unsafe=True) + assert res["complete"] + if __name__ == '__main__': WalletSendTest().main() -- cgit v1.2.3