Skip to content

Commit

Permalink
Merge bitcoin/bitcoin#28264: test: refactor: support sending funds wi…
Browse files Browse the repository at this point in the history
…th outpoint result

50d1ac1 test: remove unused `find_output` helper (Sebastian Falbesoner)
73a339a test: refactor: support sending funds with outpoint result (Sebastian Falbesoner)

Pull request description:

  In wallet-related functional tests we often want to send funds to an address and  use the resulting (non-change) UTXO directly after as input for another transaction. Doing that is currently tedious, as it involves finding the index part of the outpoint manually by calling helpers like `find_vout_for_address` or `find_output` first.  This results in two different txid/vout variables which then again have to be combined to a single dictionary `{"txid": ..., "vout": ...}` in order to be specified as input for RPCs like `createrawtransaction` or `createpsbt`. For example:

  ```
  txid1 = node1.sendtoaddress(addr1, value1)
  vout1 = find_vout_for_address(node1, txid1, addr1)
  txid2 = node2.sendtoaddress(addr2, value2)
  vout2 = find_vout_for_address(node2, txid2, addr2)
  node.createrawtransaction([{'txid': txid1, 'vout': vout1}, {'txid': txid2, 'vout': vout2}], .....)
  ```

  This PR introduces a helper `create_outpoints` to immediately return the outpoint as
  UTXO dictionary in the common format, making the tests more readable and avoiding unnecessary duplication:

  ```
  utxo1 = self.create_outpoints(node1, outputs=[{addr1: value1}])[0]
  utxo2 = self.create_outpoints(node2, outputs=[{addr2: value2}])[0]
  node.createrawtransaction([utxo1, utxo2], .....)
  ```

  Tests are switched to work with UTXO-objects rather than two individual txid/vout variables accordingly.

  The `find_output` helper is removed, as it seems generally a bad idea to search for an outpoint only based on the output value. If that's really ever needed in the future, it makes probably more sense to add it as an additional parameter to `find_vout_of_address`. Note that `find_output` supported specifying a block-hash for where to look for the transaction (being passed on to the `getrawtransaction` RPC). This seems to be unneeded, as txids are always unique and for the only test that used that parameter (rpc_psbt.py) there was no observed difference in run-time, so it was not reintroduced in the new helper.

  There are still some `find_vout_of_address` calls remaining, used for detecting change outputs or for whenever the sending happens via `sendrawtransaction` instead, so this PR tackles not all, but the most common case.

ACKs for top commit:
  achow101:
    ACK 50d1ac1
  BrandonOdiwuor:
    ACK 50d1ac1
  maflcko:
    ACK 50d1ac1 🖨

Tree-SHA512: af2bbf13a56cc840fefc1781390cf51625f1e41b3c030f07fc9abb1181b2d414ddbf795e887db029e119cbe45de14f7c987c0cba72ff0b8953080ee218a7915a
  • Loading branch information
achow101 committed Oct 25, 2023
2 parents 64879f4 + 50d1ac1 commit 2a349f9
Show file tree
Hide file tree
Showing 10 changed files with 90 additions and 119 deletions.
47 changes: 19 additions & 28 deletions test/functional/rpc_psbt.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
assert_greater_than,
assert_greater_than_or_equal,
assert_raises_rpc_error,
find_output,
find_vout_for_address,
random_bytes,
)
Expand Down Expand Up @@ -417,16 +416,17 @@ def run_test(self):
self.nodes[0].converttopsbt(hexstring=signedtx['hex'], permitsigdata=True)

# Create outputs to nodes 1 and 2
# (note that we intentionally create two different txs here, as we want
# to check that each node is missing prevout data for one of the two
# utxos, see "should only have data for one input" test below)
node1_addr = self.nodes[1].getnewaddress()
node2_addr = self.nodes[2].getnewaddress()
txid1 = self.nodes[0].sendtoaddress(node1_addr, 13)
txid2 = self.nodes[0].sendtoaddress(node2_addr, 13)
blockhash = self.generate(self.nodes[0], 6)[0]
vout1 = find_output(self.nodes[1], txid1, 13, blockhash=blockhash)
vout2 = find_output(self.nodes[2], txid2, 13, blockhash=blockhash)
utxo1 = self.create_outpoints(self.nodes[0], outputs=[{node1_addr: 13}])[0]
utxo2 = self.create_outpoints(self.nodes[0], outputs=[{node2_addr: 13}])[0]
self.generate(self.nodes[0], 6)[0]

# Create a psbt spending outputs from nodes 1 and 2
psbt_orig = self.nodes[0].createpsbt([{"txid":txid1, "vout":vout1}, {"txid":txid2, "vout":vout2}], {self.nodes[0].getnewaddress():25.999})
psbt_orig = self.nodes[0].createpsbt([utxo1, utxo2], {self.nodes[0].getnewaddress():25.999})

# Update psbts, should only have data for one input and not the other
psbt1 = self.nodes[1].walletprocesspsbt(psbt_orig, False, "ALL")['psbt']
Expand Down Expand Up @@ -603,22 +603,17 @@ def run_test(self):

# Send to all types of addresses
addr1 = self.nodes[1].getnewaddress("", "bech32")
txid1 = self.nodes[0].sendtoaddress(addr1, 11)
vout1 = find_output(self.nodes[0], txid1, 11)
addr2 = self.nodes[1].getnewaddress("", "legacy")
txid2 = self.nodes[0].sendtoaddress(addr2, 11)
vout2 = find_output(self.nodes[0], txid2, 11)
addr3 = self.nodes[1].getnewaddress("", "p2sh-segwit")
txid3 = self.nodes[0].sendtoaddress(addr3, 11)
vout3 = find_output(self.nodes[0], txid3, 11)
utxo1, utxo2, utxo3 = self.create_outpoints(self.nodes[1], outputs=[{addr1: 11}, {addr2: 11}, {addr3: 11}])
self.sync_all()

def test_psbt_input_keys(psbt_input, keys):
"""Check that the psbt input has only the expected keys."""
assert_equal(set(keys), set(psbt_input.keys()))

# Create a PSBT. None of the inputs are filled initially
psbt = self.nodes[1].createpsbt([{"txid":txid1, "vout":vout1},{"txid":txid2, "vout":vout2},{"txid":txid3, "vout":vout3}], {self.nodes[0].getnewaddress():32.999})
psbt = self.nodes[1].createpsbt([utxo1, utxo2, utxo3], {self.nodes[0].getnewaddress():32.999})
decoded = self.nodes[1].decodepsbt(psbt)
test_psbt_input_keys(decoded['inputs'][0], [])
test_psbt_input_keys(decoded['inputs'][1], [])
Expand All @@ -641,15 +636,14 @@ def test_psbt_input_keys(psbt_input, keys):
test_psbt_input_keys(decoded['inputs'][2], ['non_witness_utxo','witness_utxo', 'bip32_derivs', 'redeem_script'])

# Two PSBTs with a common input should not be joinable
psbt1 = self.nodes[1].createpsbt([{"txid":txid1, "vout":vout1}], {self.nodes[0].getnewaddress():Decimal('10.999')})
psbt1 = self.nodes[1].createpsbt([utxo1], {self.nodes[0].getnewaddress():Decimal('10.999')})
assert_raises_rpc_error(-8, "exists in multiple PSBTs", self.nodes[1].joinpsbts, [psbt1, updated])

# Join two distinct PSBTs
addr4 = self.nodes[1].getnewaddress("", "p2sh-segwit")
txid4 = self.nodes[0].sendtoaddress(addr4, 5)
vout4 = find_output(self.nodes[0], txid4, 5)
utxo4 = self.create_outpoints(self.nodes[0], outputs=[{addr4: 5}])[0]
self.generate(self.nodes[0], 6)
psbt2 = self.nodes[1].createpsbt([{"txid":txid4, "vout":vout4}], {self.nodes[0].getnewaddress():Decimal('4.999')})
psbt2 = self.nodes[1].createpsbt([utxo4], {self.nodes[0].getnewaddress():Decimal('4.999')})
psbt2 = self.nodes[1].walletprocesspsbt(psbt2)['psbt']
psbt2_decoded = self.nodes[0].decodepsbt(psbt2)
assert "final_scriptwitness" in psbt2_decoded['inputs'][0] and "final_scriptSig" in psbt2_decoded['inputs'][0]
Expand All @@ -669,11 +663,10 @@ def test_psbt_input_keys(psbt_input, keys):

# Newly created PSBT needs UTXOs and updating
addr = self.nodes[1].getnewaddress("", "p2sh-segwit")
txid = self.nodes[0].sendtoaddress(addr, 7)
utxo = self.create_outpoints(self.nodes[0], outputs=[{addr: 7}])[0]
addrinfo = self.nodes[1].getaddressinfo(addr)
blockhash = self.generate(self.nodes[0], 6)[0]
vout = find_output(self.nodes[0], txid, 7, blockhash=blockhash)
psbt = self.nodes[1].createpsbt([{"txid":txid, "vout":vout}], {self.nodes[0].getnewaddress("", "p2sh-segwit"):Decimal('6.999')})
self.generate(self.nodes[0], 6)[0]
psbt = self.nodes[1].createpsbt([utxo], {self.nodes[0].getnewaddress("", "p2sh-segwit"):Decimal('6.999')})
analyzed = self.nodes[0].analyzepsbt(psbt)
assert not analyzed['inputs'][0]['has_utxo'] and not analyzed['inputs'][0]['is_final'] and analyzed['inputs'][0]['next'] == 'updater' and analyzed['next'] == 'updater'

Expand Down Expand Up @@ -872,9 +865,8 @@ def test_psbt_input_keys(psbt_input, keys):

self.log.info("Test that walletprocesspsbt both updates and signs a non-updated psbt containing Taproot inputs")
addr = self.nodes[0].getnewaddress("", "bech32m")
txid = self.nodes[0].sendtoaddress(addr, 1)
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}])
utxo = self.create_outpoints(self.nodes[0], outputs=[{addr: 1}])[0]
psbt = self.nodes[0].createpsbt([utxo], [{self.nodes[0].getnewaddress(): 0.9999}])
signed = self.nodes[0].walletprocesspsbt(psbt)
rawtx = signed["hex"]
self.nodes[0].sendrawtransaction(rawtx)
Expand Down Expand Up @@ -962,11 +954,10 @@ def test_psbt_input_keys(psbt_input, keys):

descriptor = descsum_create(f"wpkh({key})")

txid = self.nodes[0].sendtoaddress(address, 1)
utxo = self.create_outpoints(self.nodes[0], outputs=[{address: 1}])[0]
self.sync_all()
vout = find_output(self.nodes[0], txid, 1)

psbt = self.nodes[2].createpsbt([{"txid": txid, "vout": vout}], {self.nodes[0].getnewaddress(): 0.99999})
psbt = self.nodes[2].createpsbt([utxo], {self.nodes[0].getnewaddress(): 0.99999})
decoded = self.nodes[2].decodepsbt(psbt)
test_psbt_input_keys(decoded['inputs'][0], [])

Expand Down
17 changes: 17 additions & 0 deletions test/functional/test_framework/test_framework.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
PortSeed,
assert_equal,
check_json_precision,
find_vout_for_address,
get_datadir_path,
initialize_datadir,
p2p_port,
Expand Down Expand Up @@ -697,6 +698,22 @@ def generatetodescriptor(self, generator, *args, sync_fun=None, **kwargs):
sync_fun() if sync_fun else self.sync_all()
return blocks

def create_outpoints(self, node, *, outputs):
"""Send funds to a given list of `{address: amount}` targets using the bitcoind
wallet and return the corresponding outpoints as a list of dictionaries
`[{"txid": txid, "vout": vout1}, {"txid": txid, "vout": vout2}, ...]`.
The result can be used to specify inputs for RPCs like `createrawtransaction`,
`createpsbt`, `lockunspent` etc."""
assert all(len(output.keys()) == 1 for output in outputs)
send_res = node.send(outputs)
assert send_res["complete"]
utxos = []
for output in outputs:
address = list(output.keys())[0]
vout = find_vout_for_address(node, send_res["txid"], address)
utxos.append({"txid": send_res["txid"], "vout": vout})
return utxos

def sync_blocks(self, nodes=None, wait=1, timeout=60):
"""
Wait until everybody has the same tip.
Expand Down
12 changes: 0 additions & 12 deletions test/functional/test_framework/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -490,18 +490,6 @@ def check_node_connections(*, node, num_in, num_out):
#############################


def find_output(node, txid, amount, *, blockhash=None):
"""
Return index to output of txid with value amount
Raises exception if there is none.
"""
txdata = node.getrawtransaction(txid, 1, blockhash)
for i in range(len(txdata["vout"])):
if txdata["vout"][i]["value"] == amount:
return i
raise RuntimeError("find_output txid %s : %s not found" % (txid, str(amount)))


# Create large OP_RETURN txouts that can be appended to a transaction
# to make it large (helper for constructing large transactions). The
# total serialized size of the txouts is about 66k vbytes.
Expand Down
6 changes: 2 additions & 4 deletions test/functional/wallet_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
assert_equal,
assert_fee_amount,
assert_raises_rpc_error,
find_vout_for_address,
)
from test_framework.wallet_util import test_address
from test_framework.wallet import MiniWallet
Expand Down Expand Up @@ -471,10 +470,9 @@ def run_test(self):
# Import address and private key to check correct behavior of spendable unspents
# 1. Send some coins to generate new UTXO
address_to_import = self.nodes[2].getnewaddress()
txid = self.nodes[0].sendtoaddress(address_to_import, 1)
utxo = self.create_outpoints(self.nodes[0], outputs=[{address_to_import: 1}])[0]
self.sync_mempools(self.nodes[0:3])
vout = find_vout_for_address(self.nodes[2], txid, address_to_import)
self.nodes[2].lockunspent(False, [{"txid": txid, "vout": vout}])
self.nodes[2].lockunspent(False, [utxo])
self.generate(self.nodes[0], 1, sync_fun=lambda: self.sync_all(self.nodes[0:3]))

self.log.info("Test sendtoaddress with fee_rate param (explicit fee rate in sat/vB)")
Expand Down
37 changes: 15 additions & 22 deletions test/functional/wallet_fundrawtransaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
assert_greater_than_or_equal,
assert_raises_rpc_error,
count_bytes,
find_vout_for_address,
get_fee,
)
from test_framework.wallet_util import generate_keypair, WalletUnlock
Expand Down Expand Up @@ -85,14 +84,13 @@ def unlock_utxos(self, wallet):
Unlock all UTXOs except the watchonly one
"""
to_keep = []
if self.watchonly_txid is not None and self.watchonly_vout is not None:
to_keep.append({"txid": self.watchonly_txid, "vout": self.watchonly_vout})
if self.watchonly_utxo is not None:
to_keep.append(self.watchonly_utxo)
wallet.lockunspent(True)
wallet.lockunspent(False, to_keep)

def run_test(self):
self.watchonly_txid = None
self.watchonly_vout = None
self.watchonly_utxo = None
self.log.info("Connect nodes, set fees, generate blocks, and sync")
self.min_relay_tx_fee = self.nodes[0].getnetworkinfo()['relayfee']
# This test is not meant to test fee estimation and we'd like
Expand Down Expand Up @@ -163,11 +161,10 @@ def test_change_position(self):
watchonly_pubkey = self.nodes[0].getaddressinfo(watchonly_address)["pubkey"]
self.watchonly_amount = Decimal(200)
wwatch.importpubkey(watchonly_pubkey, "", True)
self.watchonly_txid = self.nodes[0].sendtoaddress(watchonly_address, self.watchonly_amount)
self.watchonly_utxo = self.create_outpoints(self.nodes[0], outputs=[{watchonly_address: self.watchonly_amount}])[0]

# Lock UTXO so nodes[0] doesn't accidentally spend it
self.watchonly_vout = find_vout_for_address(self.nodes[0], self.watchonly_txid, watchonly_address)
self.nodes[0].lockunspent(False, [{"txid": self.watchonly_txid, "vout": self.watchonly_vout}])
self.nodes[0].lockunspent(False, [self.watchonly_utxo])

self.nodes[0].sendtoaddress(self.nodes[3].get_wallet_rpc(self.default_wallet_name).getnewaddress(), self.watchonly_amount / 10)

Expand Down Expand Up @@ -738,7 +735,7 @@ def test_watchonly(self):
result = wwatch.fundrawtransaction(rawtx, True)
res_dec = self.nodes[0].decoderawtransaction(result["hex"])
assert_equal(len(res_dec["vin"]), 1)
assert_equal(res_dec["vin"][0]["txid"], self.watchonly_txid)
assert_equal(res_dec["vin"][0]["txid"], self.watchonly_utxo['txid'])

assert "fee" in result.keys()
assert_greater_than(result["changepos"], -1)
Expand All @@ -758,7 +755,7 @@ def test_all_watched_funds(self):
result = wwatch.fundrawtransaction(rawtx, includeWatching=True, changeAddress=w3.getrawchangeaddress(), subtractFeeFromOutputs=[0])
res_dec = self.nodes[0].decoderawtransaction(result["hex"])
assert_equal(len(res_dec["vin"]), 1)
assert res_dec["vin"][0]["txid"] == self.watchonly_txid
assert res_dec["vin"][0]["txid"] == self.watchonly_utxo['txid']

assert_greater_than(result["fee"], 0)
assert_equal(result["changepos"], -1)
Expand Down Expand Up @@ -970,10 +967,9 @@ def test_subtract_fee_with_presets(self):
self.log.info("Test fundrawtxn subtract fee from outputs with preset inputs that are sufficient")

addr = self.nodes[0].getnewaddress()
txid = self.nodes[0].sendtoaddress(addr, 10)
vout = find_vout_for_address(self.nodes[0], txid, addr)
utxo = self.create_outpoints(self.nodes[0], outputs=[{addr: 10}])[0]

rawtx = self.nodes[0].createrawtransaction([{'txid': txid, 'vout': vout}], [{self.nodes[0].getnewaddress(): 5}])
rawtx = self.nodes[0].createrawtransaction([utxo], [{self.nodes[0].getnewaddress(): 5}])
fundedtx = self.nodes[0].fundrawtransaction(rawtx, subtractFeeFromOutputs=[0])
signedtx = self.nodes[0].signrawtransactionwithwallet(fundedtx['hex'])
self.nodes[0].sendrawtransaction(signedtx['hex'])
Expand Down Expand Up @@ -1264,22 +1260,20 @@ def test_weight_calculation(self):

addr = wallet.getnewaddress(address_type="bech32")
ext_addr = self.nodes[0].getnewaddress(address_type="bech32")
txid = self.nodes[0].send([{addr: 5}, {ext_addr: 5}])["txid"]
vout = find_vout_for_address(self.nodes[0], txid, addr)
ext_vout = find_vout_for_address(self.nodes[0], txid, ext_addr)
utxo, ext_utxo = self.create_outpoints(self.nodes[0], outputs=[{addr: 5}, {ext_addr: 5}])

self.nodes[0].sendtoaddress(wallet.getnewaddress(address_type="bech32"), 5)
self.generate(self.nodes[0], 1)

rawtx = wallet.createrawtransaction([{'txid': txid, 'vout': vout}], [{self.nodes[0].getnewaddress(address_type="bech32"): 8}])
rawtx = wallet.createrawtransaction([utxo], [{self.nodes[0].getnewaddress(address_type="bech32"): 8}])
fundedtx = wallet.fundrawtransaction(rawtx, fee_rate=10, change_type="bech32")
# with 71-byte signatures we should expect following tx size
# tx overhead (10) + 2 inputs (41 each) + 2 p2wpkh (31 each) + (segwit marker and flag (2) + 2 p2wpkh 71 byte sig witnesses (107 each)) / witness scaling factor (4)
tx_size = ceil(10 + 41*2 + 31*2 + (2 + 107*2)/4)
assert_equal(fundedtx['fee'] * COIN, tx_size * 10)

# Using the other output should have 72 byte sigs
rawtx = wallet.createrawtransaction([{'txid': txid, 'vout': ext_vout}], [{self.nodes[0].getnewaddress(): 13}])
rawtx = wallet.createrawtransaction([ext_utxo], [{self.nodes[0].getnewaddress(): 13}])
ext_desc = self.nodes[0].getaddressinfo(ext_addr)["desc"]
fundedtx = wallet.fundrawtransaction(rawtx, fee_rate=10, change_type="bech32", solving_data={"descriptors": [ext_desc]})
# tx overhead (10) + 3 inputs (41 each) + 2 p2wpkh(31 each) + (segwit marker and flag (2) + 2 p2wpkh 71 bytes sig witnesses (107 each) + p2wpkh 72 byte sig witness (108)) / witness scaling factor (4)
Expand All @@ -1298,10 +1292,9 @@ def test_include_unsafe(self):
addr = wallet.getnewaddress()
inputs = []
for i in range(0, 2):
txid = self.nodes[2].sendtoaddress(addr, 5)
self.sync_mempools()
vout = find_vout_for_address(wallet, txid, addr)
inputs.append((txid, vout))
utxo = self.create_outpoints(self.nodes[2], outputs=[{addr: 5}])[0]
inputs.append((utxo['txid'], utxo['vout']))
self.sync_mempools()

# Unsafe inputs are ignored by default.
rawtx = wallet.createrawtransaction([], [{self.nodes[2].getnewaddress(): 7.5}])
Expand Down
11 changes: 4 additions & 7 deletions test/functional/wallet_importdescriptors.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
from test_framework.util import (
assert_equal,
assert_raises_rpc_error,
find_vout_for_address,
)
from test_framework.wallet_util import (
get_generate_key,
Expand Down Expand Up @@ -493,12 +492,10 @@ def run_test(self):
assert_equal(wmulti_pub.getwalletinfo()['keypoolsize'], 999)

# generate some utxos for next tests
txid = w0.sendtoaddress(addr, 10)
vout = find_vout_for_address(self.nodes[0], txid, addr)
utxo = self.create_outpoints(w0, outputs=[{addr: 10}])[0]

addr2 = wmulti_pub.getnewaddress('', 'bech32')
txid2 = w0.sendtoaddress(addr2, 10)
vout2 = find_vout_for_address(self.nodes[0], txid2, addr2)
utxo2 = self.create_outpoints(w0, outputs=[{addr2: 10}])[0]

self.generate(self.nodes[0], 6)
assert_equal(wmulti_pub.getbalance(), wmulti_priv.getbalance())
Expand Down Expand Up @@ -554,7 +551,7 @@ def run_test(self):
assert_equal(res[1]['success'], True)
assert_equal(res[1]['warnings'][0], 'Not all private keys provided. Some wallet functionality may return unexpected errors')

rawtx = self.nodes[1].createrawtransaction([{'txid': txid, 'vout': vout}], {w0.getnewaddress(): 9.999})
rawtx = self.nodes[1].createrawtransaction([utxo], {w0.getnewaddress(): 9.999})
tx_signed_1 = wmulti_priv1.signrawtransactionwithwallet(rawtx)
assert_equal(tx_signed_1['complete'], False)
tx_signed_2 = wmulti_priv2.signrawtransactionwithwallet(tx_signed_1['hex'])
Expand Down Expand Up @@ -648,7 +645,7 @@ def run_test(self):
}])
assert_equal(res[0]['success'], True)

rawtx = self.nodes[1].createrawtransaction([{'txid': txid2, 'vout': vout2}], {w0.getnewaddress(): 9.999})
rawtx = self.nodes[1].createrawtransaction([utxo2], {w0.getnewaddress(): 9.999})
tx = wmulti_priv3.signrawtransactionwithwallet(rawtx)
assert_equal(tx['complete'], True)
self.nodes[1].sendrawtransaction(tx['hex'])
Expand Down
Loading

0 comments on commit 2a349f9

Please sign in to comment.