diff --git a/tests/test_framework/revault_network.py b/tests/test_framework/revault_network.py deleted file mode 100644 index 0a840f85..00000000 --- a/tests/test_framework/revault_network.py +++ /dev/null @@ -1,811 +0,0 @@ -import bip32 -import logging -import os -import random - -from ephemeral_port_reserve import reserve -from nacl.public import PrivateKey as Curve25519Private -from test_framework import serializations -from test_framework.bitcoind import BitcoindRpcProxy -from test_framework.coordinatord import Coordinatord -from test_framework.cosignerd import Cosignerd -from test_framework.miradord import Miradord -from test_framework.revaultd import ManagerRevaultd, StakeholderRevaultd, StkManRevaultd -from test_framework.utils import ( - get_descriptors, - get_participants, - finalize_input, - wait_for, - TIMEOUT, - WT_PLUGINS_DIR, -) - - -class RevaultNetwork: - # FIXME: we use a single bitcoind for all the wallets because it's much - # more efficient. Eventually, we may have to test with separate ones. - def __init__( - self, - root_dir, - bitcoind, - executor, - postgres_user, - postgres_pass, - postgres_host="localhost", - ): - self.root_dir = root_dir - self.bitcoind = bitcoind - self.daemons = [] - - self.executor = executor - - self.postgres_user = postgres_user - self.postgres_pass = postgres_pass - self.postgres_host = postgres_host - self.coordinator_port = reserve() - - self.stk_wallets = [] - self.stkman_wallets = [] - self.man_wallets = [] - - self.csv = None - self.emergency_address = None - - self.bitcoind_proxy = None - - def deploy( - self, - n_stakeholders, - n_managers, - n_stkmanagers=0, - csv=None, - managers_threshold=None, - with_cosigs=True, - with_watchtowers=True, - with_cpfp=True, - bitcoind_rpc_mocks=[], - ): - """ - Deploy a revault setup with {n_stakeholders} stakeholders, {n_managers} - managers. - """ - # They didn't provide it, defaults to n_managers - # PS: No I can't just managers_threshold=n_managers in the method's signature :( - if managers_threshold == None: - managers_threshold = n_managers + n_stkmanagers - - assert n_stakeholders + n_stkmanagers >= 2, "Not enough stakeholders" - assert n_managers + n_stkmanagers >= 1, "Not enough managers" - assert managers_threshold <= n_managers + n_stkmanagers, "Invalid threshold" - - # Connection info to bitcoind. Change the port depending on whether we are proxying - # the daemons' requests. - bitcoind_cookie = os.path.join(self.bitcoind.bitcoin_dir, "regtest", ".cookie") - if len(bitcoind_rpc_mocks) > 0: - self.bitcoind_proxy = BitcoindRpcProxy( - self.bitcoind.rpcport, bitcoind_cookie, bitcoind_rpc_mocks - ) - bitcoind_rpcport = self.bitcoind_proxy.rpcport - else: - bitcoind_rpcport = self.bitcoind.rpcport - - ( - stkonly_keychains, - stkonly_cosig_keychains, - manonly_keychains, - stkman_stk_keychains, - stkman_cosig_keychains, - stkman_man_keychains, - ) = get_participants(n_stakeholders, n_managers, n_stkmanagers, with_cosigs) - stks_keychains = stkonly_keychains + stkman_stk_keychains - cosigs_keychains = stkonly_cosig_keychains + stkman_cosig_keychains - mans_keychains = manonly_keychains + stkman_man_keychains - - if csv is None: - # Not more than 6 months - csv = random.randint(1, 26784) - self.csv = csv - - man_cpfp_seeds = [os.urandom(32) for _ in range(len(manonly_keychains))] - man_cpfp_privs = [ - bip32.BIP32.from_seed(seed, network="test") for seed in man_cpfp_seeds - ] - stkman_cpfp_seeds = [os.urandom(32) for _ in range(len(stkman_man_keychains))] - stkman_cpfp_privs = [ - bip32.BIP32.from_seed(seed, network="test") for seed in stkman_cpfp_seeds - ] - cpfp_xpubs = [c.get_xpub() for c in man_cpfp_privs + stkman_cpfp_privs] - stks_xpubs = [stk.get_xpub() for stk in stks_keychains] - cosigs_keys = [cosig.get_static_key().hex() for cosig in cosigs_keychains] - mans_xpubs = [man.get_xpub() for man in mans_keychains] - (self.deposit_desc, self.unvault_desc, self.cpfp_desc) = get_descriptors( - stks_xpubs, cosigs_keys, mans_xpubs, managers_threshold, cpfp_xpubs, csv - ) - # Generate a dummy 2of2 to be used as our Emergency address - desc = "wsh(multi(2,cRE7qAArQYnFQK7S1gXFTArFT4UWvh8J2v2EUajRWXbWFvRzxoeF,\ - cTzcgRCmHNqUqZuZgvCPLUDXXrQSoVQpZiXQZWQzsLEytcTr6iXi))" - checksum = self.bitcoind.rpc.getdescriptorinfo(desc)["checksum"] - desc = f"{desc}#{checksum}" - self.emergency_address = self.bitcoind.rpc.deriveaddresses(desc)[0] - desc_import = self.bitcoind.rpc.importdescriptors( - [ - { - "desc": desc, - "timestamp": "now", - "label": "revault-emergency", - } - ] - ) - if not desc_import[0]["success"]: - raise Exception(desc_import) - - # FIXME: this is getting dirty.. We should re-centralize information - # about each participant in specified data structures - stkonly_cosigners_ports = [] - stkman_cosigners_ports = [] - - # The Noise keys are interdependant, so generate everything in advance - # to avoid roundtrips - coordinator_noisepriv = os.urandom(32) - coordinator_noisepub = bytes( - Curve25519Private(coordinator_noisepriv).public_key - ) - - (stkonly_noiseprivs, stkonly_noisepubs) = ([], []) - (stkonly_wt_noiseprivs, stkonly_wt_noisepubs) = ([], []) - (stkonly_cosig_noiseprivs, stkonly_cosig_noisepubs) = ([], []) - for i in range(len(stkonly_keychains)): - stkonly_noiseprivs.append(os.urandom(32)) - stkonly_noisepubs.append( - bytes(Curve25519Private(stkonly_noiseprivs[i]).public_key) - ) - if with_cosigs: - stkonly_cosig_noiseprivs.append(os.urandom(32)) - stkonly_cosig_noisepubs.append( - bytes(Curve25519Private(stkonly_cosig_noiseprivs[i]).public_key) - ) - # Unused yet - stkonly_wt_noiseprivs.append(os.urandom(32)) - stkonly_wt_noisepubs.append( - bytes(Curve25519Private(stkonly_wt_noiseprivs[i]).public_key) - ) - - (stkman_noiseprivs, stkman_noisepubs) = ([], []) - (stkman_wt_noiseprivs, stkman_wt_noisepubs) = ([], []) - (stkman_cosig_noiseprivs, stkman_cosig_noisepubs) = ([], []) - for i in range(len(stkman_stk_keychains)): - stkman_noiseprivs.append(os.urandom(32)) - stkman_noisepubs.append( - bytes(Curve25519Private(stkman_noiseprivs[i]).public_key) - ) - if with_cosigs: - stkman_cosig_noiseprivs.append(os.urandom(32)) - stkman_cosig_noisepubs.append( - bytes(Curve25519Private(stkman_cosig_noiseprivs[i]).public_key) - ) - # Unused yet - stkman_wt_noiseprivs.append(os.urandom(32)) - stkman_wt_noisepubs.append( - bytes(Curve25519Private(stkman_wt_noiseprivs[i]).public_key) - ) - - (man_noiseprivs, man_noisepubs) = ([], []) - for i in range(len(manonly_keychains)): - man_noiseprivs.append(os.urandom(32)) - man_noisepubs.append(bytes(Curve25519Private(man_noiseprivs[i]).public_key)) - - logging.debug( - f"Using Noise pubkeys:\n- Stakeholders: {stkonly_noisepubs + stkman_noisepubs}" - f" (of which {len(stkman_noisepubs)} are also managers)" - f"\n- Managers: {man_noisepubs}\n- Watchtowers:" - f"{stkonly_wt_noisepubs + stkman_wt_noisepubs}\n" - ) - - # Spin up the "Sync Server" - coord_datadir = os.path.join(self.root_dir, "coordinatord") - os.makedirs(coord_datadir, exist_ok=True) - coordinatord = Coordinatord( - coord_datadir, - coordinator_noisepriv, - man_noisepubs + stkman_noisepubs, - stkonly_noisepubs + stkman_noisepubs, - stkonly_wt_noisepubs + stkman_wt_noisepubs, - self.coordinator_port, - bitcoind_rpcport, - bitcoind_cookie, - self.postgres_user, - self.postgres_pass, - self.postgres_host, - ) - coordinatord.start() - self.daemons.append(coordinatord) - - cosigners_info = [] - for (i, noisepub) in enumerate(stkonly_cosig_noisepubs): - stkonly_cosigners_ports.append(reserve()) - cosigners_info.append( - { - "host": f"127.0.0.1:{stkonly_cosigners_ports[i]}", - "noise_key": noisepub, - } - ) - for (i, noisepub) in enumerate(stkman_cosig_noisepubs): - stkman_cosigners_ports.append(reserve()) - cosigners_info.append( - { - "host": f"127.0.0.1:{stkman_cosigners_ports[i]}", - "noise_key": noisepub, - } - ) - - # Start daemons in parallel, as it takes a few seconds for each - start_jobs = [] - # By default the watchtower should not revault anything - default_wt_plugin = { - "path": os.path.join(WT_PLUGINS_DIR, "revault_nothing.py"), - "conf": {}, - } - - # Spin up the stakeholders wallets and their cosigning servers - for i, stk in enumerate(stkonly_keychains): - if with_watchtowers: - datadir = os.path.join(self.root_dir, f"miradord-{i}") - os.makedirs(datadir) - wt_listen_port = reserve() - miradord = Miradord( - datadir, - str(self.deposit_desc), - str(self.unvault_desc), - str(self.cpfp_desc), - self.emergency_address, - wt_listen_port, - stkonly_wt_noiseprivs[i], - stkonly_noisepubs[i].hex(), - coordinator_noisepub.hex(), - self.coordinator_port, - bitcoind_rpcport, - bitcoind_cookie, - plugins=[default_wt_plugin], - ) - start_jobs.append(self.executor.submit(miradord.start)) - self.daemons.append(miradord) - - datadir = os.path.join(self.root_dir, f"revaultd-stk-{i}") - os.makedirs(datadir, exist_ok=True) - stk_config = { - "keychain": stk, - "watchtowers": [ - { - "host": f"127.0.0.1:{wt_listen_port}", - "noise_key": stkonly_wt_noisepubs[i].hex(), - } - ] - if with_watchtowers - else [], - "emergency_address": self.emergency_address, - } - - revaultd = StakeholderRevaultd( - datadir, - str(self.deposit_desc), - str(self.unvault_desc), - str(self.cpfp_desc), - stkonly_noiseprivs[i], - coordinator_noisepub.hex(), - self.coordinator_port, - bitcoind_rpcport, - bitcoind_cookie, - stk_config, - wt_process=miradord if with_watchtowers else None, - ) - start_jobs.append(self.executor.submit(revaultd.start)) - self.stk_wallets.append(revaultd) - - if with_cosigs: - datadir = os.path.join(self.root_dir, f"cosignerd-stk-{i}") - os.makedirs(datadir, exist_ok=True) - - cosignerd = Cosignerd( - datadir, - stkonly_cosig_noiseprivs[i], - stkonly_cosig_keychains[i].get_bitcoin_priv(), - stkonly_cosigners_ports[i], - man_noisepubs + stkman_noisepubs, - ) - start_jobs.append(self.executor.submit(cosignerd.start)) - self.daemons.append(cosignerd) - - # Spin up the stakeholder-managers wallets and their cosigning servers - for i, stkman in enumerate(stkman_stk_keychains): - if with_watchtowers: - datadir = os.path.join(self.root_dir, f"miradord-stkman-{i}") - os.makedirs(datadir) - wt_listen_port = reserve() - miradord = Miradord( - datadir, - str(self.deposit_desc), - str(self.unvault_desc), - str(self.cpfp_desc), - self.emergency_address, - wt_listen_port, - stkman_wt_noiseprivs[i], - stkman_noisepubs[i].hex(), - coordinator_noisepub.hex(), - self.coordinator_port, - bitcoind_rpcport, - bitcoind_cookie, - plugins=[default_wt_plugin], - ) - start_jobs.append(self.executor.submit(miradord.start)) - self.daemons.append(miradord) - - datadir = os.path.join(self.root_dir, f"revaultd-stkman-{i}") - os.makedirs(datadir, exist_ok=True) - stk_config = { - "keychain": stkman, - "watchtowers": [ - { - "host": f"127.0.0.1:{wt_listen_port}", - "noise_key": stkman_wt_noisepubs[i].hex(), - } - ] - if with_watchtowers - else [], - "emergency_address": self.emergency_address, - } - man_config = { - "keychain": stkman_man_keychains[i], - "cosigners": cosigners_info, - } - - revaultd = StkManRevaultd( - datadir, - str(self.deposit_desc), - str(self.unvault_desc), - str(self.cpfp_desc), - stkman_noiseprivs[i], - coordinator_noisepub.hex(), - self.coordinator_port, - bitcoind_rpcport, - bitcoind_cookie, - stk_config, - man_config, - wt_process=miradord if with_watchtowers else None, - cpfp_seed=stkman_cpfp_seeds[i] if with_cpfp else None, - ) - start_jobs.append(self.executor.submit(revaultd.start)) - self.stkman_wallets.append(revaultd) - - if with_cosigs: - datadir = os.path.join(self.root_dir, f"cosignerd-stkman-{i}") - os.makedirs(datadir, exist_ok=True) - - cosignerd = Cosignerd( - datadir, - stkman_cosig_noiseprivs[i], - stkman_cosig_keychains[i].get_bitcoin_priv(), - stkman_cosigners_ports[i], - man_noisepubs + stkman_noisepubs, - ) - start_jobs.append(self.executor.submit(cosignerd.start)) - self.daemons.append(cosignerd) - - # Spin up the managers (only) wallets - for i, man in enumerate(manonly_keychains): - datadir = os.path.join(self.root_dir, f"revaultd-man-{i}") - os.makedirs(datadir, exist_ok=True) - - man_config = {"keychain": man, "cosigners": cosigners_info} - daemon = ManagerRevaultd( - datadir, - str(self.deposit_desc), - str(self.unvault_desc), - str(self.cpfp_desc), - man_noiseprivs[i], - coordinator_noisepub.hex(), - self.coordinator_port, - bitcoind_rpcport, - bitcoind_cookie, - man_config, - cpfp_seed=man_cpfp_seeds[i] if with_cpfp else None, - ) - start_jobs.append(self.executor.submit(daemon.start)) - self.man_wallets.append(daemon) - - for j in start_jobs: - j.result(TIMEOUT) - - self.daemons += self.stk_wallets + self.stkman_wallets + self.man_wallets - - def mans(self): - return self.stkman_wallets + self.man_wallets - - def stks(self): - return self.stkman_wallets + self.stk_wallets - - def participants(self): - return self.stkman_wallets + self.stk_wallets + self.man_wallets - - def man(self, n): - """Get the {n}th manager (including the stakeholder-managers first)""" - mans = self.stkman_wallets + self.man_wallets - return mans[n] - - def stk(self, n): - """Get the {n}th stakeholder (including the stakeholder-managers first)""" - stks = self.stkman_wallets + self.stk_wallets - return stks[n] - - def signed_unvault_psbt(self, deposit, derivation_index): - """Get the fully-signed Unvault transaction for this deposit. - - This will raise if we don't have all the signatures. - """ - psbt_str = self.stks()[0].rpc.listpresignedtransactions([deposit])[ - "presigned_transactions" - ][0]["unvault"] - psbt = serializations.PSBT() - psbt.deserialize(psbt_str) - - finalize_input(self.deposit_desc, psbt.inputs[0], derivation_index) - psbt.tx.wit.vtxinwit.append(psbt.inputs[0].final_script_witness) - return psbt.tx.serialize_with_witness().hex() - - def signed_cancel_psbt(self, deposit, derivation_index): - """Get the fully-signed Cancel transaction for this deposit. - - This picks the lowest feerate version. - This will raise if we don't have all the signatures. - """ - psbt_str = self.stks()[0].rpc.listpresignedtransactions([deposit])[ - "presigned_transactions" - ][0]["cancel"][0] - psbt = serializations.PSBT() - psbt.deserialize(psbt_str) - - finalize_input(self.unvault_desc, psbt.inputs[0], derivation_index) - psbt.tx.wit.vtxinwit.append(psbt.inputs[0].final_script_witness) - return psbt.tx.serialize_with_witness().hex() - - def get_vault(self, address): - """Get a vault entry by outpoint or by address""" - for v in self.man(0).rpc.listvaults()["vaults"]: - if v["address"] == address: - return v - - def fund(self, amount=None): - """Deposit coins into the architectures, by paying to the deposit - descriptor and getting the tx 6 blocks confirmations.""" - assert ( - len(self.man_wallets + self.stkman_wallets) > 0 - ), "You must have deploy()ed first" - - man = self.man(0) - - if amount is None: - amount = 49.9999 - - addr = man.rpc.getdepositaddress()["address"] - txid = self.bitcoind.rpc.sendtoaddress(addr, amount) - man.wait_for_log(f"Got a new unconfirmed deposit at {txid}") - self.bitcoind.generate_block(6, wait_for_mempool=txid) - man.wait_for_log(f"Vault at {txid}.* is now confirmed") - - vaults = man.rpc.listvaults(["funded"])["vaults"] - for v in vaults: - if v["txid"] == txid: - for w in self.man_wallets + self.stk_wallets: - w.wait_for_deposits([f"{txid}:{v['vout']}"]) - return v - - raise Exception(f"Vault created by '{txid}' got in logs but not in listvaults?") - - def fundmany(self, amounts=[]): - """Deposit coins into the architectures in a single transaction""" - assert ( - len(self.man_wallets + self.stkman_wallets) > 0 - ), "You must have deploy()ed first" - assert len(amounts) > 0, "You must provide at least an amount!" - - man = self.man(0) - - curr_index = 0 - vaults = man.rpc.listvaults()["vaults"] - for v in vaults: - if v["derivation_index"] > curr_index: - curr_index = v["derivation_index"] - - indexes = list(range(curr_index + 1, curr_index + 1 + len(amounts))) - amounts_sendmany = {} - for i, amount in enumerate(amounts): - amounts_sendmany[man.rpc.getdepositaddress(indexes[i])["address"]] = amount - - txid = self.bitcoind.rpc.sendmany("", amounts_sendmany) - man.wait_for_logs( - [f"Got a new unconfirmed deposit at {txid}" for _ in range(len(amounts))], - timeout=TIMEOUT * max(1, len(amounts) / 10), - ) - self.bitcoind.generate_block(6, wait_for_mempool=txid) - man.wait_for_logs( - [f"Vault at {txid}.* is now confirmed" for _ in range(len(amounts))], - timeout=TIMEOUT * max(1, len(amounts) / 10), - ) - - # Return the vaults we created - all_vaults = man.rpc.listvaults(["funded"])["vaults"] - created_vaults = [] - for v in all_vaults: - if v["txid"] == txid: - created_vaults.append(v) - assert len(created_vaults) == len(amounts) - - return created_vaults - - def secure_vault(self, vault): - """Make all stakeholders share signatures for all revocation txs""" - deposit = f"{vault['txid']}:{vault['vout']}" - for stk in self.stks(): - stk.wait_for_deposits([deposit]) - psbts = stk.rpc.getrevocationtxs(deposit) - cancel_psbts = [ - stk.stk_keychain.sign_revocation_psbt(c, vault["derivation_index"]) - for c in psbts["cancel_txs"] - ] - emer_psbt = stk.stk_keychain.sign_revocation_psbt( - psbts["emergency_tx"], vault["derivation_index"] - ) - unemer_psbt = stk.stk_keychain.sign_revocation_psbt( - psbts["emergency_unvault_tx"], vault["derivation_index"] - ) - stk.rpc.revocationtxs(deposit, cancel_psbts, emer_psbt, unemer_psbt) - for w in self.participants(): - w.wait_for_secured_vaults([deposit]) - - def secure_vaults(self, vaults): - """Secure all these vaults, concurrently.""" - sec_jobs = [] - for v in vaults: - sec_jobs.append(self.executor.submit(self.secure_vault, v)) - for j in sec_jobs: - j.result(TIMEOUT) - - def activate_vault(self, vault): - """Make all stakeholders share signatures for the unvault tx""" - deposit = f"{vault['txid']}:{vault['vout']}" - for stk in self.stks(): - stk.wait_for_secured_vaults([deposit]) - unvault_psbt = stk.rpc.getunvaulttx(deposit)["unvault_tx"] - unvault_psbt = stk.stk_keychain.sign_unvault_psbt( - unvault_psbt, vault["derivation_index"] - ) - stk.rpc.unvaulttx(deposit, unvault_psbt) - for w in self.participants(): - w.wait_for_active_vaults([deposit]) - - def activate_fresh_vaults(self, vaults): - """Secure then activate all these vaults, concurrently.""" - # TODO: i'm sure we don't even need to wait for all sec jobs to be complete - # before starting the activate_vault futures, given a high enough TIMEOUT. - self.secure_vaults(vaults) - - act_jobs = [] - for v in vaults: - act_jobs.append(self.executor.submit(self.activate_vault, v)) - for j in act_jobs: - j.result(TIMEOUT) - - def broadcast_unvaults(self, vaults, destinations, feerate, priority=False): - """ - Broadcast the Unvault transactions for these {vaults}, advertizing a - Spend tx spending to these {destinations} (mapping of addresses to - amounts) - """ - man = self.man(0) - deposits = [] - deriv_indexes = [] - for v in vaults: - deposits.append(f"{v['txid']}:{v['vout']}") - deriv_indexes.append(v["derivation_index"]) - man.wait_for_active_vaults(deposits) - - spend_tx = man.rpc.getspendtx(deposits, destinations, feerate)["spend_tx"][ - "psbt" - ] - for man in self.mans(): - spend_tx = man.man_keychain.sign_spend_psbt(spend_tx, deriv_indexes) - man.rpc.updatespendtx(spend_tx) - - spend_psbt = serializations.PSBT() - spend_psbt.deserialize(spend_tx) - spend_psbt.tx.calc_sha256() - man.rpc.setspendtx(spend_psbt.tx.hash, priority) - return spend_psbt - - def unvault_vaults(self, vaults, destinations, feerate, priority=False): - """ - Unvault these {vaults}, advertizing a Spend tx spending to these {destinations} - (mapping of addresses to amounts) - """ - spend_psbt = self.broadcast_unvaults(vaults, destinations, feerate, priority) - deposits = [f"{v['txid']}:{v['vout']}" for v in vaults] - self.bitcoind.generate_block(1, wait_for_mempool=len(deposits)) - for w in self.participants(): - wait_for( - lambda: len(w.rpc.listvaults(["unvaulted"], deposits)["vaults"]) - == len(deposits) - ) - return spend_psbt - - def spend_vaults_unconfirmed(self, vaults, destinations, feerate, priority=False): - """ - Spend these {vaults} to these {destinations} (mapping of addresses to amounts), not - confirming the Spend transaction. - Make sure to call this only with revault deployment with a low (<500) CSV, or you'll encounter - an ugly timeout from bitcoinlib. - - :return: the list of spent deposits along with the Spend PSBT. - """ - assert len(vaults) > 0 - man = self.man(0) - deposits = [] - deriv_indexes = [] - for v in vaults: - deposits.append(f"{v['txid']}:{v['vout']}") - deriv_indexes.append(v["derivation_index"]) - - for man in self.mans(): - man.wait_for_active_vaults(deposits) - - spend_tx = man.rpc.getspendtx(deposits, destinations, feerate)["spend_tx"][ - "psbt" - ] - for man in self.mans(): - spend_tx = man.man_keychain.sign_spend_psbt(spend_tx, deriv_indexes) - man.rpc.updatespendtx(spend_tx) - - spend_psbt = serializations.PSBT() - spend_psbt.deserialize(spend_tx) - spend_psbt.tx.calc_sha256() - man.rpc.setspendtx(spend_psbt.tx.hash, priority) - - self.bitcoind.generate_block(1, wait_for_mempool=len(deposits)) - self.bitcoind.generate_block(self.csv) - man.wait_for_log( - f"Succesfully broadcasted Spend tx '{spend_psbt.tx.hash}'", - ) - for w in self.participants(): - wait_for( - lambda: len(w.rpc.listvaults(["spending"], deposits)["vaults"]) - == len(deposits) - ) - - return deposits, spend_psbt - - def spend_vaults(self, vaults, destinations, feerate, priority=False): - """ - Spend these {vaults} to these {destinations} (mapping of addresses to amounts). - Make sure to call this only with revault deployment with a low (<500) CSV, or you'll encounter - an ugly timeout from bitcoinlib. - - :return: the list of spent deposits along with the Spend PSBT. - """ - deposits, spend_psbt = self.spend_vaults_unconfirmed( - vaults, destinations, feerate, priority - ) - - self.bitcoind.generate_block(1, wait_for_mempool=[spend_psbt.tx.hash]) - for w in self.participants(): - wait_for( - lambda: len(w.rpc.listvaults(["spent"], deposits)["vaults"]) - == len(deposits) - ) - - return deposits, spend_psbt.tx.hash - - def _any_spend_data(self, vaults): - addr = self.bitcoind.rpc.getnewaddress() - total_spent = sum(v["amount"] for v in vaults) - feerate = 2 - fees = self.compute_spendtx_fees(feerate, len(vaults), 1) - return {addr: total_spent - fees}, feerate - - def unvault_vaults_anyhow(self, vaults, priority=False): - """ - Unvault these vaults with a random Spend transaction for a maximum amount and a - fixed feerate. - """ - destinations, feerate = self._any_spend_data(vaults) - return self.unvault_vaults(vaults, destinations, feerate, priority) - - def broadcast_unvaults_anyhow(self, vaults, priority=False): - """ - Broadcast the Unvault transactions for these vaults with a random Spend - transaction for a maximum amount and a fixed feerate. - """ - destinations, feerate = self._any_spend_data(vaults) - return self.broadcast_unvaults(vaults, destinations, feerate, priority) - - def spend_vaults_anyhow(self, vaults): - """Spend these vaults to a random address for a maximum amount for a fixed feerate""" - destinations, feerate = self._any_spend_data(vaults) - return self.spend_vaults(vaults, destinations, feerate) - - def spend_vaults_anyhow_unconfirmed(self, vaults, priority=False): - """ - Spend these vaults to a random address for a maximum amount for a fixed feerate, - not confirming the Spend transaction. - """ - destinations, feerate = self._any_spend_data(vaults) - return self.spend_vaults_unconfirmed(vaults, destinations, feerate, priority) - - def compute_spendtx_fees( - self, spendtx_feerate, n_vaults_spent, n_destinations, with_change=False - ): - """Get the fees necessary to include in a Spend transaction. - This assumes the destinations to be P2WPKH - """ - n_stk = len(self.stks()) - n_man = len(self.mans()) - - # witscript PUSH, keys , Unvault Script overhead, signatures - spend_witness_vb = ( - 1 + (n_man + n_stk * 2) * 34 + 15 + (n_man + n_stk) * 73 - ) // 4 - # Overhead, P2WPKH, P2WSH, inputs, witnesses - spend_witstrip_vb = ( - 11 - + 31 * n_destinations - + 43 * (1 + (1 if with_change else 0)) - + (32 + 4 + 4 + 1) * n_vaults_spent - ) - spendtx_vbytes = spend_witstrip_vb + spend_witness_vb * n_vaults_spent - - # witscript PUSH, keys , Deposit Script overhead, signatures - unvault_witness_vb = (1 + n_stk * (34 + 73) + 3) // 4 - # Overhead, P2WSH * 2, inputs + witness - unvaulttxs_vbytes = ( - 11 + 43 * 2 + (32 + 4 + 4 + 1) + unvault_witness_vb - ) * n_vaults_spent - - return ( - spendtx_vbytes * spendtx_feerate # Spend fees - + 2 * 32 * spendtx_vbytes # Spend CPFP - + unvaulttxs_vbytes * 24 # Unvault fees (6sat/WU feerate) - + 30_000 * n_vaults_spent # Unvault CPFP - ) - - def cancel_vault(self, vault): - deposit = f"{vault['txid']}:{vault['vout']}" - - for w in self.participants(): - wait_for( - lambda: len( - w.rpc.listvaults( - ["unvaulting", "unvaulted", "spending"], [deposit] - )["vaults"] - ) - == 1 - ) - - self.stk(0).rpc.revault(deposit) - self.bitcoind.generate_block(1, wait_for_mempool=1) - for w in self.participants(): - wait_for( - lambda: len(w.rpc.listvaults(["canceled"], [deposit])["vaults"]) == 1 - ) - - def stop_wallets(self): - jobs = [self.executor.submit(w.stop) for w in self.participants()] - for j in jobs: - j.result(TIMEOUT) - - def start_wallets(self): - jobs = [self.executor.submit(w.start) for w in self.participants()] - for j in jobs: - j.result(TIMEOUT) - - def cleanup(self): - for n in self.daemons: - n.cleanup() - if self.bitcoind_proxy is not None: - self.bitcoind_proxy.stop()