mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-01-25 02:33:24 -03:00
tests: Test specifying input weights
Added tests to rpc_fundrawtransaction, wallet_send, and rpc_psbt that test that external inputs can be spent when input weight is provided. Also tested that the input weight overrides any calculated weight. Additionally, rpc_psbt's external inputs test is cleaned up a bit to be more similar to rpc_fundrawtransaction's and avoid potential pitfalls due to non-deterministic coin selection behavior.
This commit is contained in:
parent
6fa762a372
commit
3866272c45
3 changed files with 170 additions and 12 deletions
|
@ -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()
|
||||
|
|
Loading…
Add table
Reference in a new issue