|
| 1 | +import io |
| 2 | +import tarfile |
| 3 | + |
| 4 | +from backend.kubernetes_backend import KubernetesBackend |
| 5 | +from warnet.services import ServiceType |
| 6 | +from warnet.utils import exponential_backoff, generate_ipv4_addr, handle_json |
| 7 | + |
| 8 | +from .lnchannel import LNChannel |
| 9 | +from .lnnode import LNNode |
| 10 | +from .status import RunningStatus |
| 11 | + |
| 12 | +CLN_CONFIG_BASE = " ".join( |
| 13 | + [ |
| 14 | + "--network=regtest", |
| 15 | + "--database-upgrade=true", |
| 16 | + "--bitcoin-retry-timeout=600", |
| 17 | + "--bind-addr=0.0.0.0:9735", |
| 18 | + "--developer", |
| 19 | + "--dev-fast-gossip", |
| 20 | + "--log-level=debug", |
| 21 | + ] |
| 22 | +) |
| 23 | + |
| 24 | + |
| 25 | +class CLNNode(LNNode): |
| 26 | + def __init__(self, warnet, tank, backend: KubernetesBackend, options): |
| 27 | + self.warnet = warnet |
| 28 | + self.tank = tank |
| 29 | + self.backend = backend |
| 30 | + self.image = options["ln_image"] |
| 31 | + self.cb = options["cb_image"] |
| 32 | + self.ln_config = options["ln_config"] |
| 33 | + self.ipv4 = generate_ipv4_addr(self.warnet.subnet) |
| 34 | + self.rpc_port = 10009 |
| 35 | + self.impl = "cln" |
| 36 | + |
| 37 | + @property |
| 38 | + def status(self) -> RunningStatus: |
| 39 | + return super().status |
| 40 | + |
| 41 | + @property |
| 42 | + def cb_status(self) -> RunningStatus: |
| 43 | + return super().cb_status |
| 44 | + |
| 45 | + def get_conf(self, ln_container_name, tank_container_name) -> str: |
| 46 | + conf = CLN_CONFIG_BASE |
| 47 | + conf += f" --alias={self.tank.index}" |
| 48 | + conf += f" --grpc-port={self.rpc_port}" |
| 49 | + conf += f" --bitcoin-rpcuser={self.tank.rpc_user}" |
| 50 | + conf += f" --bitcoin-rpcpassword={self.tank.rpc_password}" |
| 51 | + conf += f" --bitcoin-rpcconnect={tank_container_name}" |
| 52 | + conf += f" --bitcoin-rpcport={self.tank.rpc_port}" |
| 53 | + conf += f" --announce-addr=dns:{ln_container_name}:9735" |
| 54 | + return conf |
| 55 | + |
| 56 | + @exponential_backoff(max_retries=20, max_delay=300) |
| 57 | + @handle_json |
| 58 | + def lncli(self, cmd) -> dict: |
| 59 | + cli = "lightning-cli" |
| 60 | + cmd = f"{cli} --network=regtest {cmd}" |
| 61 | + return self.backend.exec_run(self.tank.index, ServiceType.LIGHTNING, cmd) |
| 62 | + |
| 63 | + def getnewaddress(self): |
| 64 | + return self.lncli("newaddr")["bech32"] |
| 65 | + |
| 66 | + def get_pub_key(self): |
| 67 | + res = self.lncli("getinfo") |
| 68 | + return res["id"] |
| 69 | + |
| 70 | + def getURI(self): |
| 71 | + res = self.lncli("getinfo") |
| 72 | + if len(res["address"]) < 1: |
| 73 | + return None |
| 74 | + return f'{res["id"]}@{res["address"][0]["address"]}:{res["address"][0]["port"]}' |
| 75 | + |
| 76 | + def get_wallet_balance(self) -> int: |
| 77 | + res = self.lncli("listfunds") |
| 78 | + return int(sum(o["amount_msat"] for o in res["outputs"]) / 1000) |
| 79 | + |
| 80 | + # returns the channel point in the form txid:output_index |
| 81 | + def open_channel_to_tank(self, index: int, channel_open_data: str) -> str: |
| 82 | + tank = self.warnet.tanks[index] |
| 83 | + [pubkey, host] = tank.lnnode.getURI().split("@") |
| 84 | + res = self.lncli(f"fundchannel id={pubkey} {channel_open_data}") |
| 85 | + return f"{res['txid']}:{res['outnum']}" |
| 86 | + |
| 87 | + def update_channel_policy(self, chan_point: str, policy: str) -> str: |
| 88 | + return self.lncli(f"setchannel {chan_point} {policy}") |
| 89 | + |
| 90 | + def get_graph_nodes(self) -> list[str]: |
| 91 | + return list(n["nodeid"] for n in self.lncli("listnodes")["nodes"]) |
| 92 | + |
| 93 | + def get_graph_channels(self) -> list[LNChannel]: |
| 94 | + cln_channels = self.lncli("listchannels")["channels"] |
| 95 | + # CLN lists channels twice, once for each direction. This finds the unique channel ids. |
| 96 | + short_channel_ids = {chan["short_channel_id"]: chan for chan in cln_channels}.keys() |
| 97 | + channels = [] |
| 98 | + for short_channel_id in short_channel_ids: |
| 99 | + node1, node2 = ( |
| 100 | + chans for chans in cln_channels if chans["short_channel_id"] == short_channel_id |
| 101 | + ) |
| 102 | + channels.append(self.lnchannel_from_json(node1, node2)) |
| 103 | + return channels |
| 104 | + |
| 105 | + @staticmethod |
| 106 | + def lnchannel_from_json(node1: object, node2: object) -> LNChannel: |
| 107 | + assert node1["short_channel_id"] == node2["short_channel_id"] |
| 108 | + assert node1["direction"] != node2["direction"] |
| 109 | + return LNChannel( |
| 110 | + node1_pub=node1["source"], |
| 111 | + node2_pub=node2["source"], |
| 112 | + capacity_msat=node1["amount_msat"], |
| 113 | + short_chan_id=node1["short_channel_id"], |
| 114 | + node1_min_htlc=node1["htlc_minimum_msat"], |
| 115 | + node2_min_htlc=node2["htlc_minimum_msat"], |
| 116 | + node1_max_htlc=node1["htlc_maximum_msat"], |
| 117 | + node2_max_htlc=node2["htlc_maximum_msat"], |
| 118 | + node1_base_fee_msat=node1["base_fee_millisatoshi"], |
| 119 | + node2_base_fee_msat=node2["base_fee_millisatoshi"], |
| 120 | + node1_fee_rate_milli_msat=node1["fee_per_millionth"], |
| 121 | + node2_fee_rate_milli_msat=node2["fee_per_millionth"], |
| 122 | + ) |
| 123 | + |
| 124 | + def get_peers(self) -> list[str]: |
| 125 | + return list(p["id"] for p in self.lncli("listpeers")["peers"]) |
| 126 | + |
| 127 | + def connect_to_tank(self, index): |
| 128 | + return super().connect_to_tank(index) |
| 129 | + |
| 130 | + def generate_cli_command(self, command: list[str]): |
| 131 | + network = f"--network={self.tank.warnet.bitcoin_network}" |
| 132 | + cmd = f"{network} {' '.join(command)}" |
| 133 | + cmd = f"lightning-cli {cmd}" |
| 134 | + return cmd |
| 135 | + |
| 136 | + def export(self, config: object, tar_file): |
| 137 | + # Retrieve the credentials |
| 138 | + ca_cert = self.backend.get_file( |
| 139 | + self.tank.index, |
| 140 | + ServiceType.LIGHTNING, |
| 141 | + "/root/.lightning/regtest/ca.pem", |
| 142 | + ) |
| 143 | + client_cert = self.backend.get_file( |
| 144 | + self.tank.index, |
| 145 | + ServiceType.LIGHTNING, |
| 146 | + "/root/.lightning/regtest/client.pem", |
| 147 | + ) |
| 148 | + client_key = self.backend.get_file( |
| 149 | + self.tank.index, |
| 150 | + ServiceType.LIGHTNING, |
| 151 | + "/root/.lightning/regtest/client-key.pem", |
| 152 | + ) |
| 153 | + name = f"ln-{self.tank.index}" |
| 154 | + ca_cert_filename = f"{name}_ca_cert.pem" |
| 155 | + client_cert_filename = f"{name}_client_cert.pem" |
| 156 | + client_key_filename = f"{name}_client_key.pem" |
| 157 | + host = self.backend.get_lnnode_hostname(self.tank.index) |
| 158 | + |
| 159 | + # Add the files to the in-memory tar archive |
| 160 | + tarinfo1 = tarfile.TarInfo(name=ca_cert_filename) |
| 161 | + tarinfo1.size = len(ca_cert) |
| 162 | + fileobj1 = io.BytesIO(ca_cert) |
| 163 | + tar_file.addfile(tarinfo=tarinfo1, fileobj=fileobj1) |
| 164 | + tarinfo2 = tarfile.TarInfo(name=client_cert_filename) |
| 165 | + tarinfo2.size = len(client_cert) |
| 166 | + fileobj2 = io.BytesIO(client_cert) |
| 167 | + tar_file.addfile(tarinfo=tarinfo2, fileobj=fileobj2) |
| 168 | + tarinfo3 = tarfile.TarInfo(name=client_key_filename) |
| 169 | + tarinfo3.size = len(client_key) |
| 170 | + fileobj3 = io.BytesIO(client_key) |
| 171 | + tar_file.addfile(tarinfo=tarinfo3, fileobj=fileobj3) |
| 172 | + |
| 173 | + config["nodes"].append( |
| 174 | + { |
| 175 | + "id": name, |
| 176 | + "address": f"https://{host}:{self.rpc_port}", |
| 177 | + "ca_cert": f"/simln/{ca_cert_filename}", |
| 178 | + "client_cert": f"/simln/{client_cert_filename}", |
| 179 | + "client_key": f"/simln/{client_key_filename}", |
| 180 | + } |
| 181 | + ) |
0 commit comments