Antoine Poinsot 3c82173f46
qa: abstract the signer from the Lianad class
That's prep work to introduce a multisig
2023-01-24 19:38:34 +01:00

143 lines
4.7 KiB
Python

import logging
import os
import shutil
from bip380.descriptors import Descriptor
from bip380.miniscript import SatisfactionMaterial
from test_framework.utils import (
UnixDomainSocketRpc,
TailableProc,
VERBOSE,
LOG_LEVEL,
LIANAD_PATH,
wait_for,
)
from test_framework.serializations import (
PSBT,
CTxInWitness,
CScriptWitness,
PSBT_IN_BIP32_DERIVATION,
PSBT_IN_PARTIAL_SIG,
PSBT_IN_FINAL_SCRIPTWITNESS,
)
class Lianad(TailableProc):
def __init__(
self,
datadir,
signer,
multi_desc,
bitcoind_rpc_port,
bitcoind_cookie_path,
):
TailableProc.__init__(self, datadir, verbose=VERBOSE)
self.datadir = datadir
self.prefix = os.path.split(datadir)[-1]
self.signer = signer
self.multi_desc = multi_desc
self.receive_desc, self.change_desc = multi_desc.singlepath_descriptors()
self.conf_file = os.path.join(datadir, "config.toml")
self.cmd_line = [LIANAD_PATH, "--conf", f"{self.conf_file}"]
socket_path = os.path.join(os.path.join(datadir, "regtest"), "lianad_rpc")
self.rpc = UnixDomainSocketRpc(socket_path)
with open(self.conf_file, "w") as f:
f.write(f"data_dir = '{datadir}'\n")
f.write("daemon = false\n")
f.write(f"log_level = '{LOG_LEVEL}'\n")
f.write(f'main_descriptor = "{multi_desc}"\n')
f.write("[bitcoin_config]\n")
f.write('network = "regtest"\n')
f.write("poll_interval_secs = 1\n")
f.write("[bitcoind_config]\n")
f.write(f"cookie_path = '{bitcoind_cookie_path}'\n")
f.write(f"addr = '127.0.0.1:{bitcoind_rpc_port}'\n")
def finalize_psbt(self, psbt):
"""Create a valid witness for all inputs in the PSBT.
This will fail if the PSBT input does not contain enough material.
:param psbt: PSBT of the transaction to be finalized.
:returns: PSBT with finalized inputs.
"""
assert isinstance(psbt, PSBT)
# Create a witness for each input of the transaction.
for i, psbt_in in enumerate(psbt.i):
# First, gather the needed information from the PSBT input.
# 'hd_keypaths' is of the form {pubkey: (fingerprint, derivation index)}
fing_der = next(iter(psbt_in.map[PSBT_IN_BIP32_DERIVATION].values()))
raw_der_path = fing_der[4:]
der_path = [
int.from_bytes(raw_der_path[i : i + 4], byteorder="little", signed=True)
for i in range(0, len(raw_der_path), 4)
]
assert len(der_path) == 2
# Create a copy of the descriptor to derive it at the index used in this input.
# Then create a satisfaction for it using the signature we just created.
desc = Descriptor.from_str(
str(self.receive_desc if der_path[0] == 0 else self.change_desc)
)
desc.derive(der_path[1])
sat_material = SatisfactionMaterial(
signatures=psbt_in.map[PSBT_IN_PARTIAL_SIG],
)
stack = desc.satisfy(sat_material)
logging.debug(f"Satisfaction for {desc} is {[e.hex() for e in stack]}")
# Update the transaction inside the PSBT directly.
assert stack is not None
psbt_in.map[PSBT_IN_FINAL_SCRIPTWITNESS] = CTxInWitness(
CScriptWitness(stack)
)
psbt.tx.wit.vtxinwit.append(psbt_in.map[PSBT_IN_FINAL_SCRIPTWITNESS])
return psbt
def restart_fresh(self, bitcoind):
"""Delete the internal state of the wallet and restart."""
self.stop()
dir_path = os.path.join(self.datadir, "regtest")
shutil.rmtree(dir_path)
wallet_path = os.path.join(dir_path, "lianad_watchonly_wallet")
bitcoind.node_rpc.unloadwallet(wallet_path)
self.start()
wait_for(
lambda: self.rpc.getinfo()["block_height"] == bitcoind.rpc.getblockcount()
)
def start(self):
TailableProc.start(self)
self.wait_for_logs(
[
"Database initialized and checked",
"Connection to bitcoind established and checked.",
"JSONRPC server started.",
]
)
def stop(self, timeout=5):
try:
self.rpc.stop()
self.wait_for_log(
"Stopping the liana daemon.",
)
self.proc.wait(timeout)
except Exception as e:
logging.error(f"{self.prefix} : error when calling stop: '{e}'")
return TailableProc.stop(self)
def cleanup(self):
try:
self.stop()
except Exception:
self.proc.kill()