mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-04-21 12:18:13 +02:00
Merge bitcoin/bitcoin#23201: wallet: Allow users to specify input weights when funding a transaction
3866272c45tests: Test specifying input weights (Andrew Chow)6fa762a372rpc, wallet: Allow users to specify input weights (Andrew Chow)808068e90ewallet: Allow user specified input size to override (Andrew Chow)4060c50d7ewallet: add input weights to CCoinControl (Andrew Chow) Pull request description: When funding a transaction with external inputs, instead of providing solving data, a user may want to just provide the maximum signed size of that input. This is particularly useful in cases where the input is nonstandard as our dummy signer is unable to handle those inputs. The input weight can be provided to any input regardless of whether it belongs to the wallet and the provided weight will always be used regardless of any calculated input weight. This allows the user to override the calculated input weight which may overestimate in some circumstances due to missing information (e.g. if the private key is not known, a maximum size signature will be used, but the actual signer may be doing additional work which reduces the size of the signature). For `send` and `walletcreatefundedpsbt`, the input weight is specified in a `weight` field in an input object. For `fundrawtransaction`, a new `input_weights` field is added to the `options` object. This is an array of objects consisting of a txid, vout, and weight. Closes #23187 ACKs for top commit: instagibbs: reACK3866272c45glozow: reACK3866272via range-diff t-bast: ACK3866272c45Tree-SHA512: 2c8b471ee537c62a51389b7c4e86b5ac1c3a223b444195042be8117b3c83e29c0619463610b950cbbd1648d3ed01ecc5bb0b3c4f39640680da9157763b9b9f9f
This commit is contained in:
@@ -4,8 +4,10 @@
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
"""Test the fundrawtransaction RPC."""
|
||||
|
||||
|
||||
from decimal import Decimal
|
||||
from itertools import product
|
||||
from math import ceil
|
||||
|
||||
from test_framework.descriptors import descsum_create
|
||||
from test_framework.key import ECKey
|
||||
@@ -1003,7 +1005,7 @@ class RawTransactionsTest(BitcoinTestFramework):
|
||||
ext_utxo = self.nodes[0].listunspent(addresses=[addr])[0]
|
||||
|
||||
# An external input without solving data should result in an error
|
||||
raw_tx = wallet.createrawtransaction([ext_utxo], {self.nodes[0].getnewaddress(): 15})
|
||||
raw_tx = wallet.createrawtransaction([ext_utxo], {self.nodes[0].getnewaddress(): ext_utxo["amount"] / 2})
|
||||
assert_raises_rpc_error(-4, "Insufficient funds", wallet.fundrawtransaction, raw_tx)
|
||||
|
||||
# Error conditions
|
||||
@@ -1011,6 +1013,12 @@ class RawTransactionsTest(BitcoinTestFramework):
|
||||
assert_raises_rpc_error(-5, "'01234567890a0b0c0d0e0f' is not a valid public key", wallet.fundrawtransaction, raw_tx, {"solving_data": {"pubkeys":["01234567890a0b0c0d0e0f"]}})
|
||||
assert_raises_rpc_error(-5, "'not a script' is not hex", wallet.fundrawtransaction, raw_tx, {"solving_data": {"scripts":["not a script"]}})
|
||||
assert_raises_rpc_error(-8, "Unable to parse descriptor 'not a descriptor'", wallet.fundrawtransaction, raw_tx, {"solving_data": {"descriptors":["not a descriptor"]}})
|
||||
assert_raises_rpc_error(-8, "Invalid parameter, missing vout key", wallet.fundrawtransaction, raw_tx, {"input_weights": [{"txid": ext_utxo["txid"]}]})
|
||||
assert_raises_rpc_error(-8, "Invalid parameter, vout cannot be negative", wallet.fundrawtransaction, raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": -1}]})
|
||||
assert_raises_rpc_error(-8, "Invalid parameter, missing weight key", wallet.fundrawtransaction, raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"]}]})
|
||||
assert_raises_rpc_error(-8, "Invalid parameter, weight cannot be less than 165", wallet.fundrawtransaction, raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": 164}]})
|
||||
assert_raises_rpc_error(-8, "Invalid parameter, weight cannot be less than 165", wallet.fundrawtransaction, raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": -1}]})
|
||||
assert_raises_rpc_error(-8, "Invalid parameter, weight cannot be greater than", wallet.fundrawtransaction, raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": 400001}]})
|
||||
|
||||
# But funding should work when the solving data is provided
|
||||
funded_tx = wallet.fundrawtransaction(raw_tx, {"solving_data": {"pubkeys": [addr_info['pubkey']], "scripts": [addr_info["embedded"]["scriptPubKey"]]}})
|
||||
@@ -1020,10 +1028,45 @@ class RawTransactionsTest(BitcoinTestFramework):
|
||||
assert signed_tx['complete']
|
||||
|
||||
funded_tx = wallet.fundrawtransaction(raw_tx, {"solving_data": {"descriptors": [desc]}})
|
||||
signed_tx = wallet.signrawtransactionwithwallet(funded_tx['hex'])
|
||||
assert not signed_tx['complete']
|
||||
signed_tx = self.nodes[0].signrawtransactionwithwallet(signed_tx['hex'])
|
||||
assert signed_tx['complete']
|
||||
signed_tx1 = wallet.signrawtransactionwithwallet(funded_tx['hex'])
|
||||
assert not signed_tx1['complete']
|
||||
signed_tx2 = self.nodes[0].signrawtransactionwithwallet(signed_tx1['hex'])
|
||||
assert signed_tx2['complete']
|
||||
|
||||
unsigned_weight = self.nodes[0].decoderawtransaction(signed_tx1["hex"])["weight"]
|
||||
signed_weight = self.nodes[0].decoderawtransaction(signed_tx2["hex"])["weight"]
|
||||
# Input's weight is difference between weight of signed and unsigned,
|
||||
# and the weight of stuff that didn't change (prevout, sequence, 1 byte of scriptSig)
|
||||
input_weight = signed_weight - unsigned_weight + (41 * 4)
|
||||
low_input_weight = input_weight // 2
|
||||
high_input_weight = input_weight * 2
|
||||
|
||||
# Funding should also work if the input weight is provided
|
||||
funded_tx = wallet.fundrawtransaction(raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": input_weight}]})
|
||||
signed_tx = wallet.signrawtransactionwithwallet(funded_tx["hex"])
|
||||
signed_tx = self.nodes[0].signrawtransactionwithwallet(signed_tx["hex"])
|
||||
assert_equal(self.nodes[0].testmempoolaccept([signed_tx["hex"]])[0]["allowed"], True)
|
||||
assert_equal(signed_tx["complete"], True)
|
||||
# Reducing the weight should have a lower fee
|
||||
funded_tx2 = wallet.fundrawtransaction(raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": low_input_weight}]})
|
||||
assert_greater_than(funded_tx["fee"], funded_tx2["fee"])
|
||||
# Increasing the weight should have a higher fee
|
||||
funded_tx2 = wallet.fundrawtransaction(raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": high_input_weight}]})
|
||||
assert_greater_than(funded_tx2["fee"], funded_tx["fee"])
|
||||
# The provided weight should override the calculated weight when solving data is provided
|
||||
funded_tx3 = wallet.fundrawtransaction(raw_tx, {"solving_data": {"descriptors": [desc]}, "input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": high_input_weight}]})
|
||||
assert_equal(funded_tx2["fee"], funded_tx3["fee"])
|
||||
# The feerate should be met
|
||||
funded_tx4 = wallet.fundrawtransaction(raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": high_input_weight}], "fee_rate": 10})
|
||||
input_add_weight = high_input_weight - (41 * 4)
|
||||
tx4_weight = wallet.decoderawtransaction(funded_tx4["hex"])["weight"] + input_add_weight
|
||||
tx4_vsize = int(ceil(tx4_weight / 4))
|
||||
assert_fee_amount(funded_tx4["fee"], tx4_vsize, Decimal(0.0001))
|
||||
|
||||
# Funding with weight at csuint boundaries should not cause problems
|
||||
funded_tx = wallet.fundrawtransaction(raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": 255}]})
|
||||
funded_tx = wallet.fundrawtransaction(raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": 65539}]})
|
||||
|
||||
self.nodes[2].unloadwallet("extfund")
|
||||
|
||||
def test_include_unsafe(self):
|
||||
|
||||
@@ -606,11 +606,15 @@ class PSBTTest(BitcoinTestFramework):
|
||||
|
||||
assert_raises_rpc_error(-25, 'Inputs missing or spent', self.nodes[0].walletprocesspsbt, 'cHNidP8BAJoCAAAAAkvEW8NnDtdNtDpsmze+Ht2LH35IJcKv00jKAlUs21RrAwAAAAD/////S8Rbw2cO1020OmybN74e3Ysffkglwq/TSMoCVSzbVGsBAAAAAP7///8CwLYClQAAAAAWABSNJKzjaUb3uOxixsvh1GGE3fW7zQD5ApUAAAAAFgAUKNw0x8HRctAgmvoevm4u1SbN7XIAAAAAAAEAnQIAAAACczMa321tVHuN4GKWKRncycI22aX3uXgwSFUKM2orjRsBAAAAAP7///9zMxrfbW1Ue43gYpYpGdzJwjbZpfe5eDBIVQozaiuNGwAAAAAA/v///wIA+QKVAAAAABl2qRT9zXUVA8Ls5iVqynLHe5/vSe1XyYisQM0ClQAAAAAWABRmWQUcjSjghQ8/uH4Bn/zkakwLtAAAAAAAAQEfQM0ClQAAAAAWABRmWQUcjSjghQ8/uH4Bn/zkakwLtAAAAA==')
|
||||
|
||||
# Test that we can fund psbts with external inputs specified
|
||||
self.log.info("Test that we can fund psbts with external inputs specified")
|
||||
|
||||
eckey = ECKey()
|
||||
eckey.generate()
|
||||
privkey = bytes_to_wif(eckey.get_bytes())
|
||||
|
||||
self.nodes[1].createwallet("extfund")
|
||||
wallet = self.nodes[1].get_wallet_rpc("extfund")
|
||||
|
||||
# Make a weird but signable script. sh(pkh()) descriptor accomplishes this
|
||||
desc = descsum_create("sh(pkh({}))".format(privkey))
|
||||
if self.options.descriptors:
|
||||
@@ -622,26 +626,97 @@ class PSBTTest(BitcoinTestFramework):
|
||||
addr_info = self.nodes[0].getaddressinfo(addr)
|
||||
|
||||
self.nodes[0].sendtoaddress(addr, 10)
|
||||
self.nodes[0].sendtoaddress(wallet.getnewaddress(), 10)
|
||||
self.generate(self.nodes[0], 6)
|
||||
ext_utxo = self.nodes[0].listunspent(addresses=[addr])[0]
|
||||
|
||||
# An external input without solving data should result in an error
|
||||
assert_raises_rpc_error(-4, "Insufficient funds", self.nodes[1].walletcreatefundedpsbt, [ext_utxo], {self.nodes[0].getnewaddress(): 10 + ext_utxo['amount']}, 0, {'add_inputs': True})
|
||||
assert_raises_rpc_error(-4, "Insufficient funds", wallet.walletcreatefundedpsbt, [ext_utxo], {self.nodes[0].getnewaddress(): 15})
|
||||
|
||||
# But funding should work when the solving data is provided
|
||||
psbt = self.nodes[1].walletcreatefundedpsbt([ext_utxo], {self.nodes[0].getnewaddress(): 15}, 0, {'add_inputs': True, "solving_data": {"pubkeys": [addr_info['pubkey']], "scripts": [addr_info["embedded"]["scriptPubKey"]]}})
|
||||
signed = self.nodes[1].walletprocesspsbt(psbt['psbt'])
|
||||
psbt = wallet.walletcreatefundedpsbt([ext_utxo], {self.nodes[0].getnewaddress(): 15}, 0, {"add_inputs": True, "solving_data": {"pubkeys": [addr_info['pubkey']], "scripts": [addr_info["embedded"]["scriptPubKey"]]}})
|
||||
signed = wallet.walletprocesspsbt(psbt['psbt'])
|
||||
assert not signed['complete']
|
||||
signed = self.nodes[0].walletprocesspsbt(signed['psbt'])
|
||||
assert signed['complete']
|
||||
self.nodes[0].finalizepsbt(signed['psbt'])
|
||||
|
||||
psbt = self.nodes[1].walletcreatefundedpsbt([ext_utxo], {self.nodes[0].getnewaddress(): 15}, 0, {'add_inputs': True, "solving_data":{"descriptors": [desc]}})
|
||||
signed = self.nodes[1].walletprocesspsbt(psbt['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']
|
||||
self.nodes[0].finalizepsbt(signed['psbt'])
|
||||
final = self.nodes[0].finalizepsbt(signed['psbt'], False)
|
||||
|
||||
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
|
||||
break
|
||||
psbt_in = dec["inputs"][input_idx]
|
||||
# Calculate the input weight
|
||||
# (prevout + sequence + length of scriptSig + 2 bytes buffer) * 4 + len of scriptwitness
|
||||
len_scriptsig = len(psbt_in["final_scriptSig"]["hex"]) // 2 if "final_scriptSig" in psbt_in else 0
|
||||
len_scriptwitness = len(psbt_in["final_scriptwitness"]["hex"]) // 2 if "final_scriptwitness" in psbt_in else 0
|
||||
input_weight = ((41 + len_scriptsig + 2) * 4) + len_scriptwitness
|
||||
low_input_weight = input_weight // 2
|
||||
high_input_weight = input_weight * 2
|
||||
|
||||
# Input weight error conditions
|
||||
assert_raises_rpc_error(
|
||||
-8,
|
||||
"Input weights should be specified in inputs rather than in options.",
|
||||
wallet.walletcreatefundedpsbt,
|
||||
inputs=[ext_utxo],
|
||||
outputs={self.nodes[0].getnewaddress(): 15},
|
||||
options={"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": 1000}]}
|
||||
)
|
||||
|
||||
# Funding should also work if the input weight is provided
|
||||
psbt = wallet.walletcreatefundedpsbt(
|
||||
inputs=[{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": input_weight}],
|
||||
outputs={self.nodes[0].getnewaddress(): 15},
|
||||
options={"add_inputs": True}
|
||||
)
|
||||
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"]
|
||||
# Reducing the weight should have a lower fee
|
||||
psbt2 = wallet.walletcreatefundedpsbt(
|
||||
inputs=[{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": low_input_weight}],
|
||||
outputs={self.nodes[0].getnewaddress(): 15},
|
||||
options={"add_inputs": True}
|
||||
)
|
||||
assert_greater_than(psbt["fee"], psbt2["fee"])
|
||||
# Increasing the weight should have a higher fee
|
||||
psbt2 = wallet.walletcreatefundedpsbt(
|
||||
inputs=[{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": high_input_weight}],
|
||||
outputs={self.nodes[0].getnewaddress(): 15},
|
||||
options={"add_inputs": True}
|
||||
)
|
||||
assert_greater_than(psbt2["fee"], psbt["fee"])
|
||||
# The provided weight should override the calculated weight when solving data is provided
|
||||
psbt3 = wallet.walletcreatefundedpsbt(
|
||||
inputs=[{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": high_input_weight}],
|
||||
outputs={self.nodes[0].getnewaddress(): 15},
|
||||
options={'add_inputs': True, "solving_data":{"descriptors": [desc]}}
|
||||
)
|
||||
assert_equal(psbt2["fee"], psbt3["fee"])
|
||||
|
||||
# Import the external utxo descriptor so that we can sign for it from the test wallet
|
||||
if self.options.descriptors:
|
||||
res = wallet.importdescriptors([{"desc": desc, "timestamp": "now"}])
|
||||
else:
|
||||
res = wallet.importmulti([{"desc": desc, "timestamp": "now"}])
|
||||
assert res[0]["success"]
|
||||
# The provided weight should override the calculated weight for a wallet input
|
||||
psbt3 = wallet.walletcreatefundedpsbt(
|
||||
inputs=[{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": high_input_weight}],
|
||||
outputs={self.nodes[0].getnewaddress(): 15},
|
||||
options={"add_inputs": True}
|
||||
)
|
||||
assert_equal(psbt2["fee"], psbt3["fee"])
|
||||
|
||||
if __name__ == '__main__':
|
||||
PSBTTest().main()
|
||||
|
||||
@@ -518,5 +518,45 @@ class WalletSendTest(BitcoinTestFramework):
|
||||
assert signed["complete"]
|
||||
self.nodes[0].finalizepsbt(signed["psbt"])
|
||||
|
||||
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
|
||||
break
|
||||
psbt_in = dec["inputs"][input_idx]
|
||||
# Calculate the input weight
|
||||
# (prevout + sequence + length of scriptSig + 2 bytes buffer) * 4 + len of scriptwitness
|
||||
len_scriptsig = len(psbt_in["final_scriptSig"]["hex"]) // 2 if "final_scriptSig" in psbt_in else 0
|
||||
len_scriptwitness = len(psbt_in["final_scriptwitness"]["hex"]) // 2 if "final_scriptwitness" in psbt_in else 0
|
||||
input_weight = ((41 + len_scriptsig + 2) * 4) + len_scriptwitness
|
||||
|
||||
# Input weight error conditions
|
||||
assert_raises_rpc_error(
|
||||
-8,
|
||||
"Input weights should be specified in inputs rather than in options.",
|
||||
ext_wallet.send,
|
||||
outputs={self.nodes[0].getnewaddress(): 15},
|
||||
options={"inputs": [ext_utxo], "input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": 1000}]}
|
||||
)
|
||||
|
||||
# Funding should also work when input weights are provided
|
||||
res = self.test_send(
|
||||
from_wallet=ext_wallet,
|
||||
to_wallet=self.nodes[0],
|
||||
amount=15,
|
||||
inputs=[{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": input_weight}],
|
||||
add_inputs=True,
|
||||
psbt=True,
|
||||
include_watching=True,
|
||||
fee_rate=10
|
||||
)
|
||||
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]
|
||||
assert_equal(testres["allowed"], True)
|
||||
assert_fee_amount(testres["fees"]["base"], testres["vsize"], Decimal(0.0001))
|
||||
|
||||
if __name__ == '__main__':
|
||||
WalletSendTest().main()
|
||||
|
||||
Reference in New Issue
Block a user