812 lines
31 KiB
Python
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()
|