Skip to content

Commit 3866272

Browse files
committed
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.
1 parent 6fa762a commit 3866272

File tree

3 files changed

+170
-12
lines changed

3 files changed

+170
-12
lines changed

test/functional/rpc_fundrawtransaction.py

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
55
"""Test the fundrawtransaction RPC."""
66

7+
78
from decimal import Decimal
89
from itertools import product
10+
from math import ceil
911

1012
from test_framework.descriptors import descsum_create
1113
from test_framework.key import ECKey
@@ -1003,14 +1005,20 @@ def test_external_inputs(self):
10031005
ext_utxo = self.nodes[0].listunspent(addresses=[addr])[0]
10041006

10051007
# An external input without solving data should result in an error
1006-
raw_tx = wallet.createrawtransaction([ext_utxo], {self.nodes[0].getnewaddress(): 15})
1008+
raw_tx = wallet.createrawtransaction([ext_utxo], {self.nodes[0].getnewaddress(): ext_utxo["amount"] / 2})
10071009
assert_raises_rpc_error(-4, "Insufficient funds", wallet.fundrawtransaction, raw_tx)
10081010

10091011
# Error conditions
10101012
assert_raises_rpc_error(-5, "'not a pubkey' is not hex", wallet.fundrawtransaction, raw_tx, {"solving_data": {"pubkeys":["not a pubkey"]}})
10111013
assert_raises_rpc_error(-5, "'01234567890a0b0c0d0e0f' is not a valid public key", wallet.fundrawtransaction, raw_tx, {"solving_data": {"pubkeys":["01234567890a0b0c0d0e0f"]}})
10121014
assert_raises_rpc_error(-5, "'not a script' is not hex", wallet.fundrawtransaction, raw_tx, {"solving_data": {"scripts":["not a script"]}})
10131015
assert_raises_rpc_error(-8, "Unable to parse descriptor 'not a descriptor'", wallet.fundrawtransaction, raw_tx, {"solving_data": {"descriptors":["not a descriptor"]}})
1016+
assert_raises_rpc_error(-8, "Invalid parameter, missing vout key", wallet.fundrawtransaction, raw_tx, {"input_weights": [{"txid": ext_utxo["txid"]}]})
1017+
assert_raises_rpc_error(-8, "Invalid parameter, vout cannot be negative", wallet.fundrawtransaction, raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": -1}]})
1018+
assert_raises_rpc_error(-8, "Invalid parameter, missing weight key", wallet.fundrawtransaction, raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"]}]})
1019+
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}]})
1020+
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}]})
1021+
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}]})
10141022

10151023
# But funding should work when the solving data is provided
10161024
funded_tx = wallet.fundrawtransaction(raw_tx, {"solving_data": {"pubkeys": [addr_info['pubkey']], "scripts": [addr_info["embedded"]["scriptPubKey"]]}})
@@ -1020,10 +1028,45 @@ def test_external_inputs(self):
10201028
assert signed_tx['complete']
10211029

10221030
funded_tx = wallet.fundrawtransaction(raw_tx, {"solving_data": {"descriptors": [desc]}})
1023-
signed_tx = wallet.signrawtransactionwithwallet(funded_tx['hex'])
1024-
assert not signed_tx['complete']
1025-
signed_tx = self.nodes[0].signrawtransactionwithwallet(signed_tx['hex'])
1026-
assert signed_tx['complete']
1031+
signed_tx1 = wallet.signrawtransactionwithwallet(funded_tx['hex'])
1032+
assert not signed_tx1['complete']
1033+
signed_tx2 = self.nodes[0].signrawtransactionwithwallet(signed_tx1['hex'])
1034+
assert signed_tx2['complete']
1035+
1036+
unsigned_weight = self.nodes[0].decoderawtransaction(signed_tx1["hex"])["weight"]
1037+
signed_weight = self.nodes[0].decoderawtransaction(signed_tx2["hex"])["weight"]
1038+
# Input's weight is difference between weight of signed and unsigned,
1039+
# and the weight of stuff that didn't change (prevout, sequence, 1 byte of scriptSig)
1040+
input_weight = signed_weight - unsigned_weight + (41 * 4)
1041+
low_input_weight = input_weight // 2
1042+
high_input_weight = input_weight * 2
1043+
1044+
# Funding should also work if the input weight is provided
1045+
funded_tx = wallet.fundrawtransaction(raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": input_weight}]})
1046+
signed_tx = wallet.signrawtransactionwithwallet(funded_tx["hex"])
1047+
signed_tx = self.nodes[0].signrawtransactionwithwallet(signed_tx["hex"])
1048+
assert_equal(self.nodes[0].testmempoolaccept([signed_tx["hex"]])[0]["allowed"], True)
1049+
assert_equal(signed_tx["complete"], True)
1050+
# Reducing the weight should have a lower fee
1051+
funded_tx2 = wallet.fundrawtransaction(raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": low_input_weight}]})
1052+
assert_greater_than(funded_tx["fee"], funded_tx2["fee"])
1053+
# Increasing the weight should have a higher fee
1054+
funded_tx2 = wallet.fundrawtransaction(raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": high_input_weight}]})
1055+
assert_greater_than(funded_tx2["fee"], funded_tx["fee"])
1056+
# The provided weight should override the calculated weight when solving data is provided
1057+
funded_tx3 = wallet.fundrawtransaction(raw_tx, {"solving_data": {"descriptors": [desc]}, "input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": high_input_weight}]})
1058+
assert_equal(funded_tx2["fee"], funded_tx3["fee"])
1059+
# The feerate should be met
1060+
funded_tx4 = wallet.fundrawtransaction(raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": high_input_weight}], "fee_rate": 10})
1061+
input_add_weight = high_input_weight - (41 * 4)
1062+
tx4_weight = wallet.decoderawtransaction(funded_tx4["hex"])["weight"] + input_add_weight
1063+
tx4_vsize = int(ceil(tx4_weight / 4))
1064+
assert_fee_amount(funded_tx4["fee"], tx4_vsize, Decimal(0.0001))
1065+
1066+
# Funding with weight at csuint boundaries should not cause problems
1067+
funded_tx = wallet.fundrawtransaction(raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": 255}]})
1068+
funded_tx = wallet.fundrawtransaction(raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": 65539}]})
1069+
10271070
self.nodes[2].unloadwallet("extfund")
10281071

10291072
def test_include_unsafe(self):

test/functional/rpc_psbt.py

Lines changed: 82 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -606,11 +606,15 @@ def test_psbt_input_keys(psbt_input, keys):
606606

607607
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==')
608608

609-
# Test that we can fund psbts with external inputs specified
609+
self.log.info("Test that we can fund psbts with external inputs specified")
610+
610611
eckey = ECKey()
611612
eckey.generate()
612613
privkey = bytes_to_wif(eckey.get_bytes())
613614

615+
self.nodes[1].createwallet("extfund")
616+
wallet = self.nodes[1].get_wallet_rpc("extfund")
617+
614618
# Make a weird but signable script. sh(pkh()) descriptor accomplishes this
615619
desc = descsum_create("sh(pkh({}))".format(privkey))
616620
if self.options.descriptors:
@@ -622,26 +626,97 @@ def test_psbt_input_keys(psbt_input, keys):
622626
addr_info = self.nodes[0].getaddressinfo(addr)
623627

624628
self.nodes[0].sendtoaddress(addr, 10)
629+
self.nodes[0].sendtoaddress(wallet.getnewaddress(), 10)
625630
self.generate(self.nodes[0], 6)
626631
ext_utxo = self.nodes[0].listunspent(addresses=[addr])[0]
627632

628633
# An external input without solving data should result in an error
629-
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})
634+
assert_raises_rpc_error(-4, "Insufficient funds", wallet.walletcreatefundedpsbt, [ext_utxo], {self.nodes[0].getnewaddress(): 15})
630635

631636
# But funding should work when the solving data is provided
632-
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"]]}})
633-
signed = self.nodes[1].walletprocesspsbt(psbt['psbt'])
637+
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"]]}})
638+
signed = wallet.walletprocesspsbt(psbt['psbt'])
634639
assert not signed['complete']
635640
signed = self.nodes[0].walletprocesspsbt(signed['psbt'])
636641
assert signed['complete']
637642
self.nodes[0].finalizepsbt(signed['psbt'])
638643

639-
psbt = self.nodes[1].walletcreatefundedpsbt([ext_utxo], {self.nodes[0].getnewaddress(): 15}, 0, {'add_inputs': True, "solving_data":{"descriptors": [desc]}})
640-
signed = self.nodes[1].walletprocesspsbt(psbt['psbt'])
644+
psbt = wallet.walletcreatefundedpsbt([ext_utxo], {self.nodes[0].getnewaddress(): 15}, 0, {"add_inputs": True, "solving_data":{"descriptors": [desc]}})
645+
signed = wallet.walletprocesspsbt(psbt['psbt'])
641646
assert not signed['complete']
642647
signed = self.nodes[0].walletprocesspsbt(signed['psbt'])
643648
assert signed['complete']
644-
self.nodes[0].finalizepsbt(signed['psbt'])
649+
final = self.nodes[0].finalizepsbt(signed['psbt'], False)
650+
651+
dec = self.nodes[0].decodepsbt(signed["psbt"])
652+
for i, txin in enumerate(dec["tx"]["vin"]):
653+
if txin["txid"] == ext_utxo["txid"] and txin["vout"] == ext_utxo["vout"]:
654+
input_idx = i
655+
break
656+
psbt_in = dec["inputs"][input_idx]
657+
# Calculate the input weight
658+
# (prevout + sequence + length of scriptSig + 2 bytes buffer) * 4 + len of scriptwitness
659+
len_scriptsig = len(psbt_in["final_scriptSig"]["hex"]) // 2 if "final_scriptSig" in psbt_in else 0
660+
len_scriptwitness = len(psbt_in["final_scriptwitness"]["hex"]) // 2 if "final_scriptwitness" in psbt_in else 0
661+
input_weight = ((41 + len_scriptsig + 2) * 4) + len_scriptwitness
662+
low_input_weight = input_weight // 2
663+
high_input_weight = input_weight * 2
664+
665+
# Input weight error conditions
666+
assert_raises_rpc_error(
667+
-8,
668+
"Input weights should be specified in inputs rather than in options.",
669+
wallet.walletcreatefundedpsbt,
670+
inputs=[ext_utxo],
671+
outputs={self.nodes[0].getnewaddress(): 15},
672+
options={"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": 1000}]}
673+
)
674+
675+
# Funding should also work if the input weight is provided
676+
psbt = wallet.walletcreatefundedpsbt(
677+
inputs=[{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": input_weight}],
678+
outputs={self.nodes[0].getnewaddress(): 15},
679+
options={"add_inputs": True}
680+
)
681+
signed = wallet.walletprocesspsbt(psbt["psbt"])
682+
signed = self.nodes[0].walletprocesspsbt(signed["psbt"])
683+
final = self.nodes[0].finalizepsbt(signed["psbt"])
684+
assert self.nodes[0].testmempoolaccept([final["hex"]])[0]["allowed"]
685+
# Reducing the weight should have a lower fee
686+
psbt2 = wallet.walletcreatefundedpsbt(
687+
inputs=[{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": low_input_weight}],
688+
outputs={self.nodes[0].getnewaddress(): 15},
689+
options={"add_inputs": True}
690+
)
691+
assert_greater_than(psbt["fee"], psbt2["fee"])
692+
# Increasing the weight should have a higher fee
693+
psbt2 = wallet.walletcreatefundedpsbt(
694+
inputs=[{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": high_input_weight}],
695+
outputs={self.nodes[0].getnewaddress(): 15},
696+
options={"add_inputs": True}
697+
)
698+
assert_greater_than(psbt2["fee"], psbt["fee"])
699+
# The provided weight should override the calculated weight when solving data is provided
700+
psbt3 = wallet.walletcreatefundedpsbt(
701+
inputs=[{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": high_input_weight}],
702+
outputs={self.nodes[0].getnewaddress(): 15},
703+
options={'add_inputs': True, "solving_data":{"descriptors": [desc]}}
704+
)
705+
assert_equal(psbt2["fee"], psbt3["fee"])
706+
707+
# Import the external utxo descriptor so that we can sign for it from the test wallet
708+
if self.options.descriptors:
709+
res = wallet.importdescriptors([{"desc": desc, "timestamp": "now"}])
710+
else:
711+
res = wallet.importmulti([{"desc": desc, "timestamp": "now"}])
712+
assert res[0]["success"]
713+
# The provided weight should override the calculated weight for a wallet input
714+
psbt3 = wallet.walletcreatefundedpsbt(
715+
inputs=[{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": high_input_weight}],
716+
outputs={self.nodes[0].getnewaddress(): 15},
717+
options={"add_inputs": True}
718+
)
719+
assert_equal(psbt2["fee"], psbt3["fee"])
645720

646721
if __name__ == '__main__':
647722
PSBTTest().main()

test/functional/wallet_send.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,5 +518,45 @@ def run_test(self):
518518
assert signed["complete"]
519519
self.nodes[0].finalizepsbt(signed["psbt"])
520520

521+
dec = self.nodes[0].decodepsbt(signed["psbt"])
522+
for i, txin in enumerate(dec["tx"]["vin"]):
523+
if txin["txid"] == ext_utxo["txid"] and txin["vout"] == ext_utxo["vout"]:
524+
input_idx = i
525+
break
526+
psbt_in = dec["inputs"][input_idx]
527+
# Calculate the input weight
528+
# (prevout + sequence + length of scriptSig + 2 bytes buffer) * 4 + len of scriptwitness
529+
len_scriptsig = len(psbt_in["final_scriptSig"]["hex"]) // 2 if "final_scriptSig" in psbt_in else 0
530+
len_scriptwitness = len(psbt_in["final_scriptwitness"]["hex"]) // 2 if "final_scriptwitness" in psbt_in else 0
531+
input_weight = ((41 + len_scriptsig + 2) * 4) + len_scriptwitness
532+
533+
# Input weight error conditions
534+
assert_raises_rpc_error(
535+
-8,
536+
"Input weights should be specified in inputs rather than in options.",
537+
ext_wallet.send,
538+
outputs={self.nodes[0].getnewaddress(): 15},
539+
options={"inputs": [ext_utxo], "input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": 1000}]}
540+
)
541+
542+
# Funding should also work when input weights are provided
543+
res = self.test_send(
544+
from_wallet=ext_wallet,
545+
to_wallet=self.nodes[0],
546+
amount=15,
547+
inputs=[{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": input_weight}],
548+
add_inputs=True,
549+
psbt=True,
550+
include_watching=True,
551+
fee_rate=10
552+
)
553+
signed = ext_wallet.walletprocesspsbt(res["psbt"])
554+
signed = ext_fund.walletprocesspsbt(res["psbt"])
555+
assert signed["complete"]
556+
tx = self.nodes[0].finalizepsbt(signed["psbt"])
557+
testres = self.nodes[0].testmempoolaccept([tx["hex"]])[0]
558+
assert_equal(testres["allowed"], True)
559+
assert_fee_amount(testres["fees"]["base"], testres["vsize"], Decimal(0.0001))
560+
521561
if __name__ == '__main__':
522562
WalletSendTest().main()

0 commit comments

Comments
 (0)