diff --git a/docs/circuit-breaker.md b/docs/circuit-breaker.md index d6124c07d..cd9e3d516 100644 --- a/docs/circuit-breaker.md +++ b/docs/circuit-breaker.md @@ -20,9 +20,8 @@ nodes: - name: tank-0003 addnode: - tank-0000 - ln: - lnd: true lnd: + enabled: true config: | bitcoin.timelockdelta=33 channels: @@ -51,27 +50,26 @@ nodes: - name: tank-0000 addnode: - tank-0001 - ln: - lnd: true + lnd: + enabled: true - name: tank-0001 addnode: - tank-0002 - ln: - lnd: true + lnd: + enabled: true - name: tank-0002 addnode: - tank-0000 - ln: - lnd: true + lnd: + enabled: true - name: tank-0003 addnode: - tank-0000 - ln: - lnd: true lnd: + enabled: true config: | bitcoin.timelockdelta=33 channels: @@ -88,9 +86,8 @@ nodes: - name: tank-0004 addnode: - tank-0000 - ln: - lnd: true lnd: + enabled: true channels: - id: block: 300 @@ -102,8 +99,8 @@ nodes: - name: tank-0005 addnode: - tank-0000 - ln: - lnd: true + lnd: + enabled: true ``` ## Accessing Circuit Breaker diff --git a/resources/charts/bitcoincore/Chart.yaml b/resources/charts/bitcoincore/Chart.yaml index 36f7498af..aa90de0e7 100644 --- a/resources/charts/bitcoincore/Chart.yaml +++ b/resources/charts/bitcoincore/Chart.yaml @@ -5,10 +5,13 @@ description: A Helm chart for Bitcoin Core dependencies: - name: lnd version: 0.1.0 - condition: ln.lnd + condition: lnd.enabled - name: cln version: 0.1.0 - condition: ln.cln + condition: cln.enabled + - name: eclair + version: 0.1.0 + condition: eclair.enabled # A chart can be either an 'application' or a 'library' chart. # diff --git a/resources/charts/bitcoincore/charts/eclair/.helmignore b/resources/charts/bitcoincore/charts/eclair/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/resources/charts/bitcoincore/charts/eclair/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/resources/charts/bitcoincore/charts/eclair/Chart.yaml b/resources/charts/bitcoincore/charts/eclair/Chart.yaml new file mode 100644 index 000000000..c496a596b --- /dev/null +++ b/resources/charts/bitcoincore/charts/eclair/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: eclair +description: A Helm chart for Eclair + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "0.1.0" diff --git a/resources/charts/bitcoincore/charts/eclair/templates/_helpers.tpl b/resources/charts/bitcoincore/charts/eclair/templates/_helpers.tpl new file mode 100644 index 000000000..674b4ea97 --- /dev/null +++ b/resources/charts/bitcoincore/charts/eclair/templates/_helpers.tpl @@ -0,0 +1,78 @@ +{{/* +Expand the name of the PARENT chart. +*/}} +{{- define "bitcoincore.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified PARENT app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "bitcoincore.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s" .Release.Name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} + + +{{/* +Expand the name of the chart. +*/}} +{{- define "eclair.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}-ln +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "eclair.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s" .Release.Name | trunc 63 | trimSuffix "-" }}-ln +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "eclair.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "eclair.labels" -}} +helm.sh/chart: {{ include "eclair.chart" . }} +{{ include "eclair.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "eclair.selectorLabels" -}} +app.kubernetes.io/name: {{ include "eclair.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "eclair.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "eclair.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/resources/charts/bitcoincore/charts/eclair/templates/configmap.yaml b/resources/charts/bitcoincore/charts/eclair/templates/configmap.yaml new file mode 100644 index 000000000..7d74e5476 --- /dev/null +++ b/resources/charts/bitcoincore/charts/eclair/templates/configmap.yaml @@ -0,0 +1,32 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "eclair.fullname" . }} + labels: + {{- include "eclair.labels" . | nindent 4 }} +data: + eclair.conf: | + {{- .Values.baseConfig | nindent 4 }} + {{- .Values.defaultConfig | nindent 4 }} + {{- .Values.config | nindent 4 }} + eclair.chain = {{ .Values.global.chain }} + eclair.bitcoind.host = {{ include "bitcoincore.fullname" . }} + eclair.bitcoind.rpcport = {{ index .Values.global .Values.global.chain "RPCPort" }} + eclair.bitcoind.rpcuser = user + eclair.bitcoind.rpcpassword = {{ .Values.global.rpcpassword }} + eclair.node-alias = {{ include "eclair.fullname" . }} + eclair.bitcoind.zmqblock = "tcp://{{ include "bitcoincore.fullname" . }}:{{ .Values.global.ZMQBlockPort }}" + eclair.bitcoind.zmqtx = "tcp://{{ include "bitcoincore.fullname" . }}:{{ .Values.global.ZMQTxPort }}" + eclair.bitcoind.startup-locked-utxos-behavior = "unlock" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "eclair.fullname" . }}-channels + labels: + channels: "true" + {{- include "eclair.labels" . | nindent 4 }} +data: + source: {{ include "eclair.fullname" . }} + channels: | + {{ .Values.channels | toJson }} diff --git a/resources/charts/bitcoincore/charts/eclair/templates/pod.yaml b/resources/charts/bitcoincore/charts/eclair/templates/pod.yaml new file mode 100644 index 000000000..ee6f16acc --- /dev/null +++ b/resources/charts/bitcoincore/charts/eclair/templates/pod.yaml @@ -0,0 +1,82 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{ include "eclair.fullname" . }} + labels: + {{- include "eclair.labels" . | nindent 4 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} + app: {{ include "eclair.fullname" . }} + {{- if .Values.collectLogs }} + collect_logs: "true" + {{- end }} + chain: {{ .Values.global.chain }} + annotations: + kubectl.kubernetes.io/default-container: "eclair" +spec: + {{- with .Values.imagePullSecrets }} + restartPolicy: "{{ .Values.restartPolicy }}" + imagePullSecrets: + {{- toYaml . | nindent 4 }} + {{- end }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 4 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - "sh" + - "-c" + args: + - > + /app/eclair-node/bin/eclair-node.sh -v & + while [ ! -f /root/.eclair/eclair.log ]; do + echo "Waiting for log file" + sleep 2 + done && + tail -f /root/.eclair/eclair.log + ports: + - name: server + containerPort: {{ .Values.ServerPort }} + protocol: TCP + - name: rest + containerPort: {{ .Values.RestPort }} + protocol: TCP + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 8 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 8 }} + startupProbe: + {{- toYaml .Values.startupProbe | nindent 8 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + {{- with .Values.volumeMounts }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.extraContainers }} + {{- toYaml . | nindent 6 }} + {{- end }} + volumes: + {{- with .Values.volumes }} + {{- toYaml . | nindent 4 }} + {{- end }} + - configMap: + name: {{ include "eclair.fullname" . }} + name: config + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/resources/charts/bitcoincore/charts/eclair/templates/service.yaml b/resources/charts/bitcoincore/charts/eclair/templates/service.yaml new file mode 100644 index 000000000..f1458557a --- /dev/null +++ b/resources/charts/bitcoincore/charts/eclair/templates/service.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "eclair.fullname" . }} + labels: + {{- include "eclair.labels" . | nindent 4 }} + app: {{ include "eclair.fullname" . }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.ServerPort }} + targetPort: server + protocol: TCP + name: server + - port: {{ .Values.RestPort }} + targetPort: rest + protocol: TCP + name: rest + selector: + {{- include "eclair.selectorLabels" . | nindent 4 }} diff --git a/resources/charts/bitcoincore/charts/eclair/values.yaml b/resources/charts/bitcoincore/charts/eclair/values.yaml new file mode 100644 index 000000000..6adb3ba3e --- /dev/null +++ b/resources/charts/bitcoincore/charts/eclair/values.yaml @@ -0,0 +1,111 @@ +# Default values for eclair. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +namespace: warnet + +restartPolicy: Never + +image: + repository: bitdonkey/eclair + pullPolicy: IfNotPresent + tag: "0.11.0" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +podLabels: + app: "warnet" + mission: "lightning" + +podSecurityContext: {} + +securityContext: {} + +service: + type: ClusterIP + +ServerPort: 9735 +RestPort: 8080 + +ingress: + enabled: false + className: "" + annotations: {} + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ +livenessProbe: + exec: + command: + - eclair-cli + - -p + - 21satoshi + - getinfo + failureThreshold: 3 + initialDelaySeconds: 5 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 1 +readinessProbe: + failureThreshold: 3 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 10 + exec: + command: + - eclair-cli + - -p + - 21satoshi + - getinfo + +# Additional volumes on the output Deployment definition. +volumes: + - name: temp-storage + emptyDir: {} + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: + - mountPath: /root/.eclair/eclair.conf + name: config + subPath: eclair.conf + - mountPath: /root/.eclair + name: temp-storage + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +baseConfig: | + eclair.server.port = 9735 + eclair.api.enabled = true + eclair.api.binding-ip = 0.0.0.0 + eclair.api.password = 21satoshi + eclair.api.port = 8080 + eclair.features.keysend = optional + eclair.bitcoind.startup-locked-utxos-behavior = "unlock" + +config: "" + +defaultConfig: "" + +channels: [] diff --git a/resources/charts/bitcoincore/values.yaml b/resources/charts/bitcoincore/values.yaml index 67f1b9b10..46b72819e 100644 --- a/resources/charts/bitcoincore/values.yaml +++ b/resources/charts/bitcoincore/values.yaml @@ -123,6 +123,7 @@ baseConfig: | capturemessages=1 fallbackfee=0.00001000 listen=1 + txindex=1 rpcuser=user # rpcpassword MUST be set as a chart value rpcallowip=0.0.0.0/0 @@ -140,6 +141,10 @@ loadSnapshot: enabled: false url: "" -ln: - lnd: false - cln: false +cln: + enabled: false +eclair: + enabled: false +lnd: + enabled: false + diff --git a/resources/plugins/simln/README.md b/resources/plugins/simln/README.md index a627813af..58bf4ced6 100644 --- a/resources/plugins/simln/README.md +++ b/resources/plugins/simln/README.md @@ -48,27 +48,26 @@ nodes: - name: tank-0000 addnode: - tank-0001 - ln: - lnd: true + lnd: + enabled: true - name: tank-0001 addnode: - tank-0002 - ln: - lnd: true + lnd: + enabled: true - name: tank-0002 addnode: - tank-0000 - ln: - lnd: true + eclair: + enabled: true - name: tank-0003 addnode: - tank-0000 - ln: - lnd: true lnd: + enabled: true config: | bitcoin.timelockdelta=33 channels: @@ -82,9 +81,8 @@ nodes: - name: tank-0004 addnode: - tank-0000 - ln: - cln: true cln: + enabled: true channels: - id: block: 300 @@ -96,8 +94,8 @@ nodes: - name: tank-0005 addnode: - tank-0000 - ln: - lnd: true + lnd: + enabled: true plugins: postDeploy: diff --git a/resources/plugins/simln/charts/simln/values.yaml b/resources/plugins/simln/charts/simln/values.yaml index a1647a963..dc1e47783 100644 --- a/resources/plugins/simln/charts/simln/values.yaml +++ b/resources/plugins/simln/charts/simln/values.yaml @@ -1,7 +1,7 @@ name: "simln" image: repository: "bitcoindevproject/simln" - tag: "0.2.3" + tag: "0.2.4" pullPolicy: IfNotPresent workingVolume: diff --git a/resources/plugins/simln/plugin.py b/resources/plugins/simln/plugin.py index 78df1a917..bff8b93d2 100755 --- a/resources/plugins/simln/plugin.py +++ b/resources/plugins/simln/plugin.py @@ -163,20 +163,22 @@ def _launch_activity(activity: Optional[list[dict]], plugin_dir: str) -> str: def _generate_activity_json(activity: Optional[list[dict]]) -> str: nodes = [] - for i in get_mission(LIGHTNING_MISSION): ln_name = i.metadata.name - port = 10009 node = {"id": ln_name} if "cln" in i.metadata.labels["app.kubernetes.io/name"]: - port = 9736 + node["address"] = f"https://{ln_name}:9736" node["ca_cert"] = f"/working/{ln_name}-ca.pem" node["client_cert"] = f"/working/{ln_name}-client.pem" node["client_key"] = f"/working/{ln_name}-client-key.pem" + elif "eclair" in i.metadata.labels["app.kubernetes.io/name"]: + node["base_url"] = f"http://{ln_name}:8080" + node["api_username"] = "" + node["api_password"] = "21satoshi" else: + node["address"] = f"https://{ln_name}:10009" node["macaroon"] = "/working/admin.macaroon" node["cert"] = "/working/tls.cert" - node["address"] = f"https://{ln_name}:{port}" nodes.append(node) if activity: diff --git a/resources/scenarios/commander.py b/resources/scenarios/commander.py index 4a44ab4a7..ba457fd18 100644 --- a/resources/scenarios/commander.py +++ b/resources/scenarios/commander.py @@ -13,7 +13,7 @@ from time import sleep from kubernetes import client, config -from ln_framework.ln import CLN, LND, LNNode +from ln_framework.ln import CLN, ECLAIR, LND, LNNode from test_framework.authproxy import AuthServiceProxy from test_framework.p2p import NetworkThread from test_framework.test_framework import ( @@ -73,6 +73,8 @@ lnnode = LND(pod.metadata.name, pod.status.pod_ip) if "cln" in pod.metadata.labels["app.kubernetes.io/name"]: lnnode = CLN(pod.metadata.name, pod.status.pod_ip) + elif "eclair" in pod.metadata.labels["app.kubernetes.io/name"]: + lnnode = ECLAIR(pod.metadata.name, pod.status.pod_ip) WARNET["lightning"].append(lnnode) for cm in cmaps.items: diff --git a/resources/scenarios/ln_framework/ln.py b/resources/scenarios/ln_framework/ln.py index f2ec4cbc2..68972272c 100644 --- a/resources/scenarios/ln_framework/ln.py +++ b/resources/scenarios/ln_framework/ln.py @@ -2,7 +2,9 @@ import http.client import json import logging +import re import ssl +import urllib.parse from abc import ABC, abstractmethod from time import sleep @@ -343,7 +345,7 @@ def graph(self, max_tries=2) -> dict: channel["capacity"] = channel["amount_msat"] // 1000 return {"edges": sorted_channels} else: - self.log.warning(f"unable to open channel: {res}, wait and retry...") + self.log.warning(f"unable to list channels: {res}, wait and retry...") sleep(1) else: self.log.debug(f"channel response: {response}, wait and retry...") @@ -355,6 +357,197 @@ def update(self, txid_hex: str, policy: dict, capacity: int, max_tries=2) -> dic return None +class ECLAIR(LNNode): + def __init__(self, pod_name, ip_address): + super().__init__(pod_name, ip_address) + self.conn = None + self.headers = {"Authorization": "Basic OjIxc2F0b3NoaQ=="} + self.impl = "eclair" + self.reset_connection() + + def reset_connection(self): + self.conn = http.client.HTTPConnection(host=self.name, port=8080, timeout=5) + + def get(self, uri): + attempt = 0 + while True: + try: + self.log.warning(f"headers: {self.headers}") + self.conn.request( + method="GET", + url=uri, + headers=self.headers, + ) + return self.conn.getresponse().read().decode("utf8") + except Exception as e: + self.reset_connection() + attempt += 1 + if attempt > 5: + self.log.error(f"Error ECLAIR GET, Abort: {e}") + return None + sleep(1) + + def post(self, uri, data=None): + if not data: + data = {} + body = urllib.parse.urlencode(data) + post_header = self.headers + post_header["Content-Length"] = str(len(body)) + post_header["Content-Type"] = "application/x-www-form-urlencoded" + attempt = 0 + while True: + try: + self.conn.request( + method="POST", + url=uri, + body=body, + headers=post_header, + ) + # Stream output, otherwise we get a timeout error + res = self.conn.getresponse() + stream = "" + while True: + try: + data = res.read(1) + if len(data) == 0: + break + else: + stream += data.decode("utf8") + except Exception: + break + return stream + except Exception as e: + self.reset_connection() + attempt += 1 + if attempt > 5: + self.log.error(f"Error ECLAIR POST, Abort: {e}") + return None + sleep(1) + + def newaddress(self, max_tries=10): + attempt = 0 + while attempt < max_tries: + attempt += 1 + response = self.post("/getnewaddress") + if not response: + sleep(5) + continue + return True, response.strip('"') + return False, "" + + def uri(self): + res = json.loads(self.post("/getinfo")) + return f"{res['nodeId']}@{res['alias']}:9735" + + def walletbalance(self, max_tries=2) -> int: + attempt = 0 + while attempt < max_tries: + attempt += 1 + response = self.post("/globalbalance") + if not response: + sleep(2) + continue + res = json.loads(response) + return int(res["total"] * 100000000) # convert to sats + return 0 + + def channelbalance(self, max_tries=2) -> int: + attempt = 0 + while attempt < max_tries: + attempt += 1 + response = self.post("/usablebalances") + if not response: + sleep(2) + continue + res = json.loads(response) + return int(sum(o["canSend"] + o["canReceive"] for o in res)) + return 0 + + def connect(self, target_uri, max_tries=5) -> dict: + attempt = 0 + while attempt < max_tries: + attempt += 1 + response = self.post("/connect", {"uri": target_uri}) + if "connected" in response: + return {} + else: + self.log.debug(f"connect response: {response}, wait and retry...") + sleep(2) + return None + + def channel(self, pk, capacity, push_amt, fee_rate, max_tries=10) -> dict: + NON_GROUPED_UTXO_BYTE_SIZE = 165 + data = { + "fundingSatoshis": capacity, + "pushMsat": push_amt, + "nodeId": pk, + "fundingFeerateSatByte": fee_rate, + "fundingFeeBudgetSatoshis": fee_rate * NON_GROUPED_UTXO_BYTE_SIZE, + } # FIXME: https://acinq.github.io/eclair/#open-2 what parameters should be sent? + attempt = 0 + while attempt < max_tries: + attempt += 1 + response = self.post("/open", data) + print("channel open", response) + if response: + if "created channel" in response: + # created channel e872f515dc5d8a3d61ccbd2127f33141eaa115807271dcc5c5c727f3eca914d3 with fundingTxId=bc2b8db55b9588d3a18bd06bd0e284f63ee8cc149c63138d51ac8ef81a72fc6f and fees=720 sat + channel_id = re.search(r"channel ([0-9a-f]+)", response).group(1) + funding_tx_id = re.search(r"fundingTxId=([0-9a-f]+)", response).group(1) + return { + "txid": funding_tx_id, + "outpoint": f"{funding_tx_id}:N/A", + "channel": channel_id, + } + else: + self.log.warning(f"unable to open channel: {response}, wait and retry...") + sleep(1) + else: + self.log.debug(f"channel response: {response}, wait and retry...") + sleep(5) + return None + + def createinvoice(self, sats, label, description="new invoice") -> str: + b64_desc = base64.b64encode(description.encode("utf-8")) + response = self.post( + "/createinvoice", + {"amountMsat": sats * 1000, "description": label, "descriptionHash": b64_desc}, + ) # https://acinq.github.io/eclair/#createinvoice + if response: + res = json.loads(response) + return res + return None + + def payinvoice(self, payment_request) -> str: + response = self.post("/payinvoice", {"invoice": payment_request}) + # https://acinq.github.io/eclair/#payinvoice + if response: + return response + return None + + def graph(self, max_tries=5) -> dict: + attempt = 0 + while attempt < max_tries: + attempt += 1 + response = self.post("/allupdates") # https://acinq.github.io/eclair/#allupdates + if response: + res = json.loads(response) + if len(res) > 0: + filtered_channels = [ch for ch in res if ch["channelFlags"]["isNode1"]] + return {"edges": filtered_channels} + else: + self.log.warning(f"unable to list channels: {res}, wait and retry...") + sleep(10) + else: + self.log.debug(f"channel response: {response}, wait and retry...") + sleep(2) + return None + + def update(self, txid_hex: str, policy: dict, capacity: int, max_tries=2) -> dict: + self.log.warning("Channel Policy Updates not supported by ECLAIR yet!") + return None + + class LND(LNNode): def __init__(self, pod_name, ip_address): super().__init__(pod_name, ip_address) @@ -386,7 +579,7 @@ def get(self, uri): self.reset_connection() attempt += 1 if attempt > 5: - self.log.error(f"Error LND POST, Abort: {e}") + self.log.error(f"Error LND GET, Abort: {e}") return None sleep(1) @@ -423,13 +616,16 @@ def post(self, uri, data): if attempt > 5: self.log.error(f"Error LND POST, Abort: {e}") return None - sleep(1) + sleep(5) - def newaddress(self, max_tries=10): + def newaddress(self, max_tries=5): attempt = 0 while attempt < max_tries: attempt += 1 response = self.get("/v1/newaddress") + if not response: + sleep(5) + continue res = json.loads(response) if "address" in res: return True, res["address"] @@ -437,7 +633,7 @@ def newaddress(self, max_tries=10): self.log.warning( f"Couldn't get wallet address from {self.name}:\n {res}\n wait and retry..." ) - sleep(1) + sleep(5) return False, "" def walletbalance(self) -> int: diff --git a/resources/scenarios/ln_init.py b/resources/scenarios/ln_init.py index 416763ed7..386affbc4 100644 --- a/resources/scenarios/ln_init.py +++ b/resources/scenarios/ln_init.py @@ -28,6 +28,12 @@ def run_test(self): self.log.info("Setting up miner...") miner = self.ensure_miner(self.nodes[0]) miner_addr = miner.getnewaddress() + # create wallet for any eclair node + for node in self.nodes[1:]: + for ln in self.lns.values(): + if node.tank in ln.name and ln.impl == "eclair": + self.log.info(f"creating wallet for {node.tank}") + node.createwallet("eclair", descriptors=True) def gen(n): return self.generatetoaddress(self.nodes[0], n, miner_addr, sync_fun=self.no_op) @@ -361,6 +367,9 @@ def matching_graph(self, expected, ln): f"Expected edges {len(expected)}, actual edges {len(actual)}\n{actual}" ) for i, actual_ch in enumerate(actual): + if ln.impl == "eclair": + self.log.debug("eclair nodes do not support network capacity checks") + continue expected_ch = expected[i] capacity = expected_ch["capacity"] # We assert this because it isn't updated as part of policy. diff --git a/src/warnet/deploy.py b/src/warnet/deploy.py index 1fac94d4c..58193727e 100644 --- a/src/warnet/deploy.py +++ b/src/warnet/deploy.py @@ -366,11 +366,10 @@ def deploy_network(directory: Path, debug: bool = False, namespace: Optional[str network_file = yaml.safe_load(f) needs_ln_init = False - supported_ln_projects = ["lnd", "cln"] + supported_ln_projects = ["lnd", "cln", "eclair"] for node in network_file["nodes"]: - ln_config = node.get("ln", {}) for key in supported_ln_projects: - if ln_config.get(key, False) and key in node and "channels" in node[key]: + if key in node and node[key].get("enabled", False): needs_ln_init = True break if needs_ln_init: @@ -379,7 +378,7 @@ def deploy_network(directory: Path, debug: bool = False, namespace: Optional[str default_file_path = directory / DEFAULTS_FILE with default_file_path.open() as f: default_file = yaml.safe_load(f) - if any(default_file.get("ln", {}).get(key, False) for key in supported_ln_projects): + if any(default_file.get(key, {}).get("enabled", False) for key in supported_ln_projects): needs_ln_init = True processes = [] diff --git a/test/data/ln/node-defaults.yaml b/test/data/ln/node-defaults.yaml index 62e05199f..249bd8d7c 100644 --- a/test/data/ln/node-defaults.yaml +++ b/test/data/ln/node-defaults.yaml @@ -9,9 +9,8 @@ collectLogs: false metricsExport: false #LN configs -ln: - lnd: true lnd: + enabled: true defaultConfig: | color=#000000 config: | diff --git a/test/data/logging/network.yaml b/test/data/logging/network.yaml index 067161cae..6096d35c1 100644 --- a/test/data/logging/network.yaml +++ b/test/data/logging/network.yaml @@ -11,9 +11,8 @@ nodes: - name: tank-0002 addnode: - tank-0000 - ln: - lnd: true lnd: + enabled: true metricsExport: true prometheusMetricsPort: 9332 extraContainers: diff --git a/test/data/network_with_plugins/network.yaml b/test/data/network_with_plugins/network.yaml index 6e4d64a30..4df445702 100644 --- a/test/data/network_with_plugins/network.yaml +++ b/test/data/network_with_plugins/network.yaml @@ -2,27 +2,26 @@ nodes: - name: tank-0000 addnode: - tank-0001 - ln: - lnd: true + lnd: + enabled: true - name: tank-0001 addnode: - tank-0002 - ln: - lnd: true + lnd: + enabled: true - name: tank-0002 addnode: - tank-0000 - ln: - lnd: true + lnd: + enabled: true - name: tank-0003 addnode: - tank-0000 - ln: - lnd: true lnd: + enabled: true config: | bitcoin.timelockdelta=33 channels: @@ -39,9 +38,8 @@ nodes: - name: tank-0004 addnode: - tank-0000 - ln: - lnd: true lnd: + enabled: true channels: - id: block: 300 @@ -53,8 +51,8 @@ nodes: - name: tank-0005 addnode: - tank-0000 - ln: - lnd: true + lnd: + enabled: true plugins: # Each plugin section has a number of hooks available (preDeploy, postDeploy, etc) preDeploy: # For example, the preDeploy hook means it's plugin will run before all other deploy code diff --git a/test/data/plugins/hello/README.md b/test/data/plugins/hello/README.md index 77bb5040f..9cf6d8803 100644 --- a/test/data/plugins/hello/README.md +++ b/test/data/plugins/hello/README.md @@ -35,27 +35,26 @@ nodes: - name: tank-0000 addnode: - tank-0001 - ln: - lnd: true + lnd: + enabled: true - name: tank-0001 addnode: - tank-0002 - ln: - lnd: true + lnd: + enabled: true - name: tank-0002 addnode: - tank-0000 - ln: - lnd: true + lnd: + enabled: true - name: tank-0003 addnode: - tank-0000 - ln: - lnd: true lnd: + enabled: true config: | bitcoin.timelockdelta=33 channels: @@ -69,9 +68,8 @@ nodes: - name: tank-0004 addnode: - tank-0000 - ln: - lnd: true lnd: + enabled: true channels: - id: block: 300 @@ -83,8 +81,8 @@ nodes: - name: tank-0005 addnode: - tank-0000 - ln: - lnd: true + lnd: + enabled: true plugins: # Each plugin section has a number of hooks available (preDeploy, postDeploy, etc) preDeploy: # For example, the preDeploy hook means it's plugin will run before all other deploy code