liana/tests/test_framework/revault_network.py

812 lines
31 KiB
Python

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()