From e0def8fd38732e03efa90504ddcc2caa30005870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Omar=20Vergara=20P=C3=A9rez?= Date: Sat, 22 Feb 2025 21:38:22 -0600 Subject: [PATCH 01/24] add eclair chart --- .../bitcoincore/charts/eclair/.helmignore | 23 +++ .../bitcoincore/charts/eclair/Chart.yaml | 24 ++++ .../charts/eclair/templates/_helpers.tpl | 62 ++++++++ .../charts/eclair/templates/configmap.yaml | 0 .../charts/eclair/templates/pod.yaml | 58 ++++++++ .../charts/eclair/templates/service.yaml | 20 +++ .../bitcoincore/charts/eclair/values.yaml | 134 ++++++++++++++++++ 7 files changed, 321 insertions(+) create mode 100644 resources/charts/bitcoincore/charts/eclair/.helmignore create mode 100644 resources/charts/bitcoincore/charts/eclair/Chart.yaml create mode 100644 resources/charts/bitcoincore/charts/eclair/templates/_helpers.tpl create mode 100644 resources/charts/bitcoincore/charts/eclair/templates/configmap.yaml create mode 100644 resources/charts/bitcoincore/charts/eclair/templates/pod.yaml create mode 100644 resources/charts/bitcoincore/charts/eclair/templates/service.yaml create mode 100644 resources/charts/bitcoincore/charts/eclair/values.yaml 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..2d9f220ce --- /dev/null +++ b/resources/charts/bitcoincore/charts/eclair/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "eclair.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- 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 }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- 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..e69de29bb 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..4010cacfa --- /dev/null +++ b/resources/charts/bitcoincore/charts/eclair/templates/pod.yaml @@ -0,0 +1,58 @@ +apiVersion: apps/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 }} +spec: + {{- with .Values.imagePullSecrets }} + 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 }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- 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..5e7c17db9 --- /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.APIPort }} + targetPort: api + protocol: TCP + name: api + 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..ed9df4bb3 --- /dev/null +++ b/resources/charts/bitcoincore/charts/eclair/values.yaml @@ -0,0 +1,134 @@ +# Default values for eclair. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +namespace: warnet + +# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ +image: + repository: acinq/eclair + # This sets the pull policy for images. + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "release-0.8.0" + +# This is for the secretes for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +# This is for setting Kubernetes Labels to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +podLabels: + app: "warnet" + mission: "lightning" + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/ +service: + # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types + type: ClusterIP + # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports +ServerPort: 9735 +APIPort: 8080 + +# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +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: + httpGet: + path: /getinfo + port: 8080 + failureThreshold: 3 + initialDelaySeconds: 10 + periodSeconds: 20 + timeoutSeconds: 5 +readinessProbe: + initialDelaySeconds: 10 + failureThreshold: 3 + periodSeconds: 10 + tcpSocket: + port: 8080 +startupProbe: + initialDelaySeconds: 15 + periodSeconds: 10 + failureThreshold: 5 + timeoutSeconds: 30 + exec: + command: + - sh + - -c + - 'curl -s http://localhost:8080/getinfo | grep -q "blockHeight"' + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +baseConfig: | + # server port + eclair.server.port=9737 + # node's label + eclair.node-alias="eclair-node" + # rgb node's color + eclair.node-color=49daaa + eclair.api.enabled=true + # Make sure this port isn't accessible from the internet! + eclair.api.port=8080 + #eclair.api.password is set in configmap.yaml + +config: "" + +defaultConfig: "" + +channels: [] From 1f447942f6316c7987088a072a5c189f261ac3d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Omar=20Vergara=20P=C3=A9rez?= Date: Sat, 22 Feb 2025 23:03:17 -0600 Subject: [PATCH 02/24] add eclair as a dependency --- resources/charts/bitcoincore/Chart.yaml | 3 +++ resources/charts/bitcoincore/values.yaml | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/resources/charts/bitcoincore/Chart.yaml b/resources/charts/bitcoincore/Chart.yaml index 4feb6e32e..c004e26f2 100644 --- a/resources/charts/bitcoincore/Chart.yaml +++ b/resources/charts/bitcoincore/Chart.yaml @@ -6,6 +6,9 @@ dependencies: - name: lnd version: 0.1.0 condition: ln.lnd + - name: eclair + version: 0.1.0 + condition: ln.eclair # A chart can be either an 'application' or a 'library' chart. # diff --git a/resources/charts/bitcoincore/values.yaml b/resources/charts/bitcoincore/values.yaml index 8c9f3215f..e5f2e1651 100644 --- a/resources/charts/bitcoincore/values.yaml +++ b/resources/charts/bitcoincore/values.yaml @@ -141,4 +141,5 @@ loadSnapshot: url: "" ln: - lnd: false \ No newline at end of file + lnd: false + eclair: false From 6c54c7b81d3dc799ef97b058b76437d1b353f21a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Omar=20Vergara=20P=C3=A9rez?= Date: Mon, 24 Mar 2025 23:04:17 -0600 Subject: [PATCH 03/24] add cln chart --- .../charts/bitcoincore/charts/cln/.helmignore | 23 ++++ .../charts/bitcoincore/charts/cln/Chart.yaml | 24 ++++ .../charts/cln/templates/_helpers.tpl | 83 ++++++++++++++ .../charts/cln/templates/configmap.yaml | 28 +++++ .../bitcoincore/charts/cln/templates/pod.yaml | 70 ++++++++++++ .../charts/cln/templates/service.yaml | 16 +++ .../charts/cln/templates/servicemonitor.yaml | 15 +++ .../charts/bitcoincore/charts/cln/values.yaml | 106 ++++++++++++++++++ 8 files changed, 365 insertions(+) create mode 100644 resources/charts/bitcoincore/charts/cln/.helmignore create mode 100644 resources/charts/bitcoincore/charts/cln/Chart.yaml create mode 100644 resources/charts/bitcoincore/charts/cln/templates/_helpers.tpl create mode 100644 resources/charts/bitcoincore/charts/cln/templates/configmap.yaml create mode 100644 resources/charts/bitcoincore/charts/cln/templates/pod.yaml create mode 100644 resources/charts/bitcoincore/charts/cln/templates/service.yaml create mode 100644 resources/charts/bitcoincore/charts/cln/templates/servicemonitor.yaml create mode 100644 resources/charts/bitcoincore/charts/cln/values.yaml diff --git a/resources/charts/bitcoincore/charts/cln/.helmignore b/resources/charts/bitcoincore/charts/cln/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/resources/charts/bitcoincore/charts/cln/.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/cln/Chart.yaml b/resources/charts/bitcoincore/charts/cln/Chart.yaml new file mode 100644 index 000000000..79eeda983 --- /dev/null +++ b/resources/charts/bitcoincore/charts/cln/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: cln +description: A Helm chart for CLN + +# 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/cln/templates/_helpers.tpl b/resources/charts/bitcoincore/charts/cln/templates/_helpers.tpl new file mode 100644 index 000000000..de02b029f --- /dev/null +++ b/resources/charts/bitcoincore/charts/cln/templates/_helpers.tpl @@ -0,0 +1,83 @@ +{{/* +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 "cln.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- 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 "cln.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "cln.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "cln.labels" -}} +helm.sh/chart: {{ include "cln.chart" . }} +{{ include "cln.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "cln.selectorLabels" -}} +app.kubernetes.io/name: {{ include "cln.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "cln.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "cln.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/resources/charts/bitcoincore/charts/cln/templates/configmap.yaml b/resources/charts/bitcoincore/charts/cln/templates/configmap.yaml new file mode 100644 index 000000000..6be51ed98 --- /dev/null +++ b/resources/charts/bitcoincore/charts/cln/templates/configmap.yaml @@ -0,0 +1,28 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "cln.fullname" . }} + labels: + {{- include "cln.labels" . | nindent 4 }} +data: + cln.conf: | + {{- .Values.baseConfig | nindent 4 }} + {{- .Values.defaultConfig | nindent 4 }} + {{- .Values.config | nindent 4 }} + network={{ .Values.global.chain }} + bitcoin-rpcconnect={{ include "bitcoincore.fullname" . }} + bitcoin-rpcport={{ index .Values.global .Values.global.chain "RPCPort" }} + bitcoin-rpcpassword={{ .Values.global.rpcpassword }} + alias={{ include "cln.fullname" . }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "cln.fullname" . }}-channels + labels: + channels: "true" + {{- include "cln.labels" . | nindent 4 }} +data: + source: {{ include "cln.fullname" . }} + channels: | + {{ .Values.channels | toJson }} diff --git a/resources/charts/bitcoincore/charts/cln/templates/pod.yaml b/resources/charts/bitcoincore/charts/cln/templates/pod.yaml new file mode 100644 index 000000000..57e5fc03b --- /dev/null +++ b/resources/charts/bitcoincore/charts/cln/templates/pod.yaml @@ -0,0 +1,70 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{ include "cln.fullname" . }} + labels: + {{- include "cln.labels" . | nindent 4 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} + app: {{ include "cln.fullname" . }} + {{- if .Values.collectLogs }} + collect_logs: "true" + {{- end }} + chain: {{ .Values.global.chain }} + annotations: + kubectl.kubernetes.io/default-container: "cln" +spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 4 }} + {{- end }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 4 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 8 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: p2p + containerPort: {{ .Values.P2PPort }} + 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 8 }} + volumeMounts: + {{- with .Values.volumeMounts }} + {{- toYaml . | nindent 8 }} + {{- end }} + - mountPath: /root/.lightning/config + name: config + subPath: config + {{- with .Values.extraContainers }} + {{- toYaml . | nindent 4 }} + {{- end }} + volumes: + {{- with .Values.volumes }} + {{- toYaml . | nindent 4 }} + {{- end }} + - configMap: + name: {{ include "cln.fullname" . }} + name: config + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 4 }} + {{- end }} diff --git a/resources/charts/bitcoincore/charts/cln/templates/service.yaml b/resources/charts/bitcoincore/charts/cln/templates/service.yaml new file mode 100644 index 000000000..c30fb90be --- /dev/null +++ b/resources/charts/bitcoincore/charts/cln/templates/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "cln.fullname" . }} + labels: + {{- include "cln.labels" . | nindent 4 }} + app: {{ include "cln.fullname" . }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.P2PPort }} + targetPort: p2p + protocol: TCP + name: p2p + selector: + {{- include "cln.selectorLabels" . | nindent 4 }} diff --git a/resources/charts/bitcoincore/charts/cln/templates/servicemonitor.yaml b/resources/charts/bitcoincore/charts/cln/templates/servicemonitor.yaml new file mode 100644 index 000000000..4565a17d1 --- /dev/null +++ b/resources/charts/bitcoincore/charts/cln/templates/servicemonitor.yaml @@ -0,0 +1,15 @@ +{{- if .Values.metricsExport }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "cln.fullname" . }} + labels: + app.kubernetes.io/name: cln-metrics + release: prometheus +spec: + endpoints: + - port: prometheus-metrics + selector: + matchLabels: + app: {{ include "cln.fullname" . }} +{{- end }} diff --git a/resources/charts/bitcoincore/charts/cln/values.yaml b/resources/charts/bitcoincore/charts/cln/values.yaml new file mode 100644 index 000000000..88c60dbce --- /dev/null +++ b/resources/charts/bitcoincore/charts/cln/values.yaml @@ -0,0 +1,106 @@ +# Default values for cln. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +namespace: warnet + +# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ +replicaCount: 1 + +image: + repository: elementsproject/lightningd + pullPolicy: IfNotPresent + tag: "v25.02" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +podAnnotations: {} +podLabels: + app: "warnet" + mission: "lightning" + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + +P2PPort: 9735 + +# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +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 + +livenessProbe: + httpGet: + path: / + port: http +readinessProbe: + httpGet: + path: / + port: http + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +baseConfig: | + log-level=debug + bind-addr=0.0.0.0:9735 + bitcoin-rpcuser=user + # bitcoind.rpcpass are set in configmap.yaml + +config: "" + +defaultConfig: "" + +channels: [] From 80c3211bbc56328004574a7a1529fd17e1315a84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Omar=20Vergara=20P=C3=A9rez?= Date: Tue, 25 Mar 2025 13:50:27 -0600 Subject: [PATCH 04/24] add improvements to the cln node --- resources/charts/bitcoincore/Chart.yaml | 3 +++ .../charts/cln/templates/configmap.yaml | 2 +- .../bitcoincore/charts/cln/templates/pod.yaml | 3 +++ .../charts/bitcoincore/charts/cln/values.yaml | 25 ++++++++++++++----- test/data/cln/network.yaml | 19 ++++++++++++++ test/data/cln/node-defaults.yaml | 17 +++++++++++++ 6 files changed, 62 insertions(+), 7 deletions(-) create mode 100644 test/data/cln/network.yaml create mode 100644 test/data/cln/node-defaults.yaml diff --git a/resources/charts/bitcoincore/Chart.yaml b/resources/charts/bitcoincore/Chart.yaml index c004e26f2..00a9cd3aa 100644 --- a/resources/charts/bitcoincore/Chart.yaml +++ b/resources/charts/bitcoincore/Chart.yaml @@ -9,6 +9,9 @@ dependencies: - name: eclair version: 0.1.0 condition: ln.eclair + - name: cln + version: 0.1.0 + condition: ln.cln # A chart can be either an 'application' or a 'library' chart. # diff --git a/resources/charts/bitcoincore/charts/cln/templates/configmap.yaml b/resources/charts/bitcoincore/charts/cln/templates/configmap.yaml index 6be51ed98..48e89c7ae 100644 --- a/resources/charts/bitcoincore/charts/cln/templates/configmap.yaml +++ b/resources/charts/bitcoincore/charts/cln/templates/configmap.yaml @@ -5,7 +5,7 @@ metadata: labels: {{- include "cln.labels" . | nindent 4 }} data: - cln.conf: | + config: | {{- .Values.baseConfig | nindent 4 }} {{- .Values.defaultConfig | nindent 4 }} {{- .Values.config | nindent 4 }} diff --git a/resources/charts/bitcoincore/charts/cln/templates/pod.yaml b/resources/charts/bitcoincore/charts/cln/templates/pod.yaml index 57e5fc03b..6112ad9f2 100644 --- a/resources/charts/bitcoincore/charts/cln/templates/pod.yaml +++ b/resources/charts/bitcoincore/charts/cln/templates/pod.yaml @@ -31,6 +31,9 @@ spec: - name: p2p containerPort: {{ .Values.P2PPort }} protocol: TCP + command: + - "lightningd" + - "--conf=/root/.lightning/config" livenessProbe: {{- toYaml .Values.livenessProbe | nindent 8 }} readinessProbe: diff --git a/resources/charts/bitcoincore/charts/cln/values.yaml b/resources/charts/bitcoincore/charts/cln/values.yaml index 88c60dbce..8f5f9dc82 100644 --- a/resources/charts/bitcoincore/charts/cln/values.yaml +++ b/resources/charts/bitcoincore/charts/cln/values.yaml @@ -66,13 +66,26 @@ resources: {} # memory: 128Mi livenessProbe: - httpGet: - path: / - port: http + exec: + command: + - "/bin/sh" + - "-c" + - "lightning-cli --network=regtest getinfo >/dev/null 2>&1" + failureThreshold: 3 + initialDelaySeconds: 5 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 1 readinessProbe: - httpGet: - path: / - port: http + failureThreshold: 10 + periodSeconds: 30 + successThreshold: 1 + timeoutSeconds: 60 + exec: + command: + - "/bin/sh" + - "-c" + - "lightning-cli --network=regtest getinfo 2>/dev/null | grep -q 'id' || exit 1" # Additional volumes on the output Deployment definition. volumes: [] diff --git a/test/data/cln/network.yaml b/test/data/cln/network.yaml new file mode 100644 index 000000000..f7748ba5d --- /dev/null +++ b/test/data/cln/network.yaml @@ -0,0 +1,19 @@ +nodes: + - name: tank-0000 + addnode: + - tank-0001 + - name: tank-0001 + addnode: + - tank-0002 + - name: tank-0002 + addnode: + - tank-0000 + - name: tank-0003 + addnode: + - tank-0000 + - name: tank-0004 + addnode: + - tank-0000 + - name: tank-0005 + addnode: + - tank-0000 diff --git a/test/data/cln/node-defaults.yaml b/test/data/cln/node-defaults.yaml new file mode 100644 index 000000000..0132e1603 --- /dev/null +++ b/test/data/cln/node-defaults.yaml @@ -0,0 +1,17 @@ +# enable collectLogs and metricsExport to activate publish lnd-exporter metrics + +#Core configs +image: + repository: bitcoindevproject/bitcoin + pullPolicy: IfNotPresent + tag: "27.0" +collectLogs: false +metricsExport: false + +#LN configs +ln: + cln: true +cln: + defaultConfig: | + rgb=ff3155 + metricsExport: false From 02609bc12e032b1dc148b22791584a6c84c56e7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Omar=20Vergara=20P=C3=A9rez?= Date: Wed, 26 Mar 2025 22:15:40 -0600 Subject: [PATCH 05/24] fixes for eclair --- .../charts/eclair/templates/configmap.yaml | 30 +++++++++++++++++++ .../charts/eclair/templates/pod.yaml | 30 ++++++++++++------- .../charts/eclair/templates/service.yaml | 6 ++-- .../bitcoincore/charts/eclair/values.yaml | 22 +++++--------- resources/charts/bitcoincore/values.yaml | 1 + test/data/eclair/network.yaml | 19 ++++++++++++ test/data/eclair/node-defaults.yaml | 17 +++++++++++ 7 files changed, 98 insertions(+), 27 deletions(-) create mode 100644 test/data/eclair/network.yaml create mode 100644 test/data/eclair/node-defaults.yaml diff --git a/resources/charts/bitcoincore/charts/eclair/templates/configmap.yaml b/resources/charts/bitcoincore/charts/eclair/templates/configmap.yaml index e69de29bb..6cdb1ca82 100644 --- a/resources/charts/bitcoincore/charts/eclair/templates/configmap.yaml +++ b/resources/charts/bitcoincore/charts/eclair/templates/configmap.yaml @@ -0,0 +1,30 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "eclair.fullname" . }} + labels: + {{- include "eclair.labels" . | nindent 4 }} +data: + config: | + {{- .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.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 }} +--- +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 index 4010cacfa..3e5543510 100644 --- a/resources/charts/bitcoincore/charts/eclair/templates/pod.yaml +++ b/resources/charts/bitcoincore/charts/eclair/templates/pod.yaml @@ -1,4 +1,4 @@ -apiVersion: apps/v1 +apiVersion: v1 kind: Pod metadata: name: {{ include "eclair.fullname" . }} @@ -12,6 +12,8 @@ metadata: collect_logs: "true" {{- end }} chain: {{ .Values.global.chain }} + annotations: + kubectl.kubernetes.io/default-container: "eclair" spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: @@ -26,8 +28,11 @@ spec: image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} ports: - - name: http - containerPort: {{ .Values.service.port }} + - name: server + containerPort: {{ .Values.ServerPort }} + protocol: TCP + - name: rest + containerPort: {{ .Values.RestPort }} protocol: TCP livenessProbe: {{- toYaml .Values.livenessProbe | nindent 12 }} @@ -35,15 +40,20 @@ spec: {{- toYaml .Values.readinessProbe | nindent 12 }} resources: {{- toYaml .Values.resources | nindent 12 }} - - {{- with .Values.volumeMounts }} volumeMounts: - {{- toYaml . | nindent 12 }} - {{- end }} - {{- with .Values.volumes }} + {{- with .Values.volumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + - mountPath: /home/eclair/.eclair/eclair.conf + name: config + subPath: eclair.confg volumes: - {{- toYaml . | nindent 8 }} - {{- end }} + {{- with .Values.volumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + - configMap: + name: {{ include "eclair.fullname" . }} + name: config {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} diff --git a/resources/charts/bitcoincore/charts/eclair/templates/service.yaml b/resources/charts/bitcoincore/charts/eclair/templates/service.yaml index 5e7c17db9..f1458557a 100644 --- a/resources/charts/bitcoincore/charts/eclair/templates/service.yaml +++ b/resources/charts/bitcoincore/charts/eclair/templates/service.yaml @@ -12,9 +12,9 @@ spec: targetPort: server protocol: TCP name: server - - port: {{ .Values.APIPort }} - targetPort: api + - port: {{ .Values.RestPort }} + targetPort: rest protocol: TCP - name: api + 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 index ed9df4bb3..c57adf6be 100644 --- a/resources/charts/bitcoincore/charts/eclair/values.yaml +++ b/resources/charts/bitcoincore/charts/eclair/values.yaml @@ -9,7 +9,7 @@ image: # This sets the pull policy for images. pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. - tag: "release-0.8.0" + tag: "latest" # This is for the secretes for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ imagePullSecrets: [] @@ -33,13 +33,11 @@ securityContext: {} # runAsNonRoot: true # runAsUser: 1000 -# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/ service: - # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types type: ClusterIP - # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports + ServerPort: 9735 -APIPort: 8080 +RestPort: 8080 # This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ ingress: @@ -116,17 +114,13 @@ tolerations: [] affinity: {} baseConfig: | - # server port - eclair.server.port=9737 - # node's label - eclair.node-alias="eclair-node" - # rgb node's color - eclair.node-color=49daaa + eclair.server.port=9735 eclair.api.enabled=true - # Make sure this port isn't accessible from the internet! + eclair.api.password=foo eclair.api.port=8080 - #eclair.api.password is set in configmap.yaml - + eclair.bitcoind.rpcuser=user + # zmq* and eclair.bitcoind.rpcpassword are set in configmap.yaml + config: "" defaultConfig: "" diff --git a/resources/charts/bitcoincore/values.yaml b/resources/charts/bitcoincore/values.yaml index e5f2e1651..7b049ae65 100644 --- a/resources/charts/bitcoincore/values.yaml +++ b/resources/charts/bitcoincore/values.yaml @@ -143,3 +143,4 @@ loadSnapshot: ln: lnd: false eclair: false + cln: false diff --git a/test/data/eclair/network.yaml b/test/data/eclair/network.yaml new file mode 100644 index 000000000..f7748ba5d --- /dev/null +++ b/test/data/eclair/network.yaml @@ -0,0 +1,19 @@ +nodes: + - name: tank-0000 + addnode: + - tank-0001 + - name: tank-0001 + addnode: + - tank-0002 + - name: tank-0002 + addnode: + - tank-0000 + - name: tank-0003 + addnode: + - tank-0000 + - name: tank-0004 + addnode: + - tank-0000 + - name: tank-0005 + addnode: + - tank-0000 diff --git a/test/data/eclair/node-defaults.yaml b/test/data/eclair/node-defaults.yaml new file mode 100644 index 000000000..3ef1d9656 --- /dev/null +++ b/test/data/eclair/node-defaults.yaml @@ -0,0 +1,17 @@ +# enable collectLogs and metricsExport to activate publish lnd-exporter metrics + +#Core configs +image: + repository: bitcoindevproject/bitcoin + pullPolicy: IfNotPresent + tag: "27.0" +collectLogs: false +metricsExport: false + +#LN configs +ln: + eclair: true +eclair: + defaultConfig: | + eclair.node-color=49daaa + metricsExport: false From 814793c421aa143e091e2947e25edeade6ed73f8 Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Sat, 5 Apr 2025 10:00:53 -0400 Subject: [PATCH 06/24] checkpoint cln integrated into ln_framework issues: - restoring wallet produces same spend address on two nodes - lnd node connecting to cln produces infinite reconnect issue - system resource issue - two cln nodes can not find each other - gossip error --- .../charts/cln/templates/configmap.yaml | 26 +++ .../bitcoincore/charts/cln/templates/pod.yaml | 20 +- .../charts/cln/templates/service.yaml | 4 + .../charts/bitcoincore/charts/cln/values.yaml | 12 +- .../charts/lnd/templates/_helpers.tpl | 4 +- resources/scenarios/commander.py | 10 +- resources/scenarios/ln_framework/ln.py | 190 ++++++++++++++++-- resources/scenarios/ln_init.py | 65 +++--- src/warnet/constants.py | 1 - src/warnet/deploy.py | 12 +- src/warnet/graph.py | 4 +- test/ln_basic_test.py | 4 +- 12 files changed, 289 insertions(+), 63 deletions(-) diff --git a/resources/charts/bitcoincore/charts/cln/templates/configmap.yaml b/resources/charts/bitcoincore/charts/cln/templates/configmap.yaml index 48e89c7ae..41f5f3f03 100644 --- a/resources/charts/bitcoincore/charts/cln/templates/configmap.yaml +++ b/resources/charts/bitcoincore/charts/cln/templates/configmap.yaml @@ -10,10 +10,36 @@ data: {{- .Values.defaultConfig | nindent 4 }} {{- .Values.config | nindent 4 }} network={{ .Values.global.chain }} + bind-addr=0.0.0.0:{{ .Values.P2PPort }} + clnrest-port={{ .Values.RestPort }} bitcoin-rpcconnect={{ include "bitcoincore.fullname" . }} bitcoin-rpcport={{ index .Values.global .Values.global.chain "RPCPort" }} bitcoin-rpcpassword={{ .Values.global.rpcpassword }} alias={{ include "cln.fullname" . }} + announce-addr=dns:{{ include "cln.fullname" . }}:9735 + database-upgrade=true + bitcoin-retry-timeout=600 + clnrest-certs=/root/.lightning + tls.cert: | + -----BEGIN CERTIFICATE----- + MIIB8TCCAZagAwIBAgIUJDsR6mmY+TaO9pCfjtotlbOkzJMwCgYIKoZIzj0EAwIw + MjEfMB0GA1UECgwWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEPMA0GA1UEAwwGd2Fy + bmV0MB4XDTI0MTExMTE2NTM1MFoXDTM0MTEwOTE2NTM1MFowMjEfMB0GA1UECgwW + bG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEPMA0GA1UEAwwGd2FybmV0MFkwEwYHKoZI + zj0CAQYIKoZIzj0DAQcDQgAEBVltIvaTlAQI/3FFatTqVflZuZdRJ0SmRMSJrFLP + tp0fxE7hmteSt6gjQriy90fP8j9OJXBNAjt915kLY4zVvqOBiTCBhjAOBgNVHQ8B + Af8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUwAwEB/zAd + BgNVHQ4EFgQU5d8QMrwhLgTkDjWA+eXZGz+dybUwLwYDVR0RBCgwJoIJbG9jYWxo + b3N0ggEqhwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMAoGCCqGSM49BAMCA0kAMEYC + IQDPofN0fEl5gTwCYhk3nZbjMqJhZ8BsSJ6K8XRhxr7zbwIhAPsgQCFOqUWg632O + NEO53OQ6CIqnpxSskjsFNH4ZBQOE + -----END CERTIFICATE----- + tls.key: | + -----BEGIN EC PRIVATE KEY----- + MHcCAQEEIIcFtWTLQv5JaRRxdkPKkO98OrvgeztbZ7h8Ev/4UbE4oAoGCCqGSM49 + AwEHoUQDQgAEBVltIvaTlAQI/3FFatTqVflZuZdRJ0SmRMSJrFLPtp0fxE7hmteS + t6gjQriy90fP8j9OJXBNAjt915kLY4zVvg== + -----END EC PRIVATE KEY----- --- apiVersion: v1 kind: ConfigMap diff --git a/resources/charts/bitcoincore/charts/cln/templates/pod.yaml b/resources/charts/bitcoincore/charts/cln/templates/pod.yaml index 6112ad9f2..b632ca396 100644 --- a/resources/charts/bitcoincore/charts/cln/templates/pod.yaml +++ b/resources/charts/bitcoincore/charts/cln/templates/pod.yaml @@ -31,9 +31,19 @@ spec: - name: p2p containerPort: {{ .Values.P2PPort }} protocol: TCP + - name: rest + containerPort: {{ .Values.RestPort }} + protocol: TCP command: - - "lightningd" - - "--conf=/root/.lightning/config" + - /bin/sh + - -c + - | + lightningd --conf=/root/.lightning/config --recover=35b8182d6db5cec40d9bead20607b7c9b91ed89997a290bc0e0f07e5922e4714 & + pid=$!; + sleep 10; + kill $pid; + sleep 1; + lightningd --conf=/root/.lightning/config livenessProbe: {{- toYaml .Values.livenessProbe | nindent 8 }} readinessProbe: @@ -49,6 +59,12 @@ spec: - mountPath: /root/.lightning/config name: config subPath: config + - mountPath: /root/.lightning/cert + name: config + subPath: tls.cert + - mountPath: /root/.lightning/key + name: config + subPath: tls.key {{- with .Values.extraContainers }} {{- toYaml . | nindent 4 }} {{- end }} diff --git a/resources/charts/bitcoincore/charts/cln/templates/service.yaml b/resources/charts/bitcoincore/charts/cln/templates/service.yaml index c30fb90be..4848d8c5a 100644 --- a/resources/charts/bitcoincore/charts/cln/templates/service.yaml +++ b/resources/charts/bitcoincore/charts/cln/templates/service.yaml @@ -12,5 +12,9 @@ spec: targetPort: p2p protocol: TCP name: p2p + - port: {{ .Values.RestPort }} + targetPort: rest + protocol: TCP + name: rest selector: {{- include "cln.selectorLabels" . | nindent 4 }} diff --git a/resources/charts/bitcoincore/charts/cln/values.yaml b/resources/charts/bitcoincore/charts/cln/values.yaml index 8f5f9dc82..58859c02d 100644 --- a/resources/charts/bitcoincore/charts/cln/values.yaml +++ b/resources/charts/bitcoincore/charts/cln/values.yaml @@ -3,9 +3,6 @@ # Declare variables to be passed into your templates. namespace: warnet -# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ -replicaCount: 1 - image: repository: elementsproject/lightningd pullPolicy: IfNotPresent @@ -35,6 +32,7 @@ service: type: ClusterIP P2PPort: 9735 +RestPort: 3010 # This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ ingress: @@ -89,10 +87,6 @@ readinessProbe: # Additional volumes on the output Deployment definition. volumes: [] -# - name: foo -# secret: -# secretName: mysecret -# optional: false # Additional volumeMounts on the output Deployment definition. volumeMounts: [] @@ -108,8 +102,10 @@ affinity: {} baseConfig: | log-level=debug - bind-addr=0.0.0.0:9735 + developer + dev-fast-gossip bitcoin-rpcuser=user + clnrest-host=0.0.0.0 # bitcoind.rpcpass are set in configmap.yaml config: "" diff --git a/resources/charts/bitcoincore/charts/lnd/templates/_helpers.tpl b/resources/charts/bitcoincore/charts/lnd/templates/_helpers.tpl index de7c0c156..e3451356c 100644 --- a/resources/charts/bitcoincore/charts/lnd/templates/_helpers.tpl +++ b/resources/charts/bitcoincore/charts/lnd/templates/_helpers.tpl @@ -23,7 +23,7 @@ If release name contains chart name it will be used as a full name. Expand the name of the chart. */}} {{- define "lnd.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}-ln +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}-lnd {{- end }} {{/* @@ -35,7 +35,7 @@ If release name contains chart name it will be used as a full name. {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} -{{- printf "%s" .Release.Name | trunc 63 | trimSuffix "-" }}-ln +{{- printf "%s" .Release.Name | trunc 63 | trimSuffix "-" }}-lnd {{- end }} {{- end }} diff --git a/resources/scenarios/commander.py b/resources/scenarios/commander.py index 8a50b2fc5..814a87b7a 100644 --- a/resources/scenarios/commander.py +++ b/resources/scenarios/commander.py @@ -14,7 +14,7 @@ from typing import Dict from kubernetes import client, config -from ln_framework.ln import LND +from ln_framework.ln import CLN, LND, LNNode from test_framework.authproxy import AuthServiceProxy from test_framework.p2p import NetworkThread from test_framework.test_framework import ( @@ -161,7 +161,7 @@ def setup(self): # Keep a separate index of tanks by pod name self.tanks: Dict[str, TestNode] = {} - self.lns: Dict[str, LND] = {} + self.lns: Dict[str, LNNode] = {} self.channels = WARNET["channels"] for i, tank in enumerate(WARNET["tanks"]): @@ -194,7 +194,11 @@ def setup(self): self.tanks[tank["tank"]] = node for ln in WARNET["lightning"]: - self.lns[ln] = LND(ln) + #create the correct implementation based on pod name + if "-cln" in ln: + self.lns[ln] = CLN(ln) + else: + self.lns[ln] = LND(ln) self.num_nodes = len(self.nodes) diff --git a/resources/scenarios/ln_framework/ln.py b/resources/scenarios/ln_framework/ln.py index 8fcdc1bc7..328fc88ce 100644 --- a/resources/scenarios/ln_framework/ln.py +++ b/resources/scenarios/ln_framework/ln.py @@ -1,11 +1,15 @@ +from abc import ABC, abstractmethod import http.client +import logging import json import ssl -import time +from time import sleep # hard-coded deterministic lnd credentials ADMIN_MACAROON_HEX = "0201036c6e6402f801030a1062beabbf2a614b112128afa0c0b4fdd61201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e657261746512047265616400000620b17be53e367290871681055d0de15587f6d1cd47d1248fe2662ae27f62cfbdc6" -# Don't worry about lnd's self-signed certificates +# hard-coded deterministic cln credentials - rune +ADMIN_RUNE = "_y4Av-cXE9OKqclcNEVblZEMfxNjV1C-Jbc7KBqB2To9MA==" +# Don't worry about ln's self-signed certificates INSECURE_CONTEXT = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) INSECURE_CONTEXT.check_hostname = False INSECURE_CONTEXT.verify_mode = ssl.CERT_NONE @@ -72,13 +76,158 @@ def to_lnd_chanpolicy(self, capacity): "min_htlc_msat_specified": True, } - -class LND: +class LNNode(ABC): + @abstractmethod def __init__(self, pod_name): + self.log = logging.getLogger(self.__class__.__name__) self.name = pod_name + + @abstractmethod + def get(self, uri, max_tries=10) -> str: + pass + + @abstractmethod + def post(self, uri, data, max_tries=10) -> str: + pass + + @abstractmethod + def newaddress(self, max_tries=10) -> tuple[bool, str]: + pass + + @abstractmethod + def uri(self) -> str: + pass + + @abstractmethod + def walletbalance(self) -> int: + pass + + @abstractmethod + def graph(self): + pass + + +class CLN(LNNode): + def __init__(self, pod_name): + super().__init__(pod_name) + self.headers = {"Rune": ADMIN_RUNE} + self.impl = "cln" + self.reset_connection() + + def reset_connection(self): + self.conn = http.client.HTTPSConnection( + host=self.name, port=3010, timeout=5, context=INSECURE_CONTEXT + ) + + def get(self, uri, max_tries=2): + attempt=0 + while attempt < max_tries: + attempt+=1 + try: + self.conn.request( + method="GET", + url=uri, + headers=self.headers, + ) + return self.conn.getresponse().read().decode("utf8") + except Exception as e: + self.log.info(f"GET clnrest error: {e}") + self.reset_connection() + sleep(1) + return None + + def post(self, uri, data = {}, max_tries=2): + body = json.dumps(data) + attempt=0 + while attempt < max_tries: + attempt+=1 + try: + self.conn.request( + method="POST", + url=uri, + body=body, + headers=self.headers, + ) + return self.conn.getresponse().read().decode("utf8") + except Exception as e: + print(f"POST clnrest error: {e}") + self.reset_connection() + sleep(2) + return None + + def newaddress(self, max_tries=2): + attempt=0 + while attempt < max_tries: + attempt+=1 + response = self.post("/v1/newaddr") + res = json.loads(response) + if "bech32" in res: + return True, res["bech32"] + else: + self.log.info( + f"Couldn't get wallet address from {self.name}:\n {res}\n wait and retry..." + ) + sleep(1) + return False, "" + + def uri(self): + res = json.loads(self.post("/v1/getinfo")) + if len(res["address"]) < 1: + return None + return f'{res["id"]}@{res["address"][0]["address"]}:{res["address"][0]["port"]}' + + def walletbalance(self): + res = json.loads(self.post("/v1/listfunds")) + return int(sum(o["amount_msat"] for o in res["outputs"]) / 1000) + + def connect(self, target_uri, max_tries=2): + pk, host = target_uri.split("@") + attempt=0 + while attempt < max_tries: + attempt+=1 + res = self.post("/v1/connect", data={"id": pk, "host": host}) + if res: + return json.loads(res) + else: + print(f"connect response: {res}") + sleep(5) + return "" + + def channel(self, pk, capacity, push_amt, fee_rate, max_tries=5): + attempt=0 + while attempt < max_tries: + attempt+=1 + res = self.post( + "/v1/fundchannel", + data={ + "amount": capacity, + "push_msat": push_amt, + "id": pk, + "feerate": fee_rate, + }, + ) + if res: + return json.loads(res) + else: + print(f"channel response: {res}") + sleep(5) + return "" + + def graph(self): + res = self.post("/v1/graph") + return json.loads(res) + +class LND(LNNode): + def __init__(self, pod_name): + super().__init__(pod_name) self.conn = http.client.HTTPSConnection( host=pod_name, port=8080, timeout=5, context=INSECURE_CONTEXT ) + self.headers = { + "Grpc-Metadata-macaroon": ADMIN_MACAROON_HEX, + "Connection": "close", + } + self.impl = "lnd" def get(self, uri): while True: @@ -86,14 +235,17 @@ def get(self, uri): self.conn.request( method="GET", url=uri, - headers={"Grpc-Metadata-macaroon": ADMIN_MACAROON_HEX, "Connection": "close"}, + headers=self.headers, ) return self.conn.getresponse().read().decode("utf8") except Exception: - time.sleep(1) + sleep(1) def post(self, uri, data): body = json.dumps(data) + post_header=self.headers + post_header["Content-Length"]=str(len(body)) + post_header["Content-Type"] = "application/json" attempt = 0 while True: attempt += 1 @@ -102,12 +254,7 @@ def post(self, uri, data): method="POST", url=uri, body=body, - headers={ - "Content-Type": "application/json", - "Content-Length": str(len(body)), - "Grpc-Metadata-macaroon": ADMIN_MACAROON_HEX, - "Connection": "close", - }, + headers=post_header, ) # Stream output, otherwise we get a timeout error res = self.conn.getresponse() @@ -123,11 +270,22 @@ def post(self, uri, data): break return stream except Exception: - time.sleep(1) + sleep(1) - def newaddress(self): - res = self.get("/v1/newaddress") - return json.loads(res) + def newaddress(self, max_tries=10): + attempt=0 + while attempt < max_tries: + attempt+=1 + response = self.get("/v1/newaddress") + res = json.loads(response) + if "address" in res: + return True, res["address"] + else: + self.log.info( + f"Couldn't get wallet address from {self.name}:\n {res}\n wait and retry..." + ) + sleep(1) + return False, "" def walletbalance(self): res = self.get("/v1/balance/blockchain") diff --git a/resources/scenarios/ln_init.py b/resources/scenarios/ln_init.py index 96dd8161f..371e4a52f 100644 --- a/resources/scenarios/ln_init.py +++ b/resources/scenarios/ln_init.py @@ -6,6 +6,7 @@ from commander import Commander from ln_framework.ln import Policy +THREAD_JOIN_TIMEOUT=15 class LNInit(Commander): def set_test_params(self): @@ -15,6 +16,9 @@ def add_options(self, parser): parser.description = "Fund LN wallets and open channels" parser.usage = "warnet run /path/to/ln_init.py" + def node_names(self, nodes): + return [item.name for item in nodes] + def run_test(self): ## # L1 P2P @@ -40,20 +44,16 @@ def gen(n): ## self.log.info("Getting LN wallet addresses...") ln_addrs = [] + ln_nodes = [] def get_ln_addr(self, ln): - while True: - res = ln.newaddress() - if "address" in res: - addr = res["address"] - ln_addrs.append(addr) - self.log.info(f"Got wallet address {addr} from {ln.name}") - break - else: - self.log.info( - f"Couldn't get wallet address from {ln.name}:\n {res}\n wait and retry..." - ) - sleep(1) + success, address = ln.newaddress() + if success: + ln_addrs.append(address) + ln_nodes.append(ln) + self.log.info(f"Got wallet address {address} from {ln.name}") + else: + self.log.info(f"Couldn't get wallet address from {ln.name}") addr_threads = [ threading.Thread(target=get_ln_addr, args=(self, ln)) for ln in self.lns.values() @@ -61,7 +61,7 @@ def get_ln_addr(self, ln): for thread in addr_threads: thread.start() - all(thread.join() is None for thread in addr_threads) + all(thread.join(timeout=THREAD_JOIN_TIMEOUT) is None for thread in addr_threads) self.log.info(f"Got {len(ln_addrs)} addresses from {len(self.lns)} LN nodes") ## @@ -95,12 +95,12 @@ def confirm_ln_balance(self, ln): sleep(1) fund_threads = [ - threading.Thread(target=confirm_ln_balance, args=(self, ln)) for ln in self.lns.values() + threading.Thread(target=confirm_ln_balance, args=(self, ln)) for ln in ln_nodes ] for thread in fund_threads: thread.start() - all(thread.join() is None for thread in fund_threads) + all(thread.join(timeout=THREAD_JOIN_TIMEOUT) is None for thread in fund_threads) self.log.info("All LN nodes are funded") ## @@ -120,12 +120,12 @@ def get_ln_uri(self, ln): sleep(1) uri_threads = [ - threading.Thread(target=get_ln_uri, args=(self, ln)) for ln in self.lns.values() + threading.Thread(target=get_ln_uri, args=(self, ln)) for ln in ln_nodes ] for thread in uri_threads: thread.start() - all(thread.join() is None for thread in uri_threads) + all(thread.join(timeout=THREAD_JOIN_TIMEOUT) is None for thread in uri_threads) self.log.info("Got URIs from all LN nodes") ## @@ -135,13 +135,18 @@ def get_ln_uri(self, ln): # (source: LND, target_uri: str) tuples of LND instances connections = [] # Cycle graph through all LN nodes - nodes = list(self.lns.values()) + nodes = list(ln_nodes) prev_node = nodes[-1] for node in nodes: connections.append((node, prev_node)) prev_node = node # Explicit connections between every pair of channel partners for ch in self.channels: + node_names = self.node_names(ln_nodes) + if not ch["source"] in node_names or not ch["target"] in node_names: + self.log.info(f"LN Channel {ch} not available, removing") + self.channels.remove(ch) + continue src = self.lns[ch["source"]] tgt = self.lns[ch["target"]] # Avoid duplicates and reciprocals @@ -150,6 +155,9 @@ def get_ln_uri(self, ln): def connect_ln(self, pair): while True: + if not pair[1].name in ln_uris: + self.log.info(f"LN URIs for {pair[1].name} not found") + break res = pair[0].connect(ln_uris[pair[1].name]) if res == {}: self.log.info(f"Connected LN nodes {pair[0].name} -> {pair[1].name}") @@ -177,7 +185,7 @@ def connect_ln(self, pair): for thread in p2p_threads: thread.start() - all(thread.join() is None for thread in p2p_threads) + all(thread.join(timeout=THREAD_JOIN_TIMEOUT) is None for thread in p2p_threads) self.log.info("Established all LN p2p connections") ## @@ -188,7 +196,9 @@ def connect_ln(self, pair): # so their channel ids are deterministic ch_by_block = {} for ch in self.channels: - # TODO: if "id" not in ch ... + if not "id" in ch or not "block" in ch["id"]: + self.log.info(f"LN Channel {ch} not found") + continue block = ch["id"]["block"] if block not in ch_by_block: ch_by_block[block] = [ch] @@ -207,14 +217,19 @@ def connect_ln(self, pair): gen(need - 1) def open_channel(self, ch, fee_rate): + if not ch["source"] in self.lns or not ch["target"] in ln_uris: + return src = self.lns[ch["source"]] tgt_uri = ln_uris[ch["target"]] tgt_pk, _ = tgt_uri.split("@") + if src.impl == "lnd": + tgt_pk = self.hex_to_b64(tgt_pk) self.log.info( f"Sending channel open from {ch['source']} -> {ch['target']} with fee_rate={fee_rate}" ) + res = src.channel( - pk=self.hex_to_b64(tgt_pk), + pk=tgt_pk, capacity=ch["capacity"], push_amt=ch["push_amt"], fee_rate=fee_rate, @@ -247,7 +262,7 @@ def open_channel(self, ch, fee_rate): t.start() ch_threads.append(t) - all(thread.join() is None for thread in ch_threads) + all(thread.join(timeout=THREAD_JOIN_TIMEOUT) is None for thread in ch_threads) self.log.info(f"Waiting for {len(channels)} channel opens in mempool...") self.wait_until( lambda channels=channels: self.nodes[0].getmempoolinfo()["size"] >= len(channels), @@ -283,7 +298,7 @@ def ln_all_chs(self, ln): for thread in ch_ann_threads: thread.start() - all(thread.join() is None for thread in ch_ann_threads) + all(thread.join(timeout=THREAD_JOIN_TIMEOUT) is None for thread in ch_ann_threads) self.log.info("All LN nodes have complete graph") ## @@ -328,7 +343,7 @@ def update_policy(self, ln, txid_hex, policy, capacity): update_threads.append(tt) count = len(update_threads) - all(thread.join() is None for thread in update_threads) + all(thread.join(timeout=THREAD_JOIN_TIMEOUT) is None for thread in update_threads) self.log.info(f"Sent {count} channel policy updates") self.log.info("Waiting for all channel policy gossip to synchronize...") @@ -382,7 +397,7 @@ def matching_graph(self, expected, ln): for thread in policy_threads: thread.start() - all(thread.join() is None for thread in policy_threads) + all(thread.join(timeout=THREAD_JOIN_TIMEOUT) is None for thread in policy_threads) self.log.info("All LN nodes have matching graph!") diff --git a/src/warnet/constants.py b/src/warnet/constants.py index 017c9a749..5e79bea8c 100644 --- a/src/warnet/constants.py +++ b/src/warnet/constants.py @@ -65,7 +65,6 @@ class AnnexMember(Enum): # Helm charts BITCOIN_CHART_LOCATION = str(CHARTS_DIR.joinpath("bitcoincore")) -LND_CHART_LOCATION = str(CHARTS_DIR.joinpath("lnd")) FORK_OBSERVER_CHART = str(CHARTS_DIR.joinpath("fork-observer")) COMMANDER_CHART = str(CHARTS_DIR.joinpath("commander")) NAMESPACES_CHART_LOCATION = CHARTS_DIR.joinpath("namespaces") diff --git a/src/warnet/deploy.py b/src/warnet/deploy.py index 20e431f29..4bf5a09e8 100644 --- a/src/warnet/deploy.py +++ b/src/warnet/deploy.py @@ -114,7 +114,15 @@ def _deploy(directory, debug, namespace, to_all_users): processes.append(caddy_process) # Wait for the network process to complete - network_process.join() + print(f"Waiting for network process thread {network_process.pid} join") + network_process.join(timeout=300) + if network_process.is_alive(): + print("Process hit the timeout (still running after 300 seconds)") + # Optionally terminate the process if it timed out + network_process.terminate() + else: + print(f"Network process completed before time limit") + # input("Press Enter to continue...") run_plugins(directory, HookValue.POST_NETWORK, namespace) @@ -366,7 +374,7 @@ def deploy_network(directory: Path, debug: bool = False, namespace: Optional[str needs_ln_init = False for node in network_file["nodes"]: - if "lnd" in node and "channels" in node["lnd"] and len(node["lnd"]["channels"]) > 0: + if any(node.get("ln", {}).get(key, False) for key in ["lnd", "cln", "eclair"]): needs_ln_init = True break diff --git a/src/warnet/graph.py b/src/warnet/graph.py index d06387710..7d195cd83 100644 --- a/src/warnet/graph.py +++ b/src/warnet/graph.py @@ -253,7 +253,7 @@ def _import_network(graph_file_path, output_path): tank = f"tank-{index:04d}" pk_to_tank[node["pub_key"]] = tank tank_to_pk[tank] = node["pub_key"] - tanks[tank] = {"name": tank, "ln": {"lnd": True}, "lnd": {"channels": []}} + tanks[tank] = {"name": tank, "ln": {"lnd": True, "channels": []}} index += 1 print(f"Imported {index} nodes") @@ -274,7 +274,7 @@ def _import_network(graph_file_path, output_path): "source_policy": Policy.from_lnd_describegraph(edge["node1_policy"]).to_dict(), "target_policy": Policy.from_lnd_describegraph(edge["node2_policy"]).to_dict(), } - tanks[source]["lnd"]["channels"].append(channel) + tanks[source]["ln"]["channels"].append(channel) index += 1 if index > 1000: index = 1 diff --git a/test/ln_basic_test.py b/test/ln_basic_test.py index fdb479dbd..dbbbb767a 100755 --- a/test/ln_basic_test.py +++ b/test/ln_basic_test.py @@ -47,8 +47,8 @@ def setup_network(self): def fund_wallets(self): outputs = "" - for lnd in self.lns: - addr = json.loads(self.warnet(f"ln rpc {lnd} newaddress p2wkh"))["address"] + for ln in self.lns: + addr = json.loads(self.warnet(f"ln rpc {ln} newaddress p2wkh"))["address"] outputs += f',"{addr}":10' # trim first comma outputs = outputs[1:] From 4df0c3370c59eacf06d1ad701cfea72265d87769 Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Mon, 14 Apr 2025 16:07:35 -0400 Subject: [PATCH 07/24] move certs from cln nodes to simln --- .../charts/cln/templates/configmap.yaml | 24 +- .../bitcoincore/charts/cln/templates/pod.yaml | 16 +- .../charts/cln/templates/service.yaml | 4 - .../charts/bitcoincore/charts/cln/values.yaml | 5 +- .../charts/commander/templates/rbac.yaml | 4 +- resources/plugins/simln/plugin.py | 46 +++- resources/scenarios/ln_framework/ln.py | 230 ++++++++++++------ resources/scenarios/ln_init.py | 33 ++- src/warnet/deploy.py | 4 +- src/warnet/k8s.py | 35 +++ 10 files changed, 256 insertions(+), 145 deletions(-) diff --git a/resources/charts/bitcoincore/charts/cln/templates/configmap.yaml b/resources/charts/bitcoincore/charts/cln/templates/configmap.yaml index 41f5f3f03..5936fdd19 100644 --- a/resources/charts/bitcoincore/charts/cln/templates/configmap.yaml +++ b/resources/charts/bitcoincore/charts/cln/templates/configmap.yaml @@ -11,35 +11,13 @@ data: {{- .Values.config | nindent 4 }} network={{ .Values.global.chain }} bind-addr=0.0.0.0:{{ .Values.P2PPort }} - clnrest-port={{ .Values.RestPort }} bitcoin-rpcconnect={{ include "bitcoincore.fullname" . }} bitcoin-rpcport={{ index .Values.global .Values.global.chain "RPCPort" }} bitcoin-rpcpassword={{ .Values.global.rpcpassword }} alias={{ include "cln.fullname" . }} - announce-addr=dns:{{ include "cln.fullname" . }}:9735 + announce-addr=dns:{{ include "cln.fullname" . }}:{{ .Values.P2PPort }} database-upgrade=true bitcoin-retry-timeout=600 - clnrest-certs=/root/.lightning - tls.cert: | - -----BEGIN CERTIFICATE----- - MIIB8TCCAZagAwIBAgIUJDsR6mmY+TaO9pCfjtotlbOkzJMwCgYIKoZIzj0EAwIw - MjEfMB0GA1UECgwWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEPMA0GA1UEAwwGd2Fy - bmV0MB4XDTI0MTExMTE2NTM1MFoXDTM0MTEwOTE2NTM1MFowMjEfMB0GA1UECgwW - bG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEPMA0GA1UEAwwGd2FybmV0MFkwEwYHKoZI - zj0CAQYIKoZIzj0DAQcDQgAEBVltIvaTlAQI/3FFatTqVflZuZdRJ0SmRMSJrFLP - tp0fxE7hmteSt6gjQriy90fP8j9OJXBNAjt915kLY4zVvqOBiTCBhjAOBgNVHQ8B - Af8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUwAwEB/zAd - BgNVHQ4EFgQU5d8QMrwhLgTkDjWA+eXZGz+dybUwLwYDVR0RBCgwJoIJbG9jYWxo - b3N0ggEqhwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMAoGCCqGSM49BAMCA0kAMEYC - IQDPofN0fEl5gTwCYhk3nZbjMqJhZ8BsSJ6K8XRhxr7zbwIhAPsgQCFOqUWg632O - NEO53OQ6CIqnpxSskjsFNH4ZBQOE - -----END CERTIFICATE----- - tls.key: | - -----BEGIN EC PRIVATE KEY----- - MHcCAQEEIIcFtWTLQv5JaRRxdkPKkO98OrvgeztbZ7h8Ev/4UbE4oAoGCCqGSM49 - AwEHoUQDQgAEBVltIvaTlAQI/3FFatTqVflZuZdRJ0SmRMSJrFLPtp0fxE7hmteS - t6gjQriy90fP8j9OJXBNAjt915kLY4zVvg== - -----END EC PRIVATE KEY----- --- apiVersion: v1 kind: ConfigMap diff --git a/resources/charts/bitcoincore/charts/cln/templates/pod.yaml b/resources/charts/bitcoincore/charts/cln/templates/pod.yaml index b632ca396..64b6dc3e1 100644 --- a/resources/charts/bitcoincore/charts/cln/templates/pod.yaml +++ b/resources/charts/bitcoincore/charts/cln/templates/pod.yaml @@ -31,18 +31,10 @@ spec: - name: p2p containerPort: {{ .Values.P2PPort }} protocol: TCP - - name: rest - containerPort: {{ .Values.RestPort }} - protocol: TCP command: - /bin/sh - -c - - | - lightningd --conf=/root/.lightning/config --recover=35b8182d6db5cec40d9bead20607b7c9b91ed89997a290bc0e0f07e5922e4714 & - pid=$!; - sleep 10; - kill $pid; - sleep 1; + - | lightningd --conf=/root/.lightning/config livenessProbe: {{- toYaml .Values.livenessProbe | nindent 8 }} @@ -59,12 +51,6 @@ spec: - mountPath: /root/.lightning/config name: config subPath: config - - mountPath: /root/.lightning/cert - name: config - subPath: tls.cert - - mountPath: /root/.lightning/key - name: config - subPath: tls.key {{- with .Values.extraContainers }} {{- toYaml . | nindent 4 }} {{- end }} diff --git a/resources/charts/bitcoincore/charts/cln/templates/service.yaml b/resources/charts/bitcoincore/charts/cln/templates/service.yaml index 4848d8c5a..c30fb90be 100644 --- a/resources/charts/bitcoincore/charts/cln/templates/service.yaml +++ b/resources/charts/bitcoincore/charts/cln/templates/service.yaml @@ -12,9 +12,5 @@ spec: targetPort: p2p protocol: TCP name: p2p - - port: {{ .Values.RestPort }} - targetPort: rest - protocol: TCP - name: rest selector: {{- include "cln.selectorLabels" . | nindent 4 }} diff --git a/resources/charts/bitcoincore/charts/cln/values.yaml b/resources/charts/bitcoincore/charts/cln/values.yaml index 58859c02d..e33e42aba 100644 --- a/resources/charts/bitcoincore/charts/cln/values.yaml +++ b/resources/charts/bitcoincore/charts/cln/values.yaml @@ -102,10 +102,9 @@ affinity: {} baseConfig: | log-level=debug - developer - dev-fast-gossip + # developer + # dev-fast-gossip bitcoin-rpcuser=user - clnrest-host=0.0.0.0 # bitcoind.rpcpass are set in configmap.yaml config: "" diff --git a/resources/charts/commander/templates/rbac.yaml b/resources/charts/commander/templates/rbac.yaml index 365ec62ff..700bbe143 100644 --- a/resources/charts/commander/templates/rbac.yaml +++ b/resources/charts/commander/templates/rbac.yaml @@ -15,8 +15,8 @@ metadata: app.kubernetes.io/name: {{ .Chart.Name }} rules: - apiGroups: [""] - resources: ["pods", "configmaps"] - verbs: ["get", "list", "watch"] + resources: ["pods", "configmaps", "pods/exec"] + verbs: ["get", "list", "watch", "exec"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding diff --git a/resources/plugins/simln/plugin.py b/resources/plugins/simln/plugin.py index 1411ea645..ec9399b1f 100755 --- a/resources/plugins/simln/plugin.py +++ b/resources/plugins/simln/plugin.py @@ -17,6 +17,7 @@ get_static_client, wait_for_init, write_file_to_container, + read_file_from_container, ) from warnet.process import run_command @@ -145,6 +146,8 @@ def _launch_activity(activity: Optional[list[dict]], plugin_dir: str) -> str: activity_json = _generate_activity_json(activity) wait_for_init(name, namespace=get_default_namespace(), quiet=True) + #write cert files to container + transfer_cln_certs(name) if write_file_to_container( name, "init", @@ -162,13 +165,18 @@ def _generate_activity_json(activity: Optional[list[dict]]) -> str: nodes = [] for i in get_mission(LIGHTNING_MISSION): - name = i.metadata.name - node = { - "id": name, - "address": f"https://{name}:10009", - "macaroon": "/working/admin.macaroon", - "cert": "/working/tls.cert", - } + ln_name = i.metadata.name + port = 10009 + node = {"id": ln_name} + if "cln" in ln_name: + port = 9735 + 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" + else: + node["macaroon"] = "/working/admin.macaroon" + node["cert"] = "/working/tls.cert" + node["address"] = f"{ln_name}:{port}" nodes.append(node) if activity: @@ -178,6 +186,30 @@ def _generate_activity_json(activity: Optional[list[dict]]) -> str: return json.dumps(data, indent=2) +def transfer_cln_certs(name): + dst_container = "init" + cln_root = "/root/.lightning/regtest" + for i in get_mission(LIGHTNING_MISSION): + ln_name = i.metadata.name + if "cln" in ln_name: + copyfile(ln_name, "cln", f"{cln_root}/ca.pem", name, dst_container, f"/working/{ln_name}-ca.pem") + copyfile(ln_name, "cln", f"{cln_root}/client.pem", name, dst_container, f"/working/{ln_name}-client.pem") + copyfile(ln_name, "cln", f"{cln_root}/client-key.pem", name, dst_container, f"/working/{ln_name}-client-key.pem") + + +def copyfile(pod_name, src_container, source_path, dst_name, dst_container, dst_path): + namespace=get_default_namespace() + file_data = read_file_from_container(pod_name, source_path, src_container, namespace) + if not write_file_to_container( + dst_name, + dst_container, + dst_path, + file_data, + namespace=namespace, + quiet=True, + ): + print(f"Failed to copy {source_path} from {pod_name} to {dst_name}:{dst_path}") + def _sh(pod, method: str, params: tuple[str, ...]) -> str: namespace = get_default_namespace() diff --git a/resources/scenarios/ln_framework/ln.py b/resources/scenarios/ln_framework/ln.py index 328fc88ce..8a5e3ad34 100644 --- a/resources/scenarios/ln_framework/ln.py +++ b/resources/scenarios/ln_framework/ln.py @@ -1,19 +1,46 @@ from abc import ABC, abstractmethod +import base64 import http.client import logging import json import ssl from time import sleep +from typing import Optional +from kubernetes import client, config +from kubernetes.stream import stream # hard-coded deterministic lnd credentials ADMIN_MACAROON_HEX = "0201036c6e6402f801030a1062beabbf2a614b112128afa0c0b4fdd61201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e657261746512047265616400000620b17be53e367290871681055d0de15587f6d1cd47d1248fe2662ae27f62cfbdc6" -# hard-coded deterministic cln credentials - rune -ADMIN_RUNE = "_y4Av-cXE9OKqclcNEVblZEMfxNjV1C-Jbc7KBqB2To9MA==" # Don't worry about ln's self-signed certificates INSECURE_CONTEXT = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) INSECURE_CONTEXT.check_hostname = False INSECURE_CONTEXT.verify_mode = ssl.CERT_NONE +def run_command(name, command: list[str], namespace: Optional[str] = "default") -> str: + config.load_incluster_config() + sclient = client.CoreV1Api() + resp = stream( + sclient.connect_get_namespaced_pod_exec, + name, + namespace, + command=command, + stderr=True, + stdin=False, + stdout=True, + tty=False, + _request_timeout=20, + _preload_content=False, + ) + result = "" + while resp.is_open(): + resp.update(timeout=5) + if resp.peek_stdout(): + result+=resp.read_stdout() + if resp.peek_stderr(): + raise Exception(resp.read_stderr()) + resp.close() + return result + # https://github.com/lightningcn/lightning-rfc/blob/master/07-routing-gossip.md#the-channel_update-message # We use the field names as written in the BOLT as our canonical, internal field names. @@ -81,14 +108,28 @@ class LNNode(ABC): def __init__(self, pod_name): self.log = logging.getLogger(self.__class__.__name__) self.name = pod_name + # Configure logger if it has no handlers + if not self.log.handlers: + handler = logging.StreamHandler() + formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + handler.setFormatter(formatter) + self.log.addHandler(handler) + self.log.setLevel(logging.INFO) - @abstractmethod - def get(self, uri, max_tries=10) -> str: - pass + @staticmethod + def param_dict_to_list(params: dict) -> list[str]: + return [f'{k}={v}' for k,v in params.items()] - @abstractmethod - def post(self, uri, data, max_tries=10) -> str: - pass + @staticmethod + def hex_to_b64(hex): + return base64.b64encode(bytes.fromhex(hex)).decode() + + @staticmethod + def b64_to_hex(b64, reverse=False): + if reverse: + return base64.b64decode(b64)[::-1].hex() + else: + return base64.b64decode(b64).hex() @abstractmethod def newaddress(self, max_tries=10) -> tuple[bool, str]: @@ -106,52 +147,25 @@ def walletbalance(self) -> int: def graph(self): pass - class CLN(LNNode): def __init__(self, pod_name): super().__init__(pod_name) - self.headers = {"Rune": ADMIN_RUNE} + self.headers = {} self.impl = "cln" - self.reset_connection() - - def reset_connection(self): - self.conn = http.client.HTTPSConnection( - host=self.name, port=3010, timeout=5, context=INSECURE_CONTEXT - ) - - def get(self, uri, max_tries=2): + + def rpc(self, method: str, params: list[str] = [], namespace: Optional[str] = "default", max_tries=5): + cmd = ["lightning-cli", method] + cmd.extend(params) attempt=0 while attempt < max_tries: attempt+=1 try: - self.conn.request( - method="GET", - url=uri, - headers=self.headers, - ) - return self.conn.getresponse().read().decode("utf8") + response = run_command(self.name, cmd, namespace) + if not response: + continue + return response except Exception as e: - self.log.info(f"GET clnrest error: {e}") - self.reset_connection() - sleep(1) - return None - - def post(self, uri, data = {}, max_tries=2): - body = json.dumps(data) - attempt=0 - while attempt < max_tries: - attempt+=1 - try: - self.conn.request( - method="POST", - url=uri, - body=body, - headers=self.headers, - ) - return self.conn.getresponse().read().decode("utf8") - except Exception as e: - print(f"POST clnrest error: {e}") - self.reset_connection() + self.log.info(f"CLN rpc error: {e}") sleep(2) return None @@ -159,7 +173,10 @@ def newaddress(self, max_tries=2): attempt=0 while attempt < max_tries: attempt+=1 - response = self.post("/v1/newaddr") + response = self.rpc("newaddr") + if not response: + sleep(2) + continue res = json.loads(response) if "bech32" in res: return True, res["bech32"] @@ -167,55 +184,107 @@ def newaddress(self, max_tries=2): self.log.info( f"Couldn't get wallet address from {self.name}:\n {res}\n wait and retry..." ) - sleep(1) + sleep(2) return False, "" def uri(self): - res = json.loads(self.post("/v1/getinfo")) + res = json.loads(self.rpc("getinfo")) if len(res["address"]) < 1: return None return f'{res["id"]}@{res["address"][0]["address"]}:{res["address"][0]["port"]}' - def walletbalance(self): - res = json.loads(self.post("/v1/listfunds")) - return int(sum(o["amount_msat"] for o in res["outputs"]) / 1000) - - def connect(self, target_uri, max_tries=2): - pk, host = target_uri.split("@") + def walletbalance(self, max_tries=2): + attempt=0 + while attempt < max_tries: + attempt+=1 + response = self.rpc("listfunds") + if not response: + sleep(2) + continue + res = json.loads(response) + return int(sum(o["amount_msat"] for o in res["outputs"]) / 1000) + return 0 + + def connect(self, target_uri, max_tries=5): attempt=0 + self.log.info(f"CLN connect {self.name} to {target_uri}") while attempt < max_tries: attempt+=1 - res = self.post("/v1/connect", data={"id": pk, "host": host}) - if res: - return json.loads(res) + response = self.rpc("connect", [target_uri]) + if response: + res = json.loads(response) + if "id" in res: + self.log.debug(f"finished connect response: {response}") + return {} + elif "code" in res and res["code"] == 402: + self.log.info(f"failed connect response: {response}") + sleep(5) + else: + return res else: - print(f"connect response: {res}") + self.log.debug(f"connect response: {response}") sleep(5) return "" def channel(self, pk, capacity, push_amt, fee_rate, max_tries=5): + data={ + "amount": capacity, + "push_msat": push_amt, + "id": pk, + "feerate": fee_rate, + } attempt=0 while attempt < max_tries: attempt+=1 - res = self.post( - "/v1/fundchannel", - data={ - "amount": capacity, - "push_msat": push_amt, - "id": pk, - "feerate": fee_rate, - }, - ) - if res: - return json.loads(res) + response = self.rpc("fundchannel", self.param_dict_to_list(data)) + if response: + res = json.loads(response) + if "txid" in res: + self.log.debug(f"open channel succeeded: {res}") + return {"txid": res["txid"], "outpoint": f'{res["txid"]}:{res["outnum"]}'} + else: + self.log.info(f"unable to open channel: {res}") else: - print(f"channel response: {res}") + self.log.debug(f"channel response: {response}") sleep(5) return "" - def graph(self): - res = self.post("/v1/graph") - return json.loads(res) + def graph(self, max_tries=2): + attempt=0 + while attempt < max_tries: + attempt+=1 + response = self.rpc("listchannels") + if response: + res = json.loads(response) + if "channels" in res: + return {"edges": res["channels"]} + else: + self.log.info(f"unable to open channel: {res}") + else: + self.log.debug(f"channel response: {response}") + sleep(5) + return "" + + def update(self, txid_hex: str, policy: dict, capacity: int, max_tries=2): + self.log.info("Channel Policy Updates not supported by CLN yet!") + return + # ln_policy = Policy.from_dict(policy).to_lnd_chanpolicy(capacity) + # data = {"chan_point": {"funding_txid_str": txid_hex, "output_index": 0}, **ln_policy} + # attempt=0 + # while attempt < max_tries: + # attempt+=1 + # response = self.rpc("setchannel") + # if response: + # res = json.loads(response) + # if "channels" in res: + # print(f"graph succeeded: {res}") + # return {"edges": res["channels"]} + # else: + # print(f"unable to open channel: {res}") + # else: + # print(f"channel response: {response}") + # sleep(5) + # return "" class LND(LNNode): def __init__(self, pod_name): @@ -304,16 +373,25 @@ def connect(self, target_uri): return json.loads(res) def channel(self, pk, capacity, push_amt, fee_rate): - res = self.post( + b64_pk = self.hex_to_b64(pk) + response = self.post( "/v1/channels/stream", data={ "local_funding_amount": capacity, "push_sat": push_amt, - "node_pubkey": pk, + "node_pubkey": b64_pk, "sat_per_vbyte": fee_rate, }, ) - return json.loads(res) + try: + res = json.loads(response) + if "result" in res: + res["txid"] = self.b64_to_hex(res["result"]["chan_pending"]["txid"], reverse=True) + res["outpoint"] = f'{res["txid"]}:{res["result"]["chan_pending"]["output_index"]}' + self.log.info(f"LND channel RESPONSE: {res}") + except Exception as e: + self.log.info(f"Error opening LND channel: {e}") + return res def update(self, txid_hex: str, policy: dict, capacity: int): ln_policy = Policy.from_dict(policy).to_lnd_chanpolicy(capacity) diff --git a/resources/scenarios/ln_init.py b/resources/scenarios/ln_init.py index 371e4a52f..35bd9698f 100644 --- a/resources/scenarios/ln_init.py +++ b/resources/scenarios/ln_init.py @@ -138,6 +138,7 @@ def get_ln_uri(self, ln): nodes = list(ln_nodes) prev_node = nodes[-1] for node in nodes: + # if (node, prev_node) not in connections and (prev_node, node) not in connections: connections.append((node, prev_node)) prev_node = node # Explicit connections between every pair of channel partners @@ -183,6 +184,7 @@ def connect_ln(self, pair): threading.Thread(target=connect_ln, args=(self, pair)) for pair in connections ] for thread in p2p_threads: + sleep(5) thread.start() all(thread.join(timeout=THREAD_JOIN_TIMEOUT) is None for thread in p2p_threads) @@ -222,32 +224,29 @@ def open_channel(self, ch, fee_rate): src = self.lns[ch["source"]] tgt_uri = ln_uris[ch["target"]] tgt_pk, _ = tgt_uri.split("@") - if src.impl == "lnd": - tgt_pk = self.hex_to_b64(tgt_pk) self.log.info( f"Sending channel open from {ch['source']} -> {ch['target']} with fee_rate={fee_rate}" ) - res = src.channel( pk=tgt_pk, capacity=ch["capacity"], push_amt=ch["push_amt"], fee_rate=fee_rate, ) - if "result" not in res: + if "txid" in res: + ch["txid"] = res["txid"] self.log.info( + f"Channel open {ch['source']} -> {ch['target']}\n " + + f"outpoint={res["outpoint"]}\n " + + f"expected channel id: {ch['id']}" + ) + else: + ch["txid"] = "N/A" + self.log.info( "Unexpected channel open response:\n " + f"From {ch['source']} -> {ch['target']} fee_rate={fee_rate}\n " + f"{res}" ) - else: - txid = self.b64_to_hex(res["result"]["chan_pending"]["txid"], reverse=True) - ch["txid"] = txid - self.log.info( - f"Channel open {ch['source']} -> {ch['target']}\n " - + f"outpoint={txid}:{res['result']['chan_pending']['output_index']}\n " - + f"expected channel id: {ch['id']}" - ) channels = sorted(ch_by_block[target_block], key=lambda ch: ch["id"]["index"]) index = 0 @@ -259,6 +258,7 @@ def open_channel(self, ch, fee_rate): assert index == ch["id"]["index"], "Channel ID indexes are not consecutive" assert fee_rate >= 1, "Too many TXs in block, out of fee range" t = threading.Thread(target=open_channel, args=(self, ch, fee_rate)) + sleep(5) t.start() ch_threads.append(t) @@ -275,6 +275,9 @@ def open_channel(self, ch, fee_rate): block_txs = block["tx"] block_height = block["height"] for ch in channels: + if "txid" not in ch: + print(f"{ch} does not have a txid") + continue assert ch["id"]["block"] == block_height, f"Actual block:{block_height}\n{ch}" assert ( block_txs[ch["id"]["index"]] == ch["txid"] @@ -288,7 +291,10 @@ def open_channel(self, ch, fee_rate): def ln_all_chs(self, ln): expected = len(self.channels) - while len(ln.graph()["edges"]) != expected: + attempts=0 + max_tries=5 + while len(ln.graph()["edges"]) != expected and attempts < max_tries: + attempts+=1 sleep(1) self.log.info(f"LN {ln.name} has graph with all {expected} channels") @@ -296,6 +302,7 @@ def ln_all_chs(self, ln): threading.Thread(target=ln_all_chs, args=(self, ln)) for ln in self.lns.values() ] for thread in ch_ann_threads: + sleep(5) thread.start() all(thread.join(timeout=THREAD_JOIN_TIMEOUT) is None for thread in ch_ann_threads) diff --git a/src/warnet/deploy.py b/src/warnet/deploy.py index 4bf5a09e8..6eb09f1d6 100644 --- a/src/warnet/deploy.py +++ b/src/warnet/deploy.py @@ -115,9 +115,9 @@ def _deploy(directory, debug, namespace, to_all_users): # Wait for the network process to complete print(f"Waiting for network process thread {network_process.pid} join") - network_process.join(timeout=300) + network_process.join(timeout=500) if network_process.is_alive(): - print("Process hit the timeout (still running after 300 seconds)") + print("Process hit the timeout (still running after 500 seconds)") # Optionally terminate the process if it timed out network_process.terminate() else: diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index c12e4de1b..7bdd5bde2 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -596,3 +596,38 @@ def download( os.remove(tar_file) return destination_path + + +def read_file_from_container( + pod_name, source_path: Path, container_name: str = "", namespace: Optional[str] = None, quiet: bool = False +) -> str: + """Download the file from the `source_path` to the `destination_path`""" + + namespace = get_default_namespace_or(namespace) + + v1 = get_static_client() + + command = ["cat", str(source_path)] + + resp = stream( + v1.connect_get_namespaced_pod_exec, + name=pod_name, + namespace=namespace, + container=container_name, + command=command, + stderr=True, + stdin=False, + stdout=True, + tty=False, + _preload_content=False, + ) + + result = "" + while resp.is_open(): + resp.update(timeout=5) + if resp.peek_stdout(): + result+=resp.read_stdout() + if resp.peek_stderr(): + raise Exception(resp.read_stderr()) + resp.close() + return result From 4c409fd62d705430b3f48c71e151f520ed045abc Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Tue, 15 Apr 2025 12:16:36 -0400 Subject: [PATCH 08/24] adjust cln to start grpc --- .../charts/bitcoincore/charts/cln/templates/configmap.yaml | 1 + resources/charts/bitcoincore/charts/cln/templates/pod.yaml | 3 +++ .../charts/bitcoincore/charts/cln/templates/service.yaml | 4 ++++ resources/charts/bitcoincore/charts/cln/values.yaml | 2 +- resources/plugins/simln/plugin.py | 4 ++-- 5 files changed, 11 insertions(+), 3 deletions(-) diff --git a/resources/charts/bitcoincore/charts/cln/templates/configmap.yaml b/resources/charts/bitcoincore/charts/cln/templates/configmap.yaml index 5936fdd19..bf76a39e2 100644 --- a/resources/charts/bitcoincore/charts/cln/templates/configmap.yaml +++ b/resources/charts/bitcoincore/charts/cln/templates/configmap.yaml @@ -18,6 +18,7 @@ data: announce-addr=dns:{{ include "cln.fullname" . }}:{{ .Values.P2PPort }} database-upgrade=true bitcoin-retry-timeout=600 + grpc-port={{ .Values.RPCPort }} --- apiVersion: v1 kind: ConfigMap diff --git a/resources/charts/bitcoincore/charts/cln/templates/pod.yaml b/resources/charts/bitcoincore/charts/cln/templates/pod.yaml index 64b6dc3e1..255be8f22 100644 --- a/resources/charts/bitcoincore/charts/cln/templates/pod.yaml +++ b/resources/charts/bitcoincore/charts/cln/templates/pod.yaml @@ -31,6 +31,9 @@ spec: - name: p2p containerPort: {{ .Values.P2PPort }} protocol: TCP + - name: rpc + containerPort: {{ .Values.RPCPort }} + protocol: TCP command: - /bin/sh - -c diff --git a/resources/charts/bitcoincore/charts/cln/templates/service.yaml b/resources/charts/bitcoincore/charts/cln/templates/service.yaml index c30fb90be..3adc03dc5 100644 --- a/resources/charts/bitcoincore/charts/cln/templates/service.yaml +++ b/resources/charts/bitcoincore/charts/cln/templates/service.yaml @@ -12,5 +12,9 @@ spec: targetPort: p2p protocol: TCP name: p2p + - port: {{ .Values.RPCPort }} + targetPort: rpc + protocol: TCP + name: rpc selector: {{- include "cln.selectorLabels" . | nindent 4 }} diff --git a/resources/charts/bitcoincore/charts/cln/values.yaml b/resources/charts/bitcoincore/charts/cln/values.yaml index e33e42aba..85b58152e 100644 --- a/resources/charts/bitcoincore/charts/cln/values.yaml +++ b/resources/charts/bitcoincore/charts/cln/values.yaml @@ -32,7 +32,7 @@ service: type: ClusterIP P2PPort: 9735 -RestPort: 3010 +RPCPort: 10013 # This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ ingress: diff --git a/resources/plugins/simln/plugin.py b/resources/plugins/simln/plugin.py index ec9399b1f..770445912 100755 --- a/resources/plugins/simln/plugin.py +++ b/resources/plugins/simln/plugin.py @@ -169,14 +169,14 @@ def _generate_activity_json(activity: Optional[list[dict]]) -> str: port = 10009 node = {"id": ln_name} if "cln" in ln_name: - port = 9735 + port = 10013 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" else: node["macaroon"] = "/working/admin.macaroon" node["cert"] = "/working/tls.cert" - node["address"] = f"{ln_name}:{port}" + node["address"] = f"https://{ln_name}:{port}" nodes.append(node) if activity: From 783141311fe42e2ac6a8e86c7076fe214f1950d0 Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Fri, 18 Apr 2025 13:13:49 -0400 Subject: [PATCH 09/24] enable grpc plugin to respond to remote nodes --- .../charts/bitcoincore/charts/cln/templates/configmap.yaml | 3 ++- resources/charts/bitcoincore/charts/cln/values.yaml | 2 +- resources/plugins/simln/plugin.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/resources/charts/bitcoincore/charts/cln/templates/configmap.yaml b/resources/charts/bitcoincore/charts/cln/templates/configmap.yaml index bf76a39e2..3fe68f61c 100644 --- a/resources/charts/bitcoincore/charts/cln/templates/configmap.yaml +++ b/resources/charts/bitcoincore/charts/cln/templates/configmap.yaml @@ -10,7 +10,7 @@ data: {{- .Values.defaultConfig | nindent 4 }} {{- .Values.config | nindent 4 }} network={{ .Values.global.chain }} - bind-addr=0.0.0.0:{{ .Values.P2PPort }} + addr=0.0.0.0:{{ .Values.P2PPort }} bitcoin-rpcconnect={{ include "bitcoincore.fullname" . }} bitcoin-rpcport={{ index .Values.global .Values.global.chain "RPCPort" }} bitcoin-rpcpassword={{ .Values.global.rpcpassword }} @@ -19,6 +19,7 @@ data: database-upgrade=true bitcoin-retry-timeout=600 grpc-port={{ .Values.RPCPort }} + grpc-host=0.0.0.0 --- apiVersion: v1 kind: ConfigMap diff --git a/resources/charts/bitcoincore/charts/cln/values.yaml b/resources/charts/bitcoincore/charts/cln/values.yaml index 85b58152e..f3024d653 100644 --- a/resources/charts/bitcoincore/charts/cln/values.yaml +++ b/resources/charts/bitcoincore/charts/cln/values.yaml @@ -32,7 +32,7 @@ service: type: ClusterIP P2PPort: 9735 -RPCPort: 10013 +RPCPort: 9736 # This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ ingress: diff --git a/resources/plugins/simln/plugin.py b/resources/plugins/simln/plugin.py index 770445912..2d37d23f2 100755 --- a/resources/plugins/simln/plugin.py +++ b/resources/plugins/simln/plugin.py @@ -169,7 +169,7 @@ def _generate_activity_json(activity: Optional[list[dict]]) -> str: port = 10009 node = {"id": ln_name} if "cln" in ln_name: - port = 10013 + port = 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" From 9fdd5f84a812590569af986b555eadff9f34e0bc Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Fri, 18 Apr 2025 20:49:44 -0400 Subject: [PATCH 10/24] Add styling to standard out for improved readability Tune sleep / pauses down Set simln container restartPolicy: Never --- .../charts/bitcoincore/charts/cln/values.yaml | 6 +- .../plugins/simln/charts/simln/values.yaml | 2 + resources/plugins/simln/plugin.py | 6 +- resources/scenarios/commander.py | 24 +++++- resources/scenarios/ln_framework/ln.py | 85 ++++++++++--------- resources/scenarios/ln_init.py | 38 ++++----- 6 files changed, 95 insertions(+), 66 deletions(-) diff --git a/resources/charts/bitcoincore/charts/cln/values.yaml b/resources/charts/bitcoincore/charts/cln/values.yaml index f3024d653..714fc0459 100644 --- a/resources/charts/bitcoincore/charts/cln/values.yaml +++ b/resources/charts/bitcoincore/charts/cln/values.yaml @@ -101,9 +101,9 @@ tolerations: [] affinity: {} baseConfig: | - log-level=debug - # developer - # dev-fast-gossip + log-level=info + developer + dev-fast-gossip bitcoin-rpcuser=user # bitcoind.rpcpass are set in configmap.yaml diff --git a/resources/plugins/simln/charts/simln/values.yaml b/resources/plugins/simln/charts/simln/values.yaml index a1647a963..bea0096df 100644 --- a/resources/plugins/simln/charts/simln/values.yaml +++ b/resources/plugins/simln/charts/simln/values.yaml @@ -4,6 +4,8 @@ image: tag: "0.2.3" pullPolicy: IfNotPresent +restartPolicy: Never + workingVolume: name: working-volume mountPath: /working diff --git a/resources/plugins/simln/plugin.py b/resources/plugins/simln/plugin.py index 2d37d23f2..e75a92c7c 100755 --- a/resources/plugins/simln/plugin.py +++ b/resources/plugins/simln/plugin.py @@ -200,7 +200,7 @@ def transfer_cln_certs(name): def copyfile(pod_name, src_container, source_path, dst_name, dst_container, dst_path): namespace=get_default_namespace() file_data = read_file_from_container(pod_name, source_path, src_container, namespace) - if not write_file_to_container( + if write_file_to_container( dst_name, dst_container, dst_path, @@ -208,7 +208,9 @@ def copyfile(pod_name, src_container, source_path, dst_name, dst_container, dst_ namespace=namespace, quiet=True, ): - print(f"Failed to copy {source_path} from {pod_name} to {dst_name}:{dst_path}") + log.info(f"Copied {source_path} to {dst_path}") + else: + log.error(f"Failed to copy {source_path} from {pod_name} to {dst_name}:{dst_path}") def _sh(pod, method: str, params: tuple[str, ...]) -> str: diff --git a/resources/scenarios/commander.py b/resources/scenarios/commander.py index 814a87b7a..66fd23979 100644 --- a/resources/scenarios/commander.py +++ b/resources/scenarios/commander.py @@ -82,6 +82,27 @@ def auth_proxy_request(self, method, path, postdata): AuthServiceProxy.oldrequest = AuthServiceProxy._request AuthServiceProxy._request = auth_proxy_request +# Create a custom formatter +class ColorFormatter(logging.Formatter): + """Custom formatter to add color based on log level.""" + # Define ANSI color codes + RED = '\033[91m' + YELLOW = '\033[93m' + GREEN = '\033[92m' + RESET = '\033[0m' + + FORMATS = { + logging.DEBUG: f"{RESET}%(name)-8s - Thread-%(thread)d - %(message)s{RESET}", + logging.INFO: f"{RESET}%(name)-8s - %(message)s{RESET}", + logging.WARNING: f"{YELLOW}%(name)-8s - %(message)s{RESET}", + logging.ERROR: f"{RED}%(name)-8s - %(message)s{RESET}", + logging.CRITICAL: f"{RED}##%(name)-8s - %(message)s##{RESET}" + } + + def format(self, record): + log_fmt = self.FORMATS.get(record.levelno) + formatter = logging.Formatter(log_fmt) + return formatter.format(record) class Commander(BitcoinTestFramework): # required by subclasses of BitcoinTestFramework @@ -155,8 +176,7 @@ def setup(self): # Scenarios log directly to stdout which gets picked up by the # subprocess manager in the server, and reprinted to the global log. ch = logging.StreamHandler(sys.stdout) - formatter = logging.Formatter(fmt="%(name)-8s %(message)s") - ch.setFormatter(formatter) + ch.setFormatter(ColorFormatter()) self.log.addHandler(ch) # Keep a separate index of tanks by pod name diff --git a/resources/scenarios/ln_framework/ln.py b/resources/scenarios/ln_framework/ln.py index 8a5e3ad34..1854f5674 100644 --- a/resources/scenarios/ln_framework/ln.py +++ b/resources/scenarios/ln_framework/ln.py @@ -103,6 +103,28 @@ def to_lnd_chanpolicy(self, capacity): "min_htlc_msat_specified": True, } +# Create a custom formatter +class ColorFormatter(logging.Formatter): + """Custom formatter to add color based on log level.""" + # Define ANSI color codes + RED = '\033[91m' + YELLOW = '\033[93m' + GREEN = '\033[92m' + RESET = '\033[0m' + + FORMATS = { + logging.DEBUG: f"{RESET}%(asctime)s - (name)-8s - Thread-%(thread)d - %(message)s{RESET}", + logging.INFO: f"{RESET}%(asctime)s - (name)-8s - %(message)s{RESET}", + logging.WARNING: f"{YELLOW}%(asctime)s - (name)-8s - %(message)s{RESET}", + logging.ERROR: f"{RED}%(asctime)s - (name)-8s - %(message)s{RESET}", + logging.CRITICAL: f"{RED}##%(asctime)s - (name)-8s - %(message)s##{RESET}" + } + + def format(self, record): + log_fmt = self.FORMATS.get(record.levelno) + formatter = logging.Formatter(log_fmt) + return formatter.format(record) + class LNNode(ABC): @abstractmethod def __init__(self, pod_name): @@ -111,8 +133,7 @@ def __init__(self, pod_name): # Configure logger if it has no handlers if not self.log.handlers: handler = logging.StreamHandler() - formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") - handler.setFormatter(formatter) + handler.setFormatter(ColorFormatter()) self.log.addHandler(handler) self.log.setLevel(logging.INFO) @@ -165,7 +186,7 @@ def rpc(self, method: str, params: list[str] = [], namespace: Optional[str] = "d continue return response except Exception as e: - self.log.info(f"CLN rpc error: {e}") + self.log.error(f"CLN rpc error: {e}, wait and retry...") sleep(2) return None @@ -181,7 +202,7 @@ def newaddress(self, max_tries=2): if "bech32" in res: return True, res["bech32"] else: - self.log.info( + self.log.warning( f"Couldn't get wallet address from {self.name}:\n {res}\n wait and retry..." ) sleep(2) @@ -207,23 +228,21 @@ def walletbalance(self, max_tries=2): def connect(self, target_uri, max_tries=5): attempt=0 - self.log.info(f"CLN connect {self.name} to {target_uri}") while attempt < max_tries: attempt+=1 response = self.rpc("connect", [target_uri]) if response: res = json.loads(response) if "id" in res: - self.log.debug(f"finished connect response: {response}") return {} elif "code" in res and res["code"] == 402: - self.log.info(f"failed connect response: {response}") + self.log.warning(f"failed connect 402: {response}, wait and retry...") sleep(5) else: return res else: - self.log.debug(f"connect response: {response}") - sleep(5) + self.log.debug(f"connect response: {response}, wait and retry...") + sleep(2) return "" def channel(self, pk, capacity, push_amt, fee_rate, max_tries=5): @@ -240,13 +259,13 @@ def channel(self, pk, capacity, push_amt, fee_rate, max_tries=5): if response: res = json.loads(response) if "txid" in res: - self.log.debug(f"open channel succeeded: {res}") return {"txid": res["txid"], "outpoint": f'{res["txid"]}:{res["outnum"]}'} else: - self.log.info(f"unable to open channel: {res}") + self.log.warning(f"unable to open channel: {res}, wait and retry...") + sleep(1) else: - self.log.debug(f"channel response: {response}") - sleep(5) + self.log.debug(f"channel response: {response}, wait and retry...") + sleep(2) return "" def graph(self, max_tries=2): @@ -257,34 +276,25 @@ def graph(self, max_tries=2): if response: res = json.loads(response) if "channels" in res: - return {"edges": res["channels"]} + # Map to desired output + filtered_channels = [ch for ch in res['channels'] if ch['direction'] == 1] + # Sort by short_channel_id - block -> index -> output + sorted_channels = sorted(filtered_channels, key=lambda x: x['short_channel_id']) + # Add capacity by dividing amount_msat by 1000 + for channel in sorted_channels: + channel['capacity'] = channel['amount_msat'] // 1000 + return {'edges': sorted_channels} else: - self.log.info(f"unable to open channel: {res}") + self.log.warning(f"unable to open channel: {res}, wait and retry...") + sleep(1) else: - self.log.debug(f"channel response: {response}") - sleep(5) + self.log.debug(f"channel response: {response}, wait and retry...") + sleep(2) return "" def update(self, txid_hex: str, policy: dict, capacity: int, max_tries=2): - self.log.info("Channel Policy Updates not supported by CLN yet!") + self.log.warning("Channel Policy Updates not supported by CLN yet!") return - # ln_policy = Policy.from_dict(policy).to_lnd_chanpolicy(capacity) - # data = {"chan_point": {"funding_txid_str": txid_hex, "output_index": 0}, **ln_policy} - # attempt=0 - # while attempt < max_tries: - # attempt+=1 - # response = self.rpc("setchannel") - # if response: - # res = json.loads(response) - # if "channels" in res: - # print(f"graph succeeded: {res}") - # return {"edges": res["channels"]} - # else: - # print(f"unable to open channel: {res}") - # else: - # print(f"channel response: {response}") - # sleep(5) - # return "" class LND(LNNode): def __init__(self, pod_name): @@ -350,7 +360,7 @@ def newaddress(self, max_tries=10): if "address" in res: return True, res["address"] else: - self.log.info( + self.log.warning( f"Couldn't get wallet address from {self.name}:\n {res}\n wait and retry..." ) sleep(1) @@ -388,9 +398,8 @@ def channel(self, pk, capacity, push_amt, fee_rate): if "result" in res: res["txid"] = self.b64_to_hex(res["result"]["chan_pending"]["txid"], reverse=True) res["outpoint"] = f'{res["txid"]}:{res["result"]["chan_pending"]["output_index"]}' - self.log.info(f"LND channel RESPONSE: {res}") except Exception as e: - self.log.info(f"Error opening LND channel: {e}") + self.log.error(f"Error opening LND channel: {e}") return res def update(self, txid_hex: str, policy: dict, capacity: int): diff --git a/resources/scenarios/ln_init.py b/resources/scenarios/ln_init.py index 35bd9698f..44fe04a62 100644 --- a/resources/scenarios/ln_init.py +++ b/resources/scenarios/ln_init.py @@ -6,7 +6,7 @@ from commander import Commander from ln_framework.ln import Policy -THREAD_JOIN_TIMEOUT=15 +THREAD_JOIN_TIMEOUT=20 class LNInit(Commander): def set_test_params(self): @@ -145,7 +145,7 @@ def get_ln_uri(self, ln): for ch in self.channels: node_names = self.node_names(ln_nodes) if not ch["source"] in node_names or not ch["target"] in node_names: - self.log.info(f"LN Channel {ch} not available, removing") + self.log.error(f"LN Channel {ch} not available, removing") self.channels.remove(ch) continue src = self.lns[ch["source"]] @@ -184,7 +184,7 @@ def connect_ln(self, pair): threading.Thread(target=connect_ln, args=(self, pair)) for pair in connections ] for thread in p2p_threads: - sleep(5) + sleep(1) thread.start() all(thread.join(timeout=THREAD_JOIN_TIMEOUT) is None for thread in p2p_threads) @@ -258,7 +258,7 @@ def open_channel(self, ch, fee_rate): assert index == ch["id"]["index"], "Channel ID indexes are not consecutive" assert fee_rate >= 1, "Too many TXs in block, out of fee range" t = threading.Thread(target=open_channel, args=(self, ch, fee_rate)) - sleep(5) + sleep(2) t.start() ch_threads.append(t) @@ -275,13 +275,9 @@ def open_channel(self, ch, fee_rate): block_txs = block["tx"] block_height = block["height"] for ch in channels: - if "txid" not in ch: - print(f"{ch} does not have a txid") - continue - assert ch["id"]["block"] == block_height, f"Actual block:{block_height}\n{ch}" - assert ( - block_txs[ch["id"]["index"]] == ch["txid"] - ), f"Actual txid:{block_txs[ch["id"]["index"]]}\n{ch}" + assert ch["txid"] != "N/A", f"Channel:{ch} did not receive txid" + assert ch["id"]["block"] == block_height, f"Actual block:{block_height}" + assert ch["txid"] in block_txs, f"Block:{block_height} does not contain {ch["txid"]}" self.log.info("👍") gen(5) @@ -299,10 +295,10 @@ def ln_all_chs(self, ln): self.log.info(f"LN {ln.name} has graph with all {expected} channels") ch_ann_threads = [ - threading.Thread(target=ln_all_chs, args=(self, ln)) for ln in self.lns.values() + threading.Thread(target=ln_all_chs, args=(self, ln)) for ln in ln_nodes ] for thread in ch_ann_threads: - sleep(5) + sleep(1) thread.start() all(thread.join(timeout=THREAD_JOIN_TIMEOUT) is None for thread in ch_ann_threads) @@ -359,16 +355,18 @@ def policy_equal(pol1, pol2, capacity): return pol1.to_lnd_chanpolicy(capacity) == pol2.to_lnd_chanpolicy(capacity) def matching_graph(self, expected, ln): - while True: + done = False + while not done: actual = ln.graph()["edges"] - assert len(expected) == len(actual) - done = True + self.log.debug(f"LN {ln.name} channel graph edges: {actual}") + assert (len(expected) == len(actual),f"Expected edges {expected}, actual edges {actual}") + if len(actual) > 0: done = True for i, actual_ch in enumerate(actual): expected_ch = expected[i] capacity = expected_ch["capacity"] # We assert this because it isn't updated as part of policy. # If this fails we have a bigger issue - assert int(actual_ch["capacity"]) == capacity + assert int(actual_ch["capacity"]) == capacity, f"LN {ln.name} graph capacity mismatch:\n actual: {actual_ch}\n expected: {expected_ch}" # Policies were not defined in network.yaml if "source_policy" not in expected_ch or "target_policy" not in expected_ch: @@ -389,17 +387,15 @@ def matching_graph(self, expected, ln): ): continue done = False - break if done: self.log.info(f"LN {ln.name} graph channel policies all match expected source") - break else: - sleep(1) + sleep(5) expected = sorted(self.channels, key=lambda ch: (ch["id"]["block"], ch["id"]["index"])) policy_threads = [ threading.Thread(target=matching_graph, args=(self, expected, ln)) - for ln in self.lns.values() + for ln in ln_nodes ] for thread in policy_threads: thread.start() From a6fc662e4f205a09c18a2d27a3242e0ee83bc7ff Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Fri, 18 Apr 2025 21:34:13 -0400 Subject: [PATCH 11/24] Fix wait for gossip loop Remove all unnecessary thread sleeps --- resources/scenarios/commander.py | 2 +- resources/scenarios/ln_init.py | 26 +++++++++++++++----------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/resources/scenarios/commander.py b/resources/scenarios/commander.py index 66fd23979..e52a43c0e 100644 --- a/resources/scenarios/commander.py +++ b/resources/scenarios/commander.py @@ -144,7 +144,7 @@ def tank_connected(self, tank): if count >= tank.init_peers: break else: - sleep(1) + sleep(5) conn_threads = [ threading.Thread(target=tank_connected, args=(self, tank)) for tank in self.nodes diff --git a/resources/scenarios/ln_init.py b/resources/scenarios/ln_init.py index 44fe04a62..c9bee6ffe 100644 --- a/resources/scenarios/ln_init.py +++ b/resources/scenarios/ln_init.py @@ -184,7 +184,6 @@ def connect_ln(self, pair): threading.Thread(target=connect_ln, args=(self, pair)) for pair in connections ] for thread in p2p_threads: - sleep(1) thread.start() all(thread.join(timeout=THREAD_JOIN_TIMEOUT) is None for thread in p2p_threads) @@ -258,7 +257,6 @@ def open_channel(self, ch, fee_rate): assert index == ch["id"]["index"], "Channel ID indexes are not consecutive" assert fee_rate >= 1, "Too many TXs in block, out of fee range" t = threading.Thread(target=open_channel, args=(self, ch, fee_rate)) - sleep(2) t.start() ch_threads.append(t) @@ -288,20 +286,25 @@ def open_channel(self, ch, fee_rate): def ln_all_chs(self, ln): expected = len(self.channels) attempts=0 - max_tries=5 - while len(ln.graph()["edges"]) != expected and attempts < max_tries: + actual = 0 + while actual != expected: + actual = len(ln.graph()["edges"]) + if attempts > 10: + break attempts+=1 - sleep(1) - self.log.info(f"LN {ln.name} has graph with all {expected} channels") + sleep(5) + if actual == expected: + self.log.info(f"LN {ln.name} has graph with all {expected} channels") + else: + self.log.error(f"LN {ln.name} graph is INCOMPLETE - {actual} of {expected} channels") ch_ann_threads = [ threading.Thread(target=ln_all_chs, args=(self, ln)) for ln in ln_nodes ] for thread in ch_ann_threads: - sleep(1) thread.start() - all(thread.join(timeout=THREAD_JOIN_TIMEOUT) is None for thread in ch_ann_threads) + all(thread.join(timeout=THREAD_JOIN_TIMEOUT*2) is None for thread in ch_ann_threads) self.log.info("All LN nodes have complete graph") ## @@ -359,14 +362,15 @@ def matching_graph(self, expected, ln): while not done: actual = ln.graph()["edges"] self.log.debug(f"LN {ln.name} channel graph edges: {actual}") - assert (len(expected) == len(actual),f"Expected edges {expected}, actual edges {actual}") - if len(actual) > 0: done = True + if len(actual) > 0: + done = True + assert len(expected) == len(actual), f"Expected edges {len(expected)}, actual edges {len(actual)}\n{actual}" for i, actual_ch in enumerate(actual): expected_ch = expected[i] capacity = expected_ch["capacity"] # We assert this because it isn't updated as part of policy. # If this fails we have a bigger issue - assert int(actual_ch["capacity"]) == capacity, f"LN {ln.name} graph capacity mismatch:\n actual: {actual_ch}\n expected: {expected_ch}" + assert int(actual_ch["capacity"]) == capacity, f"LN {ln.name} graph capacity mismatch:\n actual: {actual_ch["capacity"]}\n expected: {capacity}" # Policies were not defined in network.yaml if "source_policy" not in expected_ch or "target_policy" not in expected_ch: From 57671096db5dd6275c5a3f6b709643b8ccb1c405 Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Sat, 19 Apr 2025 23:06:05 -0400 Subject: [PATCH 12/24] fix linter findings --- resources/plugins/simln/plugin.py | 40 +++++-- resources/scenarios/commander.py | 15 ++- resources/scenarios/ln_framework/ln.py | 155 +++++++++++++++---------- resources/scenarios/ln_init.py | 48 ++++---- src/warnet/bitcoin.py | 3 +- src/warnet/deploy.py | 10 +- src/warnet/graph.py | 4 +- src/warnet/k8s.py | 10 +- 8 files changed, 167 insertions(+), 118 deletions(-) diff --git a/resources/plugins/simln/plugin.py b/resources/plugins/simln/plugin.py index 49cf6c9fe..c049e5440 100755 --- a/resources/plugins/simln/plugin.py +++ b/resources/plugins/simln/plugin.py @@ -15,9 +15,9 @@ get_default_namespace, get_mission, get_static_client, + read_file_from_container, wait_for_init, write_file_to_container, - read_file_from_container, ) from warnet.process import run_command @@ -146,7 +146,7 @@ def _launch_activity(activity: Optional[list[dict]], plugin_dir: str) -> str: activity_json = _generate_activity_json(activity) wait_for_init(name, namespace=get_default_namespace(), quiet=True) - #write cert files to container + # write cert files to container transfer_cln_certs(name) if write_file_to_container( name, @@ -186,24 +186,46 @@ def _generate_activity_json(activity: Optional[list[dict]]) -> str: return json.dumps(data, indent=2) + def transfer_cln_certs(name): dst_container = "init" cln_root = "/root/.lightning/regtest" for i in get_mission(LIGHTNING_MISSION): ln_name = i.metadata.name if "cln" in ln_name: - copyfile(ln_name, "cln", f"{cln_root}/ca.pem", name, dst_container, f"/working/{ln_name}-ca.pem") - copyfile(ln_name, "cln", f"{cln_root}/client.pem", name, dst_container, f"/working/{ln_name}-client.pem") - copyfile(ln_name, "cln", f"{cln_root}/client-key.pem", name, dst_container, f"/working/{ln_name}-client-key.pem") + copyfile( + ln_name, + "cln", + f"{cln_root}/ca.pem", + name, + dst_container, + f"/working/{ln_name}-ca.pem", + ) + copyfile( + ln_name, + "cln", + f"{cln_root}/client.pem", + name, + dst_container, + f"/working/{ln_name}-client.pem", + ) + copyfile( + ln_name, + "cln", + f"{cln_root}/client-key.pem", + name, + dst_container, + f"/working/{ln_name}-client-key.pem", + ) def copyfile(pod_name, src_container, source_path, dst_name, dst_container, dst_path): - namespace=get_default_namespace() + namespace = get_default_namespace() file_data = read_file_from_container(pod_name, source_path, src_container, namespace) if write_file_to_container( - dst_name, - dst_container, - dst_path, + dst_name, + dst_container, + dst_path, file_data, namespace=namespace, quiet=True, diff --git a/resources/scenarios/commander.py b/resources/scenarios/commander.py index 9c4504443..0e8f34169 100644 --- a/resources/scenarios/commander.py +++ b/resources/scenarios/commander.py @@ -90,21 +90,23 @@ def auth_proxy_request(self, method, path, postdata): AuthServiceProxy.oldrequest = AuthServiceProxy._request AuthServiceProxy._request = auth_proxy_request + # Create a custom formatter class ColorFormatter(logging.Formatter): """Custom formatter to add color based on log level.""" + # Define ANSI color codes - RED = '\033[91m' - YELLOW = '\033[93m' - GREEN = '\033[92m' - RESET = '\033[0m' + RED = "\033[91m" + YELLOW = "\033[93m" + GREEN = "\033[92m" + RESET = "\033[0m" FORMATS = { logging.DEBUG: f"{RESET}%(name)-8s - Thread-%(thread)d - %(message)s{RESET}", logging.INFO: f"{RESET}%(name)-8s - %(message)s{RESET}", logging.WARNING: f"{YELLOW}%(name)-8s - %(message)s{RESET}", logging.ERROR: f"{RED}%(name)-8s - %(message)s{RESET}", - logging.CRITICAL: f"{RED}##%(name)-8s - %(message)s##{RESET}" + logging.CRITICAL: f"{RED}##%(name)-8s - %(message)s##{RESET}", } def format(self, record): @@ -112,6 +114,7 @@ def format(self, record): formatter = logging.Formatter(log_fmt) return formatter.format(record) + class Commander(BitcoinTestFramework): # required by subclasses of BitcoinTestFramework def set_test_params(self): @@ -222,7 +225,7 @@ def setup(self): self.tanks[tank["tank"]] = node for ln in WARNET["lightning"]: - #create the correct implementation based on pod name + # create the correct implementation based on pod name if "-cln" in ln: self.lns[ln] = CLN(ln) else: diff --git a/resources/scenarios/ln_framework/ln.py b/resources/scenarios/ln_framework/ln.py index 1854f5674..934057b7f 100644 --- a/resources/scenarios/ln_framework/ln.py +++ b/resources/scenarios/ln_framework/ln.py @@ -1,41 +1,44 @@ -from abc import ABC, abstractmethod import base64 import http.client -import logging import json +import logging import ssl +from abc import ABC, abstractmethod from time import sleep from typing import Optional + from kubernetes import client, config from kubernetes.stream import stream # hard-coded deterministic lnd credentials ADMIN_MACAROON_HEX = "0201036c6e6402f801030a1062beabbf2a614b112128afa0c0b4fdd61201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e657261746512047265616400000620b17be53e367290871681055d0de15587f6d1cd47d1248fe2662ae27f62cfbdc6" -# Don't worry about ln's self-signed certificates +# Don't worry about lnd's self-signed certificates INSECURE_CONTEXT = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) INSECURE_CONTEXT.check_hostname = False INSECURE_CONTEXT.verify_mode = ssl.CERT_NONE + +# execute kubernetes command def run_command(name, command: list[str], namespace: Optional[str] = "default") -> str: config.load_incluster_config() sclient = client.CoreV1Api() resp = stream( - sclient.connect_get_namespaced_pod_exec, - name, - namespace, - command=command, - stderr=True, - stdin=False, - stdout=True, - tty=False, - _request_timeout=20, - _preload_content=False, - ) + sclient.connect_get_namespaced_pod_exec, + name, + namespace, + command=command, + stderr=True, + stdin=False, + stdout=True, + tty=False, + _request_timeout=20, + _preload_content=False, + ) result = "" while resp.is_open(): resp.update(timeout=5) if resp.peek_stdout(): - result+=resp.read_stdout() + result += resp.read_stdout() if resp.peek_stderr(): raise Exception(resp.read_stderr()) resp.close() @@ -103,28 +106,31 @@ def to_lnd_chanpolicy(self, capacity): "min_htlc_msat_specified": True, } + # Create a custom formatter class ColorFormatter(logging.Formatter): """Custom formatter to add color based on log level.""" + # Define ANSI color codes - RED = '\033[91m' - YELLOW = '\033[93m' - GREEN = '\033[92m' - RESET = '\033[0m' + RED = "\033[91m" + YELLOW = "\033[93m" + GREEN = "\033[92m" + RESET = "\033[0m" FORMATS = { logging.DEBUG: f"{RESET}%(asctime)s - (name)-8s - Thread-%(thread)d - %(message)s{RESET}", logging.INFO: f"{RESET}%(asctime)s - (name)-8s - %(message)s{RESET}", logging.WARNING: f"{YELLOW}%(asctime)s - (name)-8s - %(message)s{RESET}", logging.ERROR: f"{RED}%(asctime)s - (name)-8s - %(message)s{RESET}", - logging.CRITICAL: f"{RED}##%(asctime)s - (name)-8s - %(message)s##{RESET}" + logging.CRITICAL: f"{RED}##%(asctime)s - (name)-8s - %(message)s##{RESET}", } def format(self, record): log_fmt = self.FORMATS.get(record.levelno) formatter = logging.Formatter(log_fmt) return formatter.format(record) - + + class LNNode(ABC): @abstractmethod def __init__(self, pod_name): @@ -139,7 +145,7 @@ def __init__(self, pod_name): @staticmethod def param_dict_to_list(params: dict) -> list[str]: - return [f'{k}={v}' for k,v in params.items()] + return [f"{k}={v}" for k, v in params.items()] @staticmethod def hex_to_b64(hex): @@ -165,21 +171,41 @@ def walletbalance(self) -> int: pass @abstractmethod - def graph(self): + def connect(self, target_uri) -> dict: pass + @abstractmethod + def channel(self, pk, capacity, push_amt, fee_rate) -> dict: + pass + + @abstractmethod + def graph(self) -> dict: + pass + + @abstractmethod + def update(self, txid_hex: str, policy: dict, capacity: int) -> dict: + pass + + class CLN(LNNode): def __init__(self, pod_name): super().__init__(pod_name) self.headers = {} self.impl = "cln" - - def rpc(self, method: str, params: list[str] = [], namespace: Optional[str] = "default", max_tries=5): + + def rpc( + self, + method: str, + params: list[str] = None, + namespace: Optional[str] = "default", + max_tries=5, + ): cmd = ["lightning-cli", method] - cmd.extend(params) - attempt=0 + if params: + cmd.extend(params) + attempt = 0 while attempt < max_tries: - attempt+=1 + attempt += 1 try: response = run_command(self.name, cmd, namespace) if not response: @@ -191,9 +217,9 @@ def rpc(self, method: str, params: list[str] = [], namespace: Optional[str] = "d return None def newaddress(self, max_tries=2): - attempt=0 + attempt = 0 while attempt < max_tries: - attempt+=1 + attempt += 1 response = self.rpc("newaddr") if not response: sleep(2) @@ -212,12 +238,12 @@ def uri(self): res = json.loads(self.rpc("getinfo")) if len(res["address"]) < 1: return None - return f'{res["id"]}@{res["address"][0]["address"]}:{res["address"][0]["port"]}' + return f"{res['id']}@{res['address'][0]['address']}:{res['address'][0]['port']}" def walletbalance(self, max_tries=2): - attempt=0 + attempt = 0 while attempt < max_tries: - attempt+=1 + attempt += 1 response = self.rpc("listfunds") if not response: sleep(2) @@ -225,11 +251,11 @@ def walletbalance(self, max_tries=2): res = json.loads(response) return int(sum(o["amount_msat"] for o in res["outputs"]) / 1000) return 0 - - def connect(self, target_uri, max_tries=5): - attempt=0 + + def connect(self, target_uri, max_tries=5) -> dict: + attempt = 0 while attempt < max_tries: - attempt+=1 + attempt += 1 response = self.rpc("connect", [target_uri]) if response: res = json.loads(response) @@ -243,58 +269,59 @@ def connect(self, target_uri, max_tries=5): else: self.log.debug(f"connect response: {response}, wait and retry...") sleep(2) - return "" - - def channel(self, pk, capacity, push_amt, fee_rate, max_tries=5): - data={ + return None + + def channel(self, pk, capacity, push_amt, fee_rate, max_tries=5) -> dict: + data = { "amount": capacity, "push_msat": push_amt, "id": pk, "feerate": fee_rate, } - attempt=0 + attempt = 0 while attempt < max_tries: - attempt+=1 + attempt += 1 response = self.rpc("fundchannel", self.param_dict_to_list(data)) if response: res = json.loads(response) if "txid" in res: - return {"txid": res["txid"], "outpoint": f'{res["txid"]}:{res["outnum"]}'} + return {"txid": res["txid"], "outpoint": f"{res['txid']}:{res['outnum']}"} else: self.log.warning(f"unable to open channel: {res}, wait and retry...") sleep(1) else: self.log.debug(f"channel response: {response}, wait and retry...") sleep(2) - return "" + return None - def graph(self, max_tries=2): - attempt=0 + def graph(self, max_tries=2) -> dict: + attempt = 0 while attempt < max_tries: - attempt+=1 + attempt += 1 response = self.rpc("listchannels") if response: res = json.loads(response) if "channels" in res: # Map to desired output - filtered_channels = [ch for ch in res['channels'] if ch['direction'] == 1] + filtered_channels = [ch for ch in res["channels"] if ch["direction"] == 1] # Sort by short_channel_id - block -> index -> output - sorted_channels = sorted(filtered_channels, key=lambda x: x['short_channel_id']) + sorted_channels = sorted(filtered_channels, key=lambda x: x["short_channel_id"]) # Add capacity by dividing amount_msat by 1000 for channel in sorted_channels: - channel['capacity'] = channel['amount_msat'] // 1000 - return {'edges': sorted_channels} + channel["capacity"] = channel["amount_msat"] // 1000 + return {"edges": sorted_channels} else: self.log.warning(f"unable to open channel: {res}, wait and retry...") sleep(1) else: self.log.debug(f"channel response: {response}, wait and retry...") sleep(2) - return "" - - def update(self, txid_hex: str, policy: dict, capacity: int, max_tries=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 CLN yet!") - return + return None + class LND(LNNode): def __init__(self, pod_name): @@ -303,9 +330,9 @@ def __init__(self, pod_name): host=pod_name, port=8080, timeout=5, context=INSECURE_CONTEXT ) self.headers = { - "Grpc-Metadata-macaroon": ADMIN_MACAROON_HEX, - "Connection": "close", - } + "Grpc-Metadata-macaroon": ADMIN_MACAROON_HEX, + "Connection": "close", + } self.impl = "lnd" def get(self, uri): @@ -322,8 +349,8 @@ def get(self, uri): def post(self, uri, data): body = json.dumps(data) - post_header=self.headers - post_header["Content-Length"]=str(len(body)) + post_header = self.headers + post_header["Content-Length"] = str(len(body)) post_header["Content-Type"] = "application/json" attempt = 0 while True: @@ -352,9 +379,9 @@ def post(self, uri, data): sleep(1) def newaddress(self, max_tries=10): - attempt=0 + attempt = 0 while attempt < max_tries: - attempt+=1 + attempt += 1 response = self.get("/v1/newaddress") res = json.loads(response) if "address" in res: @@ -397,7 +424,7 @@ def channel(self, pk, capacity, push_amt, fee_rate): res = json.loads(response) if "result" in res: res["txid"] = self.b64_to_hex(res["result"]["chan_pending"]["txid"], reverse=True) - res["outpoint"] = f'{res["txid"]}:{res["result"]["chan_pending"]["output_index"]}' + res["outpoint"] = f"{res['txid']}:{res['result']['chan_pending']['output_index']}" except Exception as e: self.log.error(f"Error opening LND channel: {e}") return res diff --git a/resources/scenarios/ln_init.py b/resources/scenarios/ln_init.py index fa884d706..65931ed29 100644 --- a/resources/scenarios/ln_init.py +++ b/resources/scenarios/ln_init.py @@ -6,7 +6,8 @@ from commander import Commander from ln_framework.ln import Policy -THREAD_JOIN_TIMEOUT=20 +THREAD_JOIN_TIMEOUT = 20 + class LNInit(Commander): def set_test_params(self): @@ -119,9 +120,7 @@ def get_ln_uri(self, ln): break sleep(1) - uri_threads = [ - threading.Thread(target=get_ln_uri, args=(self, ln)) for ln in ln_nodes - ] + uri_threads = [threading.Thread(target=get_ln_uri, args=(self, ln)) for ln in ln_nodes] for thread in uri_threads: thread.start() @@ -144,7 +143,7 @@ def get_ln_uri(self, ln): # Explicit connections between every pair of channel partners for ch in self.channels: node_names = self.node_names(ln_nodes) - if not ch["source"] in node_names or not ch["target"] in node_names: + if ch["source"] not in node_names or ch["target"] not in node_names: self.log.error(f"LN Channel {ch} not available, removing") self.channels.remove(ch) continue @@ -156,7 +155,7 @@ def get_ln_uri(self, ln): def connect_ln(self, pair): while True: - if not pair[1].name in ln_uris: + if pair[1].name not in ln_uris: self.log.info(f"LN URIs for {pair[1].name} not found") break res = pair[0].connect(ln_uris[pair[1].name]) @@ -197,7 +196,7 @@ def connect_ln(self, pair): # so their channel ids are deterministic ch_by_block = {} for ch in self.channels: - if not "id" in ch or not "block" in ch["id"]: + if "id" not in ch or "block" not in ch["id"]: self.log.info(f"LN Channel {ch} not found") continue block = ch["id"]["block"] @@ -218,7 +217,7 @@ def connect_ln(self, pair): gen(need - 1) def open_channel(self, ch, fee_rate): - if not ch["source"] in self.lns or not ch["target"] in ln_uris: + if ch["source"] not in self.lns or ch["target"] not in ln_uris: return src = self.lns[ch["source"]] tgt_uri = ln_uris[ch["target"]] @@ -236,12 +235,12 @@ def open_channel(self, ch, fee_rate): ch["txid"] = res["txid"] self.log.info( f"Channel open {ch['source']} -> {ch['target']}\n " - + f"outpoint={res["outpoint"]}\n " + + f"outpoint={res['outpoint']}\n " + f"expected channel id: {ch['id']}" ) else: - ch["txid"] = "N/A" - self.log.info( + ch["txid"] = "N/A" + self.log.info( "Unexpected channel open response:\n " + f"From {ch['source']} -> {ch['target']} fee_rate={fee_rate}\n " + f"{res}" @@ -287,26 +286,26 @@ def open_channel(self, ch, fee_rate): def ln_all_chs(self, ln): expected = len(self.channels) - attempts=0 + attempts = 0 actual = 0 while actual != expected: actual = len(ln.graph()["edges"]) if attempts > 10: break - attempts+=1 + attempts += 1 sleep(5) if actual == expected: self.log.info(f"LN {ln.name} has graph with all {expected} channels") else: - self.log.error(f"LN {ln.name} graph is INCOMPLETE - {actual} of {expected} channels") + self.log.error( + f"LN {ln.name} graph is INCOMPLETE - {actual} of {expected} channels" + ) - ch_ann_threads = [ - threading.Thread(target=ln_all_chs, args=(self, ln)) for ln in ln_nodes - ] + ch_ann_threads = [threading.Thread(target=ln_all_chs, args=(self, ln)) for ln in ln_nodes] for thread in ch_ann_threads: thread.start() - all(thread.join(timeout=THREAD_JOIN_TIMEOUT*2) is None for thread in ch_ann_threads) + all(thread.join(timeout=THREAD_JOIN_TIMEOUT * 2) is None for thread in ch_ann_threads) self.log.info("All LN nodes have complete graph") ## @@ -364,15 +363,19 @@ def matching_graph(self, expected, ln): while not done: actual = ln.graph()["edges"] self.log.debug(f"LN {ln.name} channel graph edges: {actual}") - if len(actual) > 0: + if len(actual) > 0: done = True - assert len(expected) == len(actual), f"Expected edges {len(expected)}, actual edges {len(actual)}\n{actual}" + assert len(expected) == len(actual), ( + f"Expected edges {len(expected)}, actual edges {len(actual)}\n{actual}" + ) for i, actual_ch in enumerate(actual): expected_ch = expected[i] capacity = expected_ch["capacity"] # We assert this because it isn't updated as part of policy. # If this fails we have a bigger issue - assert int(actual_ch["capacity"]) == capacity, f"LN {ln.name} graph capacity mismatch:\n actual: {actual_ch["capacity"]}\n expected: {capacity}" + assert int(actual_ch["capacity"]) == capacity, ( + f"LN {ln.name} graph capacity mismatch:\n actual: {actual_ch['capacity']}\n expected: {capacity}" + ) # Policies were not defined in network.yaml if "source_policy" not in expected_ch or "target_policy" not in expected_ch: @@ -400,8 +403,7 @@ def matching_graph(self, expected, ln): expected = sorted(self.channels, key=lambda ch: (ch["id"]["block"], ch["id"]["index"])) policy_threads = [ - threading.Thread(target=matching_graph, args=(self, expected, ln)) - for ln in ln_nodes + threading.Thread(target=matching_graph, args=(self, expected, ln)) for ln in ln_nodes ] for thread in policy_threads: thread.start() diff --git a/src/warnet/bitcoin.py b/src/warnet/bitcoin.py index dd06c773c..9d0c54f50 100644 --- a/src/warnet/bitcoin.py +++ b/src/warnet/bitcoin.py @@ -6,10 +6,9 @@ from typing import Optional import click -from urllib3.exceptions import MaxRetryError - from test_framework.messages import ser_uint256 from test_framework.p2p import MESSAGEMAP +from urllib3.exceptions import MaxRetryError from .constants import BITCOINCORE_CONTAINER from .k8s import get_default_namespace_or, get_mission, pod_log diff --git a/src/warnet/deploy.py b/src/warnet/deploy.py index 6eb09f1d6..c58a0c8df 100644 --- a/src/warnet/deploy.py +++ b/src/warnet/deploy.py @@ -114,15 +114,7 @@ def _deploy(directory, debug, namespace, to_all_users): processes.append(caddy_process) # Wait for the network process to complete - print(f"Waiting for network process thread {network_process.pid} join") - network_process.join(timeout=500) - if network_process.is_alive(): - print("Process hit the timeout (still running after 500 seconds)") - # Optionally terminate the process if it timed out - network_process.terminate() - else: - print(f"Network process completed before time limit") - # input("Press Enter to continue...") + network_process.join() run_plugins(directory, HookValue.POST_NETWORK, namespace) diff --git a/src/warnet/graph.py b/src/warnet/graph.py index a63e97f1c..cff7caa15 100644 --- a/src/warnet/graph.py +++ b/src/warnet/graph.py @@ -254,7 +254,7 @@ def _import_network(graph_file_path, output_path): tank = f"tank-{index:04d}" pk_to_tank[node["pub_key"]] = tank tank_to_pk[tank] = node["pub_key"] - tanks[tank] = {"name": tank, "ln": {"lnd": True, "channels": []}} + tanks[tank] = {"name": tank, "ln": {"lnd": True}, "lnd": {"channels": []}} index += 1 print(f"Imported {index} nodes") @@ -275,7 +275,7 @@ def _import_network(graph_file_path, output_path): "source_policy": Policy.from_lnd_describegraph(edge["node1_policy"]).to_dict(), "target_policy": Policy.from_lnd_describegraph(edge["node2_policy"]).to_dict(), } - tanks[source]["ln"]["channels"].append(channel) + tanks[source]["lnd"]["channels"].append(channel) index += 1 if index > 1000: index = 1 diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index 7bdd5bde2..26400c8b3 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -599,7 +599,11 @@ def download( def read_file_from_container( - pod_name, source_path: Path, container_name: str = "", namespace: Optional[str] = None, quiet: bool = False + pod_name, + source_path: Path, + container_name: str = "", + namespace: Optional[str] = None, + quiet: bool = False, ) -> str: """Download the file from the `source_path` to the `destination_path`""" @@ -621,12 +625,12 @@ def read_file_from_container( tty=False, _preload_content=False, ) - + result = "" while resp.is_open(): resp.update(timeout=5) if resp.peek_stdout(): - result+=resp.read_stdout() + result += resp.read_stdout() if resp.peek_stderr(): raise Exception(resp.read_stderr()) resp.close() From ff9d32e40e78dfb0783ae453b19b8cf9a3a09623 Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Sun, 20 Apr 2025 15:38:53 -0400 Subject: [PATCH 13/24] don't setup a ln connection to itself migrate references from -ln to -lnd remove eclair references for now support ln: type: True in node-defaults.yaml update cln test data reorder channel index for stability LNNode shares commander logger --- docs/plugins.md | 2 +- resources/charts/bitcoincore/Chart.yaml | 3 - .../bitcoincore/charts/eclair/.helmignore | 23 ---- .../bitcoincore/charts/eclair/Chart.yaml | 24 ---- .../charts/eclair/templates/_helpers.tpl | 62 --------- .../charts/eclair/templates/configmap.yaml | 30 ---- .../charts/eclair/templates/pod.yaml | 68 ---------- .../charts/eclair/templates/service.yaml | 20 --- .../bitcoincore/charts/eclair/values.yaml | 128 ------------------ resources/charts/bitcoincore/values.yaml | 1 - resources/networks/hello/network.yaml | 6 +- resources/plugins/hello/README.md | 6 +- resources/plugins/simln/README.md | 6 +- resources/scenarios/commander.py | 4 +- resources/scenarios/ln_framework/ln.py | 89 +++++------- resources/scenarios/ln_init.py | 12 +- src/warnet/deploy.py | 15 +- src/warnet/graph.py | 2 +- test/data/LN_10.json | 4 +- test/data/cln/network.yaml | 20 +++ test/data/cln/node-defaults.yaml | 5 - test/data/eclair/network.yaml | 19 --- test/data/eclair/node-defaults.yaml | 17 --- test/data/ln/network.yaml | 4 +- test/ln_basic_test.py | 28 ++-- test/ln_test.py | 6 +- 26 files changed, 108 insertions(+), 496 deletions(-) delete mode 100644 resources/charts/bitcoincore/charts/eclair/.helmignore delete mode 100644 resources/charts/bitcoincore/charts/eclair/Chart.yaml delete mode 100644 resources/charts/bitcoincore/charts/eclair/templates/_helpers.tpl delete mode 100644 resources/charts/bitcoincore/charts/eclair/templates/configmap.yaml delete mode 100644 resources/charts/bitcoincore/charts/eclair/templates/pod.yaml delete mode 100644 resources/charts/bitcoincore/charts/eclair/templates/service.yaml delete mode 100644 resources/charts/bitcoincore/charts/eclair/values.yaml delete mode 100644 test/data/eclair/network.yaml delete mode 100644 test/data/eclair/node-defaults.yaml diff --git a/docs/plugins.md b/docs/plugins.md index bce833864..8ab1d9295 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -34,7 +34,7 @@ plugins: postDeploy: # Plugins will run after all the `deploy` code has run. simln: entrypoint: "../plugins/simln" - activity: '[{"source": "tank-0003-ln", "destination": "tank-0005-ln", "interval_secs": 1, "amount_msat": 2000}]' + activity: '[{"source": "tank-0003-lnd", "destination": "tank-0005-lnd", "interval_secs": 1, "amount_msat": 2000}]' hello: entrypoint: "../plugins/hello" podName: "hello-post-deploy" diff --git a/resources/charts/bitcoincore/Chart.yaml b/resources/charts/bitcoincore/Chart.yaml index 00a9cd3aa..36f7498af 100644 --- a/resources/charts/bitcoincore/Chart.yaml +++ b/resources/charts/bitcoincore/Chart.yaml @@ -6,9 +6,6 @@ dependencies: - name: lnd version: 0.1.0 condition: ln.lnd - - name: eclair - version: 0.1.0 - condition: ln.eclair - name: cln version: 0.1.0 condition: ln.cln diff --git a/resources/charts/bitcoincore/charts/eclair/.helmignore b/resources/charts/bitcoincore/charts/eclair/.helmignore deleted file mode 100644 index 0e8a0eb36..000000000 --- a/resources/charts/bitcoincore/charts/eclair/.helmignore +++ /dev/null @@ -1,23 +0,0 @@ -# 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 deleted file mode 100644 index c496a596b..000000000 --- a/resources/charts/bitcoincore/charts/eclair/Chart.yaml +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index 2d9f220ce..000000000 --- a/resources/charts/bitcoincore/charts/eclair/templates/_helpers.tpl +++ /dev/null @@ -1,62 +0,0 @@ -{{/* -Expand the name of the chart. -*/}} -{{- define "eclair.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} -{{- 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 }} -{{- $name := default .Chart.Name .Values.nameOverride }} -{{- if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- 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 deleted file mode 100644 index 6cdb1ca82..000000000 --- a/resources/charts/bitcoincore/charts/eclair/templates/configmap.yaml +++ /dev/null @@ -1,30 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ include "eclair.fullname" . }} - labels: - {{- include "eclair.labels" . | nindent 4 }} -data: - config: | - {{- .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.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 }} ---- -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 deleted file mode 100644 index 3e5543510..000000000 --- a/resources/charts/bitcoincore/charts/eclair/templates/pod.yaml +++ /dev/null @@ -1,68 +0,0 @@ -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 }} - 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 }} - ports: - - name: server - containerPort: {{ .Values.ServerPort }} - protocol: TCP - - name: rest - containerPort: {{ .Values.RestPort }} - protocol: TCP - livenessProbe: - {{- toYaml .Values.livenessProbe | nindent 12 }} - readinessProbe: - {{- toYaml .Values.readinessProbe | nindent 12 }} - resources: - {{- toYaml .Values.resources | nindent 12 }} - volumeMounts: - {{- with .Values.volumeMounts }} - {{- toYaml . | nindent 12 }} - {{- end }} - - mountPath: /home/eclair/.eclair/eclair.conf - name: config - subPath: eclair.confg - volumes: - {{- with .Values.volumes }} - {{- toYaml . | nindent 8 }} - {{- 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 deleted file mode 100644 index f1458557a..000000000 --- a/resources/charts/bitcoincore/charts/eclair/templates/service.yaml +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index c57adf6be..000000000 --- a/resources/charts/bitcoincore/charts/eclair/values.yaml +++ /dev/null @@ -1,128 +0,0 @@ -# Default values for eclair. -# This is a YAML-formatted file. -# Declare variables to be passed into your templates. -namespace: warnet - -# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ -image: - repository: acinq/eclair - # This sets the pull policy for images. - pullPolicy: IfNotPresent - # Overrides the image tag whose default is the chart appVersion. - tag: "latest" - -# This is for the secretes for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ -imagePullSecrets: [] -nameOverride: "" -fullnameOverride: "" - -# This is for setting Kubernetes Labels to a Pod. -# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ -podLabels: - app: "warnet" - mission: "lightning" - -podSecurityContext: {} - # fsGroup: 2000 - -securityContext: {} - # capabilities: - # drop: - # - ALL - # readOnlyRootFilesystem: true - # runAsNonRoot: true - # runAsUser: 1000 - -service: - type: ClusterIP - -ServerPort: 9735 -RestPort: 8080 - -# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ -ingress: - enabled: false - className: "" - annotations: {} - # kubernetes.io/ingress.class: nginx - # kubernetes.io/tls-acme: "true" - hosts: - - host: chart-example.local - paths: - - path: / - pathType: ImplementationSpecific - tls: [] - # - secretName: chart-example-tls - # hosts: - # - chart-example.local - -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: - httpGet: - path: /getinfo - port: 8080 - failureThreshold: 3 - initialDelaySeconds: 10 - periodSeconds: 20 - timeoutSeconds: 5 -readinessProbe: - initialDelaySeconds: 10 - failureThreshold: 3 - periodSeconds: 10 - tcpSocket: - port: 8080 -startupProbe: - initialDelaySeconds: 15 - periodSeconds: 10 - failureThreshold: 5 - timeoutSeconds: 30 - exec: - command: - - sh - - -c - - 'curl -s http://localhost:8080/getinfo | grep -q "blockHeight"' - -# Additional volumes on the output Deployment definition. -volumes: [] -# - name: foo -# secret: -# secretName: mysecret -# optional: false - -# Additional volumeMounts on the output Deployment definition. -volumeMounts: [] -# - name: foo -# mountPath: "/etc/foo" -# readOnly: true - -nodeSelector: {} - -tolerations: [] - -affinity: {} - -baseConfig: | - eclair.server.port=9735 - eclair.api.enabled=true - eclair.api.password=foo - eclair.api.port=8080 - eclair.bitcoind.rpcuser=user - # zmq* and eclair.bitcoind.rpcpassword are set in configmap.yaml - -config: "" - -defaultConfig: "" - -channels: [] diff --git a/resources/charts/bitcoincore/values.yaml b/resources/charts/bitcoincore/values.yaml index 7b049ae65..67f1b9b10 100644 --- a/resources/charts/bitcoincore/values.yaml +++ b/resources/charts/bitcoincore/values.yaml @@ -142,5 +142,4 @@ loadSnapshot: ln: lnd: false - eclair: false cln: false diff --git a/resources/networks/hello/network.yaml b/resources/networks/hello/network.yaml index f5acf0a83..341e4d761 100644 --- a/resources/networks/hello/network.yaml +++ b/resources/networks/hello/network.yaml @@ -29,7 +29,7 @@ nodes: - id: block: 300 index: 1 - target: tank-0004-ln + target: tank-0004-lnd capacity: 100000 push_amt: 50000 @@ -43,7 +43,7 @@ nodes: - id: block: 300 index: 2 - target: tank-0005-ln + target: tank-0005-lnd capacity: 50000 push_amt: 25000 @@ -66,7 +66,7 @@ plugins: # Each plugin section has a number of hooks available (preDeploy, post helloTo: "postDeploy!" simln: # You can have multiple plugins per hook entrypoint: "../../plugins/simln" - activity: '[{"source": "tank-0003-ln", "destination": "tank-0005-ln", "interval_secs": 1, "amount_msat": 2000}]' + activity: '[{"source": "tank-0003-lnd", "destination": "tank-0005-lnd", "interval_secs": 1, "amount_msat": 2000}]' preNode: # preNode plugins run before each node is deployed hello: entrypoint: "../../plugins/hello" diff --git a/resources/plugins/hello/README.md b/resources/plugins/hello/README.md index 77bb5040f..c608de997 100644 --- a/resources/plugins/hello/README.md +++ b/resources/plugins/hello/README.md @@ -62,7 +62,7 @@ nodes: - id: block: 300 index: 1 - target: tank-0004-ln + target: tank-0004-lnd capacity: 100000 push_amt: 50000 @@ -76,7 +76,7 @@ nodes: - id: block: 300 index: 2 - target: tank-0005-ln + target: tank-0005-lnd capacity: 50000 push_amt: 25000 @@ -99,7 +99,7 @@ plugins: # Each plugin section has a number of hooks available (preDeploy, post helloTo: "postDeploy!" simln: # You can have multiple plugins per hook entrypoint: "../../plugins/simln" - activity: '[{"source": "tank-0003-ln", "destination": "tank-0005-ln", "interval_secs": 1, "amount_msat": 2000}]' + activity: '[{"source": "tank-0003-lnd", "destination": "tank-0005-lnd", "interval_secs": 1, "amount_msat": 2000}]' preNode: # preNode plugins run before each node is deployed hello: entrypoint: "../../plugins/hello" diff --git a/resources/plugins/simln/README.md b/resources/plugins/simln/README.md index f6b24ef92..a65efb71d 100644 --- a/resources/plugins/simln/README.md +++ b/resources/plugins/simln/README.md @@ -65,7 +65,7 @@ nodes: - id: block: 300 index: 1 - target: tank-0004-ln + target: tank-0004-lnd capacity: 100000 push_amt: 50000 @@ -79,7 +79,7 @@ nodes: - id: block: 300 index: 2 - target: tank-0005-ln + target: tank-0005-lnd capacity: 50000 push_amt: 25000 @@ -93,7 +93,7 @@ plugins: postDeploy: simln: entrypoint: "../../plugins/simln" # This is the path to the simln plugin folder (relative to the network.yaml file). - activity: '[{"source": "tank-0003-ln", "destination": "tank-0005-ln", "interval_secs": 1, "amount_msat": 2000}]' + activity: '[{"source": "tank-0003-lnd", "destination": "tank-0005-lnd", "interval_secs": 1, "amount_msat": 2000}]' ```` diff --git a/resources/scenarios/commander.py b/resources/scenarios/commander.py index 0e8f34169..c3cd1655f 100644 --- a/resources/scenarios/commander.py +++ b/resources/scenarios/commander.py @@ -227,9 +227,9 @@ def setup(self): for ln in WARNET["lightning"]: # create the correct implementation based on pod name if "-cln" in ln: - self.lns[ln] = CLN(ln) + self.lns[ln] = CLN(ln, self.log) else: - self.lns[ln] = LND(ln) + self.lns[ln] = LND(ln, self.log) self.num_nodes = len(self.nodes) diff --git a/resources/scenarios/ln_framework/ln.py b/resources/scenarios/ln_framework/ln.py index 934057b7f..ab6fc5b5a 100644 --- a/resources/scenarios/ln_framework/ln.py +++ b/resources/scenarios/ln_framework/ln.py @@ -1,7 +1,6 @@ import base64 import http.client import json -import logging import ssl from abc import ABC, abstractmethod from time import sleep @@ -107,41 +106,11 @@ def to_lnd_chanpolicy(self, capacity): } -# Create a custom formatter -class ColorFormatter(logging.Formatter): - """Custom formatter to add color based on log level.""" - - # Define ANSI color codes - RED = "\033[91m" - YELLOW = "\033[93m" - GREEN = "\033[92m" - RESET = "\033[0m" - - FORMATS = { - logging.DEBUG: f"{RESET}%(asctime)s - (name)-8s - Thread-%(thread)d - %(message)s{RESET}", - logging.INFO: f"{RESET}%(asctime)s - (name)-8s - %(message)s{RESET}", - logging.WARNING: f"{YELLOW}%(asctime)s - (name)-8s - %(message)s{RESET}", - logging.ERROR: f"{RED}%(asctime)s - (name)-8s - %(message)s{RESET}", - logging.CRITICAL: f"{RED}##%(asctime)s - (name)-8s - %(message)s##{RESET}", - } - - def format(self, record): - log_fmt = self.FORMATS.get(record.levelno) - formatter = logging.Formatter(log_fmt) - return formatter.format(record) - - class LNNode(ABC): @abstractmethod - def __init__(self, pod_name): - self.log = logging.getLogger(self.__class__.__name__) + def __init__(self, pod_name, logger): + self.log = logger self.name = pod_name - # Configure logger if it has no handlers - if not self.log.handlers: - handler = logging.StreamHandler() - handler.setFormatter(ColorFormatter()) - self.log.addHandler(handler) - self.log.setLevel(logging.INFO) @staticmethod def param_dict_to_list(params: dict) -> list[str]: @@ -188,8 +157,8 @@ def update(self, txid_hex: str, policy: dict, capacity: int) -> dict: class CLN(LNNode): - def __init__(self, pod_name): - super().__init__(pod_name) + def __init__(self, pod_name, logger): + super().__init__(pod_name, logger) self.headers = {} self.impl = "cln" @@ -324,8 +293,8 @@ def update(self, txid_hex: str, policy: dict, capacity: int, max_tries=2) -> dic class LND(LNNode): - def __init__(self, pod_name): - super().__init__(pod_name) + def __init__(self, pod_name, logger): + super().__init__(pod_name, logger) self.conn = http.client.HTTPSConnection( host=pod_name, port=8080, timeout=5, context=INSECURE_CONTEXT ) @@ -409,25 +378,35 @@ def connect(self, target_uri): res = self.post("/v1/peers", data={"addr": {"pubkey": pk, "host": host}}) return json.loads(res) - def channel(self, pk, capacity, push_amt, fee_rate): + def channel(self, pk, capacity, push_amt, fee_rate, max_tries=2): b64_pk = self.hex_to_b64(pk) - response = self.post( - "/v1/channels/stream", - data={ - "local_funding_amount": capacity, - "push_sat": push_amt, - "node_pubkey": b64_pk, - "sat_per_vbyte": fee_rate, - }, - ) - try: - res = json.loads(response) - if "result" in res: - res["txid"] = self.b64_to_hex(res["result"]["chan_pending"]["txid"], reverse=True) - res["outpoint"] = f"{res['txid']}:{res['result']['chan_pending']['output_index']}" - except Exception as e: - self.log.error(f"Error opening LND channel: {e}") - return res + attempt = 0 + while attempt < max_tries: + attempt += 1 + response = self.post( + "/v1/channels/stream", + data={ + "local_funding_amount": capacity, + "push_sat": push_amt, + "node_pubkey": b64_pk, + "sat_per_vbyte": fee_rate, + }, + ) + try: + res = json.loads(response) + if "result" in res: + res["txid"] = self.b64_to_hex( + res["result"]["chan_pending"]["txid"], reverse=True + ) + res["outpoint"] = ( + f"{res['txid']}:{res['result']['chan_pending']['output_index']}" + ) + return res + self.log.warning(f"Open LND channel error: {res}") + except Exception as e: + self.log.error(f"Error opening LND channel: {e}") + sleep(2) + return None def update(self, txid_hex: str, policy: dict, capacity: int): ln_policy = Policy.from_dict(policy).to_lnd_chanpolicy(capacity) diff --git a/resources/scenarios/ln_init.py b/resources/scenarios/ln_init.py index 65931ed29..63abf19aa 100644 --- a/resources/scenarios/ln_init.py +++ b/resources/scenarios/ln_init.py @@ -137,8 +137,8 @@ def get_ln_uri(self, ln): nodes = list(ln_nodes) prev_node = nodes[-1] for node in nodes: - # if (node, prev_node) not in connections and (prev_node, node) not in connections: - connections.append((node, prev_node)) + if node != prev_node: + connections.append((node, prev_node)) prev_node = node # Explicit connections between every pair of channel partners for ch in self.channels: @@ -174,10 +174,12 @@ def connect_ln(self, pair): ) sleep(1) else: - self.log.info( + self.log.error( f"Unexpected response attempting to connect {pair[0].name} -> {pair[1].name}:\n {res}\n ABORTING" ) - break + raise Exception( + f"Unable to connect {pair[0].name} -> {pair[1].name}:\n {res}" + ) p2p_threads = [ threading.Thread(target=connect_ln, args=(self, pair)) for pair in connections @@ -231,7 +233,7 @@ def open_channel(self, ch, fee_rate): push_amt=ch["push_amt"], fee_rate=fee_rate, ) - if "txid" in res: + if res and "txid" in res: ch["txid"] = res["txid"] self.log.info( f"Channel open {ch['source']} -> {ch['target']}\n " diff --git a/src/warnet/deploy.py b/src/warnet/deploy.py index c58a0c8df..bc30e1ab1 100644 --- a/src/warnet/deploy.py +++ b/src/warnet/deploy.py @@ -365,11 +365,22 @@ 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"] for node in network_file["nodes"]: - if any(node.get("ln", {}).get(key, False) for key in ["lnd", "cln", "eclair"]): - needs_ln_init = True + 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]: + needs_ln_init = True + break + if needs_ln_init: break + 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): + needs_ln_init = True + processes = [] for node in network_file["nodes"]: p = Process(target=deploy_single_node, args=(node, directory, debug, namespace)) diff --git a/src/warnet/graph.py b/src/warnet/graph.py index cff7caa15..d2fc99421 100644 --- a/src/warnet/graph.py +++ b/src/warnet/graph.py @@ -269,7 +269,7 @@ def _import_network(graph_file_path, output_path): source = pk_to_tank[edge["node1_pub"]] channel = { "id": {"block": block, "index": index}, - "target": pk_to_tank[edge["node2_pub"]] + "-ln", + "target": pk_to_tank[edge["node2_pub"]] + "-lnd", "capacity": int(edge["capacity"]), "push_amt": int(edge["capacity"]) // 2, "source_policy": Policy.from_lnd_describegraph(edge["node1_policy"]).to_dict(), diff --git a/test/data/LN_10.json b/test/data/LN_10.json index c9e71adb0..72be3a29e 100644 --- a/test/data/LN_10.json +++ b/test/data/LN_10.json @@ -987,7 +987,7 @@ { "node1_pub": "02b705ad9fff5b30e69dd3810d100372332c1a750db5a50edf7353c77ab486643e", "node2_pub": "037659a0ac8eb3b8d0a720114efc861d3a940382dcfa1403746b4f8f6b2e8810ba", - "channel_id": "891818279431438336", + "channel_id": "908655100992880641", "chan_point": "316bc4974bc4382554b80ba30eaaecf18c3f45f03fed41f99e9600d1c5dd3029:0", "last_update": 1710800025, "capacity": "2000000", @@ -1016,7 +1016,7 @@ { "node1_pub": "02b705ad9fff5b30e69dd3810d100372332c1a750db5a50edf7353c77ab486643e", "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", - "channel_id": "908655100992880641", + "channel_id": "891818279431438336", "chan_point": "0acfd2470db4eb06268b3f22b4c45c0fdcc11d10b6519577662e54823a0f264e:1", "last_update": 1710841427, "capacity": "10000000", diff --git a/test/data/cln/network.yaml b/test/data/cln/network.yaml index f7748ba5d..e3ab3dc45 100644 --- a/test/data/cln/network.yaml +++ b/test/data/cln/network.yaml @@ -5,9 +5,29 @@ nodes: - name: tank-0001 addnode: - tank-0002 + ln: + cln: true + cln: + channels: + - id: + block: 300 + index: 1 + target: tank-0002-cln + capacity: 500000 + push_amt: 25000 - name: tank-0002 addnode: - tank-0000 + ln: + cln: true + cln: + channels: + - id: + block: 300 + index: 2 + target: tank-0001-cln + capacity: 500001 + push_amt: 25001 - name: tank-0003 addnode: - tank-0000 diff --git a/test/data/cln/node-defaults.yaml b/test/data/cln/node-defaults.yaml index 0132e1603..e059cf18b 100644 --- a/test/data/cln/node-defaults.yaml +++ b/test/data/cln/node-defaults.yaml @@ -1,12 +1,8 @@ -# enable collectLogs and metricsExport to activate publish lnd-exporter metrics - #Core configs image: repository: bitcoindevproject/bitcoin pullPolicy: IfNotPresent tag: "27.0" -collectLogs: false -metricsExport: false #LN configs ln: @@ -14,4 +10,3 @@ ln: cln: defaultConfig: | rgb=ff3155 - metricsExport: false diff --git a/test/data/eclair/network.yaml b/test/data/eclair/network.yaml deleted file mode 100644 index f7748ba5d..000000000 --- a/test/data/eclair/network.yaml +++ /dev/null @@ -1,19 +0,0 @@ -nodes: - - name: tank-0000 - addnode: - - tank-0001 - - name: tank-0001 - addnode: - - tank-0002 - - name: tank-0002 - addnode: - - tank-0000 - - name: tank-0003 - addnode: - - tank-0000 - - name: tank-0004 - addnode: - - tank-0000 - - name: tank-0005 - addnode: - - tank-0000 diff --git a/test/data/eclair/node-defaults.yaml b/test/data/eclair/node-defaults.yaml deleted file mode 100644 index 3ef1d9656..000000000 --- a/test/data/eclair/node-defaults.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# enable collectLogs and metricsExport to activate publish lnd-exporter metrics - -#Core configs -image: - repository: bitcoindevproject/bitcoin - pullPolicy: IfNotPresent - tag: "27.0" -collectLogs: false -metricsExport: false - -#LN configs -ln: - eclair: true -eclair: - defaultConfig: | - eclair.node-color=49daaa - metricsExport: false diff --git a/test/data/ln/network.yaml b/test/data/ln/network.yaml index 5d30686d4..a5ab4c36f 100644 --- a/test/data/ln/network.yaml +++ b/test/data/ln/network.yaml @@ -18,7 +18,7 @@ nodes: - id: block: 300 index: 1 - target: tank-0004-ln + target: tank-0004-lnd capacity: 100000 push_amt: 50000 - name: tank-0004 @@ -29,7 +29,7 @@ nodes: - id: block: 300 index: 2 - target: tank-0005-ln + target: tank-0005-lnd capacity: 50000 push_amt: 25000 - name: tank-0005 diff --git a/test/ln_basic_test.py b/test/ln_basic_test.py index dbbbb767a..e12223553 100755 --- a/test/ln_basic_test.py +++ b/test/ln_basic_test.py @@ -16,12 +16,12 @@ def __init__(self): self.network_dir = Path(os.path.dirname(__file__)) / "data" / "ln" self.scen_dir = Path(os.path.dirname(__file__)).parent / "resources" / "scenarios" self.lns = [ - "tank-0000-ln", - "tank-0001-ln", - "tank-0002-ln", - "tank-0003-ln", - "tank-0004-ln", - "tank-0005-ln", + "tank-0000-lnd", + "tank-0001-lnd", + "tank-0002-lnd", + "tank-0003-lnd", + "tank-0004-lnd", + "tank-0005-lnd", ] def run_test(self): @@ -30,13 +30,13 @@ def run_test(self): self.setup_network() # Send a payment across channels opened automatically by ln_init - self.pay_invoice(sender="tank-0005-ln", recipient="tank-0003-ln") + self.pay_invoice(sender="tank-0005-lnd", recipient="tank-0003-lnd") # Manually open two more channels between first three nodes # and send a payment using warnet RPC self.manual_open_channels() self.wait_for_gossip_sync(self.lns[:3], 2 + 2) - self.pay_invoice(sender="tank-0000-ln", recipient="tank-0002-ln") + self.pay_invoice(sender="tank-0000-lnd", recipient="tank-0002-lnd") finally: self.cleanup() @@ -63,27 +63,27 @@ def wait_for_two_txs(self): def manual_open_channels(self): # 0 -> 1 -> 2 - pk1 = self.warnet("ln pubkey tank-0001-ln") - pk2 = self.warnet("ln pubkey tank-0002-ln") + pk1 = self.warnet("ln pubkey tank-0001-lnd") + pk2 = self.warnet("ln pubkey tank-0002-lnd") host1 = "" host2 = "" while not host1 or not host2: if not host1: - host1 = self.warnet("ln host tank-0001-ln") + host1 = self.warnet("ln host tank-0001-lnd") if not host2: - host2 = self.warnet("ln host tank-0002-ln") + host2 = self.warnet("ln host tank-0002-lnd") sleep(1) print( self.warnet( - f"ln rpc tank-0000-ln openchannel --node_key {pk1} --local_amt 100000 --connect {host1}" + f"ln rpc tank-0000-lnd openchannel --node_key {pk1} --local_amt 100000 --connect {host1}" ) ) print( self.warnet( - f"ln rpc tank-0001-ln openchannel --node_key {pk2} --local_amt 100000 --connect {host2}" + f"ln rpc tank-0001-lnd openchannel --node_key {pk2} --local_amt 100000 --connect {host2}" ) ) diff --git a/test/ln_test.py b/test/ln_test.py index ee27b6256..f13fc37fe 100755 --- a/test/ln_test.py +++ b/test/ln_test.py @@ -43,7 +43,7 @@ def test_channel_policies(self): self.log.info("Ensuring node-level channel policy settings") graphs = [] for n in range(10): - ln = f"tank-{n:04d}-ln" + ln = f"tank-{n:04d}-lnd" res = self.warnet(f"ln rpc {ln} describegraph") graphs.append(json.loads(res)["edges"]) @@ -73,8 +73,8 @@ def check_policy(node: int, index: int, field: str, values: tuple): def test_payments(self): def get_and_pay(src, tgt): - src = f"tank-{src:04d}-ln" - tgt = f"tank-{tgt:04d}-ln" + src = f"tank-{src:04d}-lnd" + tgt = f"tank-{tgt:04d}-lnd" invoice = json.loads(self.warnet(f"ln rpc {tgt} addinvoice --amt 230118"))[ "payment_request" ] From cda355be328b6e9242595bd4176a9405aa114fe4 Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Wed, 23 Apr 2025 09:58:23 -0400 Subject: [PATCH 14/24] Add ln_mixed node test Update documentation to address CLN support Make ln_framework/ln.py work in commander or local device Add CLN support to warnet/ln.py Add short sleep between thread starts to improve stability --- .github/workflows/test.yml | 1 + docs/plugins.md | 2 +- docs/scenarios.md | 6 + resources/plugins/simln/README.md | 16 ++- resources/plugins/simln/plugin.py | 2 +- resources/scenarios/ln_framework/ln.py | 86 +++++++++++- resources/scenarios/ln_init.py | 6 + src/warnet/ln.py | 21 ++- test/data/cln/network.yaml | 39 ------ test/data/ln_mixed/network.yaml | 66 +++++++++ .../data/{cln => ln_mixed}/node-defaults.yaml | 10 +- test/ln_mixed_test.py | 125 ++++++++++++++++++ 12 files changed, 320 insertions(+), 60 deletions(-) delete mode 100644 test/data/cln/network.yaml create mode 100644 test/data/ln_mixed/network.yaml rename test/data/{cln => ln_mixed}/node-defaults.yaml (60%) create mode 100755 test/ln_mixed_test.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c6cbded0e..553b899e1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,6 +41,7 @@ jobs: - graph_test.py - logging_test.py - ln_basic_test.py + - ln_mixed_test.py - ln_test.py - rpc_test.py - services_test.py diff --git a/docs/plugins.md b/docs/plugins.md index 8ab1d9295..7d4449076 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -34,7 +34,7 @@ plugins: postDeploy: # Plugins will run after all the `deploy` code has run. simln: entrypoint: "../plugins/simln" - activity: '[{"source": "tank-0003-lnd", "destination": "tank-0005-lnd", "interval_secs": 1, "amount_msat": 2000}]' + activity: '[{"source": "tank-0003-lnd", "destination": "tank-0005-cln", "interval_secs": 1, "amount_msat": 2000}]' hello: entrypoint: "../plugins/hello" podName: "hello-post-deploy" diff --git a/docs/scenarios.md b/docs/scenarios.md index c2892c421..da19880de 100644 --- a/docs/scenarios.md +++ b/docs/scenarios.md @@ -72,3 +72,9 @@ Total Tanks: 6 | Active Scenarios: 0 ## Running a custom scenario You can write your own scenario file and run it in the same way. + +## Scenarios with lightning nodes + +When defining network.yaml all lnd nodes should be indexed in the same block before any cln nodes otherwise node responsiveness causes the expected index to get out of order with actual regardless of how the channels are opened +Review `test/data/ln_mixed/network.yaml` for an example + diff --git a/resources/plugins/simln/README.md b/resources/plugins/simln/README.md index a65efb71d..2593d7665 100644 --- a/resources/plugins/simln/README.md +++ b/resources/plugins/simln/README.md @@ -13,19 +13,29 @@ SimLN also requires access details for each node; however, the SimLN plugin will ```` JSON { - "id": , + "id": -lnd, "address": https://, "macaroon": , "cert": } ```` +SimLN also supports Core Lightning (CLN). CLN nodes connection details are transfered from the CLN node to SimLN node during launch-activity processing. +```` JSON +{ + "id": -cln, + "address": https://, + "ca_cert": /working/-cln-ca.pem, + "client_cert": /working/-cln-client.pem, + "client_key": /working/-cln-client-key.pem +} +```` -Since SimLN already has access to those LND connection details, it means you can focus on the "activity" definitions. +Since SimLN already has access to those LND and CLN connection details, it means you can focus on the "activity" definitions. ### Launch activity definitions from the command line The SimLN plugin takes "activity" definitions like so: -`./simln/plugin.py launch-activity '[{\"source\": \"tank-0003-ln\", \"destination\": \"tank-0005-ln\", \"interval_secs\": 1, \"amount_msat\": 2000}]'"''` +`./simln/plugin.py launch-activity '[{\"source\": \"tank-0003-lnd\", \"destination\": \"tank-0005-lnd\", \"interval_secs\": 1, \"amount_msat\": 2000}]'"''` ### Launch activity definitions from within `network.yaml` When you initialize a new Warnet network, Warnet will create a new `network.yaml` file. If your `network.yaml` file includes lightning nodes, then you can use SimLN to produce activity between those nodes like this: diff --git a/resources/plugins/simln/plugin.py b/resources/plugins/simln/plugin.py index c049e5440..f773e60df 100755 --- a/resources/plugins/simln/plugin.py +++ b/resources/plugins/simln/plugin.py @@ -168,7 +168,7 @@ def _generate_activity_json(activity: Optional[list[dict]]) -> str: ln_name = i.metadata.name port = 10009 node = {"id": ln_name} - if "cln" in ln_name: + if "-cln" in ln_name: port = 9736 node["ca_cert"] = f"/working/{ln_name}-ca.pem" node["client_cert"] = f"/working/{ln_name}-client.pem" diff --git a/resources/scenarios/ln_framework/ln.py b/resources/scenarios/ln_framework/ln.py index ab6fc5b5a..dbe8506c4 100644 --- a/resources/scenarios/ln_framework/ln.py +++ b/resources/scenarios/ln_framework/ln.py @@ -1,6 +1,7 @@ import base64 import http.client import json +import os import ssl from abc import ABC, abstractmethod from time import sleep @@ -19,7 +20,10 @@ # execute kubernetes command def run_command(name, command: list[str], namespace: Optional[str] = "default") -> str: - config.load_incluster_config() + if os.getenv("KUBERNETES_SERVICE_HOST") and os.getenv("KUBERNETES_SERVICE_PORT"): + config.load_incluster_config() + else: + config.load_kube_config() sclient = client.CoreV1Api() resp = stream( sclient.connect_get_namespaced_pod_exec, @@ -209,7 +213,7 @@ def uri(self): return None return f"{res['id']}@{res['address'][0]['address']}:{res['address'][0]['port']}" - def walletbalance(self, max_tries=2): + def walletbalance(self, max_tries=2) -> int: attempt = 0 while attempt < max_tries: attempt += 1 @@ -221,6 +225,18 @@ def walletbalance(self, max_tries=2): return int(sum(o["amount_msat"] for o in res["outputs"]) / 1000) return 0 + def channelbalance(self, max_tries=2) -> int: + attempt = 0 + while attempt < max_tries: + attempt += 1 + response = self.rpc("listfunds") + if not response: + sleep(2) + continue + res = json.loads(response) + return int(sum(o["our_amount_msat"] for o in res["channels"]) / 1000) + return 0 + def connect(self, target_uri, max_tries=5) -> dict: attempt = 0 while attempt < max_tries: @@ -263,6 +279,23 @@ def channel(self, pk, capacity, push_amt, fee_rate, max_tries=5) -> dict: sleep(2) return None + def createinvoice(self, sats, label, description="new invoice") -> str: + response = self.rpc("invoice", [sats * 1000, label, description]) + if response: + res = json.loads(response) + return res["bolt11"] + return None + + def payinvoice(self, payment_request) -> str: + response = self.rpc("pay", [payment_request]) + if response: + res = json.loads(response) + if "code" in res: + return res["message"] + else: + return res["payment_hash"] + return None + def graph(self, max_tries=2) -> dict: attempt = 0 while attempt < max_tries: @@ -304,7 +337,13 @@ def __init__(self, pod_name, logger): } self.impl = "lnd" + def reset_connection(self): + self.conn = http.client.HTTPSConnection( + host=self.name, port=8080, timeout=5, context=INSECURE_CONTEXT + ) + def get(self, uri): + attempt = 0 while True: try: self.conn.request( @@ -313,7 +352,12 @@ def get(self, uri): headers=self.headers, ) return self.conn.getresponse().read().decode("utf8") - except Exception: + except Exception as e: + self.reset_connection() + attempt += 1 + if attempt > 5: + self.log.error(f"Error LND POST, Abort: {e}") + return None sleep(1) def post(self, uri, data): @@ -323,7 +367,6 @@ def post(self, uri, data): post_header["Content-Type"] = "application/json" attempt = 0 while True: - attempt += 1 try: self.conn.request( method="POST", @@ -344,7 +387,12 @@ def post(self, uri, data): except Exception: break return stream - except Exception: + except Exception as e: + self.reset_connection() + attempt += 1 + if attempt > 5: + self.log.error(f"Error LND POST, Abort: {e}") + return None sleep(1) def newaddress(self, max_tries=10): @@ -362,10 +410,14 @@ def newaddress(self, max_tries=10): sleep(1) return False, "" - def walletbalance(self): + def walletbalance(self) -> int: res = self.get("/v1/balance/blockchain") return int(json.loads(res)["confirmed_balance"]) + def channelbalance(self) -> int: + res = self.get("/v1/balance/channels") + return int(json.loads(res)["balance"]) + def uri(self): res = self.get("/v1/getinfo") info = json.loads(res) @@ -420,6 +472,28 @@ def update(self, txid_hex: str, policy: dict, capacity: int): ) return json.loads(res) + def createinvoice(self, sats, label, description="new invoice") -> str: + b64_desc = base64.b64encode(description.encode("utf-8")) + response = self.post( + "/v1/invoices", data={"value": sats, "memo": label, "description_hash": b64_desc} + ) + if response: + res = json.loads(response) + return res["payment_request"] + return None + + def payinvoice(self, payment_request) -> str: + response = self.post( + "/v1/channels/transaction-stream", data={"payment_request": payment_request} + ) + if response: + res = json.loads(response) + if "payment_error" in res: + return res["payment_error"] + else: + return res["payment_hash"] + return None + def graph(self): res = self.get("/v1/graph") return json.loads(res) diff --git a/resources/scenarios/ln_init.py b/resources/scenarios/ln_init.py index 63abf19aa..aa8157d06 100644 --- a/resources/scenarios/ln_init.py +++ b/resources/scenarios/ln_init.py @@ -185,6 +185,7 @@ def connect_ln(self, pair): threading.Thread(target=connect_ln, args=(self, pair)) for pair in connections ] for thread in p2p_threads: + sleep(0.25) thread.start() all(thread.join(timeout=THREAD_JOIN_TIMEOUT) is None for thread in p2p_threads) @@ -258,6 +259,7 @@ def open_channel(self, ch, fee_rate): assert index == ch["id"]["index"], "Channel ID indexes are not consecutive" assert fee_rate >= 1, "Too many TXs in block, out of fee range" t = threading.Thread(target=open_channel, args=(self, ch, fee_rate)) + sleep(0.25) t.start() ch_threads.append(t) @@ -305,6 +307,7 @@ def ln_all_chs(self, ln): ch_ann_threads = [threading.Thread(target=ln_all_chs, args=(self, ln)) for ln in ln_nodes] for thread in ch_ann_threads: + sleep(0.25) thread.start() all(thread.join(timeout=THREAD_JOIN_TIMEOUT * 2) is None for thread in ch_ann_threads) @@ -335,6 +338,7 @@ def update_policy(self, ln, txid_hex, policy, capacity): ch["capacity"], ), ) + sleep(0.25) ts.start() update_threads.append(ts) if "target_policy" in ch: @@ -348,6 +352,7 @@ def update_policy(self, ln, txid_hex, policy, capacity): ch["capacity"], ), ) + sleep(0.25) tt.start() update_threads.append(tt) count = len(update_threads) @@ -408,6 +413,7 @@ def matching_graph(self, expected, ln): threading.Thread(target=matching_graph, args=(self, expected, ln)) for ln in ln_nodes ] for thread in policy_threads: + sleep(0.25) thread.start() all(thread.join(timeout=THREAD_JOIN_TIMEOUT) is None for thread in policy_threads) diff --git a/src/warnet/ln.py b/src/warnet/ln.py index a1f7c1eb2..d45c0a03b 100644 --- a/src/warnet/ln.py +++ b/src/warnet/ln.py @@ -31,7 +31,10 @@ def _rpc(pod_name: str, method: str, params: str = "", namespace: Optional[str] pod = get_pod(pod_name) namespace = get_default_namespace_or(namespace) chain = pod.metadata.labels["chain"] - cmd = f"kubectl -n {namespace} exec {pod_name} -- lncli --network {chain} {method} {' '.join(map(str, params))}" + ln_client = "lncli" + if "-cln" in pod_name: + ln_client = "lightning-cli" + cmd = f"kubectl -n {namespace} exec {pod_name} -- {ln_client} --network {chain} {method} {' '.join(map(str, params))}" return run_command(cmd) @@ -48,7 +51,10 @@ def pubkey( def _pubkey(pod: str): info = _rpc(pod, "getinfo") - return json.loads(info)["identity_pubkey"] + pubkey_key = "identity_pubkey" + if "-cln" in pod: + pubkey_key = "id" + return json.loads(info)[pubkey_key] @ln.command() @@ -64,8 +70,11 @@ def host( def _host(pod): info = _rpc(pod, "getinfo") - uris = json.loads(info)["uris"] - if uris and len(uris) >= 0: - return uris[0].split("@")[1] + if "-cln" in pod: + return json.loads(info)["alias"] else: - return "" + uris = json.loads(info)["uris"] + if uris and len(uris) >= 0: + return uris[0].split("@")[1] + else: + return "" diff --git a/test/data/cln/network.yaml b/test/data/cln/network.yaml deleted file mode 100644 index e3ab3dc45..000000000 --- a/test/data/cln/network.yaml +++ /dev/null @@ -1,39 +0,0 @@ -nodes: - - name: tank-0000 - addnode: - - tank-0001 - - name: tank-0001 - addnode: - - tank-0002 - ln: - cln: true - cln: - channels: - - id: - block: 300 - index: 1 - target: tank-0002-cln - capacity: 500000 - push_amt: 25000 - - name: tank-0002 - addnode: - - tank-0000 - ln: - cln: true - cln: - channels: - - id: - block: 300 - index: 2 - target: tank-0001-cln - capacity: 500001 - push_amt: 25001 - - name: tank-0003 - addnode: - - tank-0000 - - name: tank-0004 - addnode: - - tank-0000 - - name: tank-0005 - addnode: - - tank-0000 diff --git a/test/data/ln_mixed/network.yaml b/test/data/ln_mixed/network.yaml new file mode 100644 index 000000000..2afced756 --- /dev/null +++ b/test/data/ln_mixed/network.yaml @@ -0,0 +1,66 @@ +nodes: + - name: tank-0001 + addnode: + - tank-0003 + ln: + cln: true + cln: + channels: + - id: + block: 300 + index: 4 + target: tank-0003-lnd + capacity: 50001 + push_amt: 25003 + - name: tank-0002 + addnode: + - tank-0001 + ln: + cln: true + cln: + channels: + - id: + block: 300 + index: 5 + target: tank-0001-cln + capacity: 50002 + push_amt: 25001 + - name: tank-0003 + addnode: + - tank-0004 + ln: + lnd: true + lnd: + channels: + - id: + block: 300 + index: 1 + target: tank-0004-lnd + capacity: 100003 + push_amt: 50004 + - name: tank-0004 + addnode: + - tank-0003 + ln: + lnd: true + lnd: + channels: + - id: + block: 300 + index: 2 + target: tank-0005-lnd + capacity: 50004 + push_amt: 25005 + - name: tank-0005 + addnode: + - tank-0003 + ln: + lnd: true + lnd: + channels: + - id: + block: 300 + index: 3 + target: tank-0002-cln + capacity: 100005 + push_amt: 50002 \ No newline at end of file diff --git a/test/data/cln/node-defaults.yaml b/test/data/ln_mixed/node-defaults.yaml similarity index 60% rename from test/data/cln/node-defaults.yaml rename to test/data/ln_mixed/node-defaults.yaml index e059cf18b..df6d01dbb 100644 --- a/test/data/cln/node-defaults.yaml +++ b/test/data/ln_mixed/node-defaults.yaml @@ -1,12 +1,14 @@ -#Core configs image: repository: bitcoindevproject/bitcoin pullPolicy: IfNotPresent tag: "27.0" -#LN configs -ln: - cln: true +lnd: + defaultConfig: | + color=#000000 + config: | + bitcoin.timelockdelta=33 + cln: defaultConfig: | rgb=ff3155 diff --git a/test/ln_mixed_test.py b/test/ln_mixed_test.py new file mode 100755 index 000000000..43beb5e0d --- /dev/null +++ b/test/ln_mixed_test.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 + +import json +import os +from pathlib import Path +from time import sleep + +from test_base import TestBase + +from resources.scenarios.ln_framework.ln import CLN, LND, LNNode +from warnet.process import stream_command + + +class LNMultiTest(TestBase): + def __init__(self): + super().__init__() + self.network_dir = Path(os.path.dirname(__file__)) / "data" / "ln_mixed" + self.scen_dir = Path(os.path.dirname(__file__)).parent / "resources" / "scenarios" + self.lns = [ + CLN("tank-0001-cln", self.log), + CLN("tank-0002-cln", self.log), + LND("tank-0003-lnd", self.log), + LND("tank-0004-lnd", self.log), + LND("tank-0005-lnd", self.log), + ] + + def node(self, name: str) -> LNNode: + matching_nodes = [n for n in self.lns if n.name == name] + if not matching_nodes: + raise ValueError(f"No node found with name: {name}") + return matching_nodes[0] + + def run_test(self): + try: + # Wait for all nodes to wake up. ln_init will start automatically + self.setup_network() + + assert self.lns[0].walletbalance() > 0, ( + f"{self.lns[0]} has does not have a wallet balance" + ) + # Send a payment across channels opened automatically by ln_init + self.pay_invoice_rpc(sender="tank-0003-lnd", recipient="tank-0001-cln") + # self.pay_invoice_node(sender="tank-0001-cln", recipient="tank-0003-lnd") + + # Manually open more channels between first three nodes + # and send a payment using warnet RPC + self.manual_open_channels() + # FIXME: need to decide how to interact with LND via REST outside cluster + # self.wait_for_gossip_sync(self.lns, 5) + # self.pay_invoice(sender="tank-0004-lnd", recipient="tank-0002-cln") + + finally: + self.cleanup() + + def setup_network(self): + self.log.info("Setting up network") + stream_command(f"warnet deploy {self.network_dir}") + + def wait_for_txs(self, count): + self.wait_for_predicate( + lambda: json.loads(self.warnet("bitcoin rpc tank-0001 getmempoolinfo"))["size"] == count + ) + + def manual_open_channels(self): + # 1 -> 4 + pk1 = self.warnet("ln pubkey tank-0004-lnd") # prefer -> self.node("tank-0004-lnd").uri() + cln1_lnd4_channel = self.node("tank-0001-cln").channel(pk1, 444444, 200000, 5000) + assert "txid" in cln1_lnd4_channel, "Failed to create channel between CLN and LND" + print(cln1_lnd4_channel["txid"]) + + # 4 -> 2 + # lnd4_cln2_channel = self.node("tank-0004-lnd").channel( + # self.lns[1].uri(), 333333, 150000, 5000 + # ) + # assert "txid" in lnd4_cln2_channel, "Failed to create channel between LND and CLN" + # print(lnd4_cln2_channel["txid"]) + + self.wait_for_txs(1) + + self.warnet("bitcoin rpc tank-0001 -generate 10") + + def wait_for_gossip_sync(self, nodes, expected): + while len(nodes) > 0: + for node in nodes: + chs = node.graph()["edges"] + if len(chs) >= expected: + print(f"Too many edges for {node}") + sleep(1) + + def pay_invoice_rpc(self, sender: str, recipient: str): + print("pay invoice using ln rpc") + init_balance = self.node(recipient).channelbalance() + print("initial balance", init_balance) + # create cln invoice + inv = json.loads(self.warnet(f"ln rpc {recipient} invoice 1000000 label description")) + print(inv) + # pay from lightning + print(self.warnet(f"ln rpc {sender} payinvoice -f {inv['bolt11']}")) + + def wait_for_success(): + return self.node(recipient).channelbalance() == init_balance + 1000 + + self.wait_for_predicate(wait_for_success) + + # def pay_invoice_node(self, sender: str, recipient: str): + # print("pay invoice using ln framework") + # #FIXME: LND Node is not accessible outside the cluster + # init_balance = self.node(recipient).channelbalance() + # print("initial balance", init_balance) + # # create invoice + # inv = self.node(recipient).createinvoice(1000, "label2") + # print(inv) + # # pay invoie + # print(self.node(sender).payinvoice(inv)) + + # def wait_for_success(): + # print(self.node(recipient).channelbalance()) + # return self.node(recipient).channelbalance() == init_balance + 1000 + + # self.wait_for_predicate(wait_for_success) + + +if __name__ == "__main__": + test = LNMultiTest() + test.run_test() From f1a8b373d357a5605f93df3185c41b6f454ba601 Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Wed, 23 Apr 2025 10:11:07 -0400 Subject: [PATCH 15/24] switch from print to logger --- test/ln_mixed_test.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/test/ln_mixed_test.py b/test/ln_mixed_test.py index 43beb5e0d..31889da6e 100755 --- a/test/ln_mixed_test.py +++ b/test/ln_mixed_test.py @@ -64,16 +64,16 @@ def wait_for_txs(self, count): def manual_open_channels(self): # 1 -> 4 pk1 = self.warnet("ln pubkey tank-0004-lnd") # prefer -> self.node("tank-0004-lnd").uri() - cln1_lnd4_channel = self.node("tank-0001-cln").channel(pk1, 444444, 200000, 5000) - assert "txid" in cln1_lnd4_channel, "Failed to create channel between CLN and LND" - print(cln1_lnd4_channel["txid"]) + channel = self.node("tank-0001-cln").channel(pk1, 444444, 200000, 5000) + assert "txid" in channel, "Failed to create channel between CLN and LND" + self.log.info(f'Channel txid {channel["txid"]}') # 4 -> 2 - # lnd4_cln2_channel = self.node("tank-0004-lnd").channel( + # channel = self.node("tank-0004-lnd").channel( # self.lns[1].uri(), 333333, 150000, 5000 # ) - # assert "txid" in lnd4_cln2_channel, "Failed to create channel between LND and CLN" - # print(lnd4_cln2_channel["txid"]) + # assert "txid" in channel, "Failed to create channel between LND and CLN" + # self.log.info(f'Channel txid {channel["txid"]}') self.wait_for_txs(1) @@ -84,18 +84,18 @@ def wait_for_gossip_sync(self, nodes, expected): for node in nodes: chs = node.graph()["edges"] if len(chs) >= expected: - print(f"Too many edges for {node}") + self.log.info(f"Too many edges for {node}") sleep(1) def pay_invoice_rpc(self, sender: str, recipient: str): - print("pay invoice using ln rpc") + self.log.info("pay invoice using ln rpc") init_balance = self.node(recipient).channelbalance() - print("initial balance", init_balance) + self.log.info(f"initial balance {init_balance}") # create cln invoice inv = json.loads(self.warnet(f"ln rpc {recipient} invoice 1000000 label description")) - print(inv) + self.log.info(inv) # pay from lightning - print(self.warnet(f"ln rpc {sender} payinvoice -f {inv['bolt11']}")) + self.log.info(self.warnet(f"ln rpc {sender} payinvoice -f {inv['bolt11']}")) def wait_for_success(): return self.node(recipient).channelbalance() == init_balance + 1000 From 6355e853f33f1429e47e9bfe2506bd64d0768f9e Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Wed, 23 Apr 2025 10:48:14 -0400 Subject: [PATCH 16/24] correct formatting ruff finding --- test/ln_mixed_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ln_mixed_test.py b/test/ln_mixed_test.py index 31889da6e..efba7f027 100755 --- a/test/ln_mixed_test.py +++ b/test/ln_mixed_test.py @@ -66,7 +66,7 @@ def manual_open_channels(self): pk1 = self.warnet("ln pubkey tank-0004-lnd") # prefer -> self.node("tank-0004-lnd").uri() channel = self.node("tank-0001-cln").channel(pk1, 444444, 200000, 5000) assert "txid" in channel, "Failed to create channel between CLN and LND" - self.log.info(f'Channel txid {channel["txid"]}') + self.log.info(f"Channel txid {channel['txid']}") # 4 -> 2 # channel = self.node("tank-0004-lnd").channel( From f1d7b397bc02fb9152979d257eb33f47ca8031dd Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Wed, 23 Apr 2025 16:19:49 -0400 Subject: [PATCH 17/24] revert -lnd changes based on comments reverse LN_10.json sequence change --- docs/plugins.md | 2 +- resources/plugins/hello/README.md | 6 +++--- resources/scenarios/commander.py | 12 ++++++------ resources/scenarios/ln_framework/ln.py | 15 +++++++++------ src/warnet/graph.py | 2 +- test/data/LN_10.json | 4 ++-- test/data/ln/network.yaml | 4 ++-- test/data/ln_mixed/network.yaml | 10 +++++----- test/ln_mixed_test.py | 22 +++++++++++----------- test/ln_test.py | 6 +++--- 10 files changed, 43 insertions(+), 40 deletions(-) diff --git a/docs/plugins.md b/docs/plugins.md index 7d4449076..bce833864 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -34,7 +34,7 @@ plugins: postDeploy: # Plugins will run after all the `deploy` code has run. simln: entrypoint: "../plugins/simln" - activity: '[{"source": "tank-0003-lnd", "destination": "tank-0005-cln", "interval_secs": 1, "amount_msat": 2000}]' + activity: '[{"source": "tank-0003-ln", "destination": "tank-0005-ln", "interval_secs": 1, "amount_msat": 2000}]' hello: entrypoint: "../plugins/hello" podName: "hello-post-deploy" diff --git a/resources/plugins/hello/README.md b/resources/plugins/hello/README.md index c608de997..77bb5040f 100644 --- a/resources/plugins/hello/README.md +++ b/resources/plugins/hello/README.md @@ -62,7 +62,7 @@ nodes: - id: block: 300 index: 1 - target: tank-0004-lnd + target: tank-0004-ln capacity: 100000 push_amt: 50000 @@ -76,7 +76,7 @@ nodes: - id: block: 300 index: 2 - target: tank-0005-lnd + target: tank-0005-ln capacity: 50000 push_amt: 25000 @@ -99,7 +99,7 @@ plugins: # Each plugin section has a number of hooks available (preDeploy, post helloTo: "postDeploy!" simln: # You can have multiple plugins per hook entrypoint: "../../plugins/simln" - activity: '[{"source": "tank-0003-lnd", "destination": "tank-0005-lnd", "interval_secs": 1, "amount_msat": 2000}]' + activity: '[{"source": "tank-0003-ln", "destination": "tank-0005-ln", "interval_secs": 1, "amount_msat": 2000}]' preNode: # preNode plugins run before each node is deployed hello: entrypoint: "../../plugins/hello" diff --git a/resources/scenarios/commander.py b/resources/scenarios/commander.py index c3cd1655f..8c1eee4c0 100644 --- a/resources/scenarios/commander.py +++ b/resources/scenarios/commander.py @@ -70,7 +70,10 @@ ) if pod.metadata.labels["mission"] == "lightning": - WARNET["lightning"].append(pod.metadata.name) + lnnode = LND(pod.metadata.name) + if "cln" in pod.metadata.labels["app.kubernetes.io/name"]: + lnnode = CLN(pod.metadata.name) + WARNET["lightning"].append(lnnode) for cm in cmaps.items: if not cm.metadata.labels or "channels" not in cm.metadata.labels: @@ -225,11 +228,8 @@ def setup(self): self.tanks[tank["tank"]] = node for ln in WARNET["lightning"]: - # create the correct implementation based on pod name - if "-cln" in ln: - self.lns[ln] = CLN(ln, self.log) - else: - self.lns[ln] = LND(ln, self.log) + ln.setLogger(self.log) + self.lns[ln.name] = ln self.num_nodes = len(self.nodes) diff --git a/resources/scenarios/ln_framework/ln.py b/resources/scenarios/ln_framework/ln.py index dbe8506c4..74c79196b 100644 --- a/resources/scenarios/ln_framework/ln.py +++ b/resources/scenarios/ln_framework/ln.py @@ -112,10 +112,13 @@ def to_lnd_chanpolicy(self, capacity): class LNNode(ABC): @abstractmethod - def __init__(self, pod_name, logger): - self.log = logger + def __init__(self, pod_name): + self.log = None self.name = pod_name + def setLogger(self, logger): + self.log = logger + @staticmethod def param_dict_to_list(params: dict) -> list[str]: return [f"{k}={v}" for k, v in params.items()] @@ -161,8 +164,8 @@ def update(self, txid_hex: str, policy: dict, capacity: int) -> dict: class CLN(LNNode): - def __init__(self, pod_name, logger): - super().__init__(pod_name, logger) + def __init__(self, pod_name): + super().__init__(pod_name) self.headers = {} self.impl = "cln" @@ -326,8 +329,8 @@ def update(self, txid_hex: str, policy: dict, capacity: int, max_tries=2) -> dic class LND(LNNode): - def __init__(self, pod_name, logger): - super().__init__(pod_name, logger) + def __init__(self, pod_name): + super().__init__(pod_name) self.conn = http.client.HTTPSConnection( host=pod_name, port=8080, timeout=5, context=INSECURE_CONTEXT ) diff --git a/src/warnet/graph.py b/src/warnet/graph.py index d2fc99421..cff7caa15 100644 --- a/src/warnet/graph.py +++ b/src/warnet/graph.py @@ -269,7 +269,7 @@ def _import_network(graph_file_path, output_path): source = pk_to_tank[edge["node1_pub"]] channel = { "id": {"block": block, "index": index}, - "target": pk_to_tank[edge["node2_pub"]] + "-lnd", + "target": pk_to_tank[edge["node2_pub"]] + "-ln", "capacity": int(edge["capacity"]), "push_amt": int(edge["capacity"]) // 2, "source_policy": Policy.from_lnd_describegraph(edge["node1_policy"]).to_dict(), diff --git a/test/data/LN_10.json b/test/data/LN_10.json index 72be3a29e..c9e71adb0 100644 --- a/test/data/LN_10.json +++ b/test/data/LN_10.json @@ -987,7 +987,7 @@ { "node1_pub": "02b705ad9fff5b30e69dd3810d100372332c1a750db5a50edf7353c77ab486643e", "node2_pub": "037659a0ac8eb3b8d0a720114efc861d3a940382dcfa1403746b4f8f6b2e8810ba", - "channel_id": "908655100992880641", + "channel_id": "891818279431438336", "chan_point": "316bc4974bc4382554b80ba30eaaecf18c3f45f03fed41f99e9600d1c5dd3029:0", "last_update": 1710800025, "capacity": "2000000", @@ -1016,7 +1016,7 @@ { "node1_pub": "02b705ad9fff5b30e69dd3810d100372332c1a750db5a50edf7353c77ab486643e", "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", - "channel_id": "891818279431438336", + "channel_id": "908655100992880641", "chan_point": "0acfd2470db4eb06268b3f22b4c45c0fdcc11d10b6519577662e54823a0f264e:1", "last_update": 1710841427, "capacity": "10000000", diff --git a/test/data/ln/network.yaml b/test/data/ln/network.yaml index a5ab4c36f..5d30686d4 100644 --- a/test/data/ln/network.yaml +++ b/test/data/ln/network.yaml @@ -18,7 +18,7 @@ nodes: - id: block: 300 index: 1 - target: tank-0004-lnd + target: tank-0004-ln capacity: 100000 push_amt: 50000 - name: tank-0004 @@ -29,7 +29,7 @@ nodes: - id: block: 300 index: 2 - target: tank-0005-lnd + target: tank-0005-ln capacity: 50000 push_amt: 25000 - name: tank-0005 diff --git a/test/data/ln_mixed/network.yaml b/test/data/ln_mixed/network.yaml index 2afced756..56a2938d1 100644 --- a/test/data/ln_mixed/network.yaml +++ b/test/data/ln_mixed/network.yaml @@ -9,7 +9,7 @@ nodes: - id: block: 300 index: 4 - target: tank-0003-lnd + target: tank-0003-ln capacity: 50001 push_amt: 25003 - name: tank-0002 @@ -22,7 +22,7 @@ nodes: - id: block: 300 index: 5 - target: tank-0001-cln + target: tank-0001-ln capacity: 50002 push_amt: 25001 - name: tank-0003 @@ -35,7 +35,7 @@ nodes: - id: block: 300 index: 1 - target: tank-0004-lnd + target: tank-0004-ln capacity: 100003 push_amt: 50004 - name: tank-0004 @@ -48,7 +48,7 @@ nodes: - id: block: 300 index: 2 - target: tank-0005-lnd + target: tank-0005-ln capacity: 50004 push_amt: 25005 - name: tank-0005 @@ -61,6 +61,6 @@ nodes: - id: block: 300 index: 3 - target: tank-0002-cln + target: tank-0002-ln capacity: 100005 push_amt: 50002 \ No newline at end of file diff --git a/test/ln_mixed_test.py b/test/ln_mixed_test.py index efba7f027..c3e20c6a9 100755 --- a/test/ln_mixed_test.py +++ b/test/ln_mixed_test.py @@ -17,11 +17,11 @@ def __init__(self): self.network_dir = Path(os.path.dirname(__file__)) / "data" / "ln_mixed" self.scen_dir = Path(os.path.dirname(__file__)).parent / "resources" / "scenarios" self.lns = [ - CLN("tank-0001-cln", self.log), - CLN("tank-0002-cln", self.log), - LND("tank-0003-lnd", self.log), - LND("tank-0004-lnd", self.log), - LND("tank-0005-lnd", self.log), + CLN("tank-0001-ln"), + CLN("tank-0002-ln"), + LND("tank-0003-ln"), + LND("tank-0004-ln"), + LND("tank-0005-ln"), ] def node(self, name: str) -> LNNode: @@ -39,15 +39,15 @@ def run_test(self): f"{self.lns[0]} has does not have a wallet balance" ) # Send a payment across channels opened automatically by ln_init - self.pay_invoice_rpc(sender="tank-0003-lnd", recipient="tank-0001-cln") - # self.pay_invoice_node(sender="tank-0001-cln", recipient="tank-0003-lnd") + self.pay_invoice_rpc(sender="tank-0003-ln", recipient="tank-0001-ln") + # self.pay_invoice_node(sender="tank-0001-ln", recipient="tank-0003-ln") # Manually open more channels between first three nodes # and send a payment using warnet RPC self.manual_open_channels() # FIXME: need to decide how to interact with LND via REST outside cluster # self.wait_for_gossip_sync(self.lns, 5) - # self.pay_invoice(sender="tank-0004-lnd", recipient="tank-0002-cln") + # self.pay_invoice(sender="tank-0004-ln", recipient="tank-0002-ln") finally: self.cleanup() @@ -63,13 +63,13 @@ def wait_for_txs(self, count): def manual_open_channels(self): # 1 -> 4 - pk1 = self.warnet("ln pubkey tank-0004-lnd") # prefer -> self.node("tank-0004-lnd").uri() - channel = self.node("tank-0001-cln").channel(pk1, 444444, 200000, 5000) + pk1 = self.warnet("ln pubkey tank-0004-ln") # prefer -> self.node("tank-0004-ln").uri() + channel = self.node("tank-0001-ln").channel(pk1, 444444, 200000, 5000) assert "txid" in channel, "Failed to create channel between CLN and LND" self.log.info(f"Channel txid {channel['txid']}") # 4 -> 2 - # channel = self.node("tank-0004-lnd").channel( + # channel = self.node("tank-0004-ln").channel( # self.lns[1].uri(), 333333, 150000, 5000 # ) # assert "txid" in channel, "Failed to create channel between LND and CLN" diff --git a/test/ln_test.py b/test/ln_test.py index f13fc37fe..ee27b6256 100755 --- a/test/ln_test.py +++ b/test/ln_test.py @@ -43,7 +43,7 @@ def test_channel_policies(self): self.log.info("Ensuring node-level channel policy settings") graphs = [] for n in range(10): - ln = f"tank-{n:04d}-lnd" + ln = f"tank-{n:04d}-ln" res = self.warnet(f"ln rpc {ln} describegraph") graphs.append(json.loads(res)["edges"]) @@ -73,8 +73,8 @@ def check_policy(node: int, index: int, field: str, values: tuple): def test_payments(self): def get_and_pay(src, tgt): - src = f"tank-{src:04d}-lnd" - tgt = f"tank-{tgt:04d}-lnd" + src = f"tank-{src:04d}-ln" + tgt = f"tank-{tgt:04d}-ln" invoice = json.loads(self.warnet(f"ln rpc {tgt} addinvoice --amt 230118"))[ "payment_request" ] From 10531d5a8bacc247845403a3f756122f96a9d44b Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Wed, 23 Apr 2025 17:05:32 -0400 Subject: [PATCH 18/24] Remove all dependences on -cln or -lnd name to determine type - use pod metadata --- .../charts/cln/templates/_helpers.tpl | 7 +---- .../charts/lnd/templates/_helpers.tpl | 4 +-- resources/networks/hello/network.yaml | 6 ++-- resources/plugins/simln/README.md | 18 ++++++------ resources/plugins/simln/plugin.py | 4 +-- resources/scenarios/ln_framework/ln.py | 12 ++++---- src/warnet/ln.py | 16 ++++++----- test/ln_basic_test.py | 28 +++++++++---------- test/ln_mixed_test.py | 10 +++---- 9 files changed, 51 insertions(+), 54 deletions(-) diff --git a/resources/charts/bitcoincore/charts/cln/templates/_helpers.tpl b/resources/charts/bitcoincore/charts/cln/templates/_helpers.tpl index de02b029f..3c243f897 100644 --- a/resources/charts/bitcoincore/charts/cln/templates/_helpers.tpl +++ b/resources/charts/bitcoincore/charts/cln/templates/_helpers.tpl @@ -35,12 +35,7 @@ If release name contains chart name it will be used as a full name. {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} -{{- $name := default .Chart.Name .Values.nameOverride }} -{{- if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} +{{- printf "%s-ln" .Release.Name | trunc 63 | trimSuffix "-" }} {{- end }} {{- end }} diff --git a/resources/charts/bitcoincore/charts/lnd/templates/_helpers.tpl b/resources/charts/bitcoincore/charts/lnd/templates/_helpers.tpl index e3451356c..8af486025 100644 --- a/resources/charts/bitcoincore/charts/lnd/templates/_helpers.tpl +++ b/resources/charts/bitcoincore/charts/lnd/templates/_helpers.tpl @@ -23,7 +23,7 @@ If release name contains chart name it will be used as a full name. Expand the name of the chart. */}} {{- define "lnd.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}-lnd +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} {{/* @@ -35,7 +35,7 @@ If release name contains chart name it will be used as a full name. {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} -{{- printf "%s" .Release.Name | trunc 63 | trimSuffix "-" }}-lnd +{{- printf "%s-ln" .Release.Name | trunc 63 | trimSuffix "-" }} {{- end }} {{- end }} diff --git a/resources/networks/hello/network.yaml b/resources/networks/hello/network.yaml index 341e4d761..f5acf0a83 100644 --- a/resources/networks/hello/network.yaml +++ b/resources/networks/hello/network.yaml @@ -29,7 +29,7 @@ nodes: - id: block: 300 index: 1 - target: tank-0004-lnd + target: tank-0004-ln capacity: 100000 push_amt: 50000 @@ -43,7 +43,7 @@ nodes: - id: block: 300 index: 2 - target: tank-0005-lnd + target: tank-0005-ln capacity: 50000 push_amt: 25000 @@ -66,7 +66,7 @@ plugins: # Each plugin section has a number of hooks available (preDeploy, post helloTo: "postDeploy!" simln: # You can have multiple plugins per hook entrypoint: "../../plugins/simln" - activity: '[{"source": "tank-0003-lnd", "destination": "tank-0005-lnd", "interval_secs": 1, "amount_msat": 2000}]' + activity: '[{"source": "tank-0003-ln", "destination": "tank-0005-ln", "interval_secs": 1, "amount_msat": 2000}]' preNode: # preNode plugins run before each node is deployed hello: entrypoint: "../../plugins/hello" diff --git a/resources/plugins/simln/README.md b/resources/plugins/simln/README.md index 2593d7665..f1acb1d94 100644 --- a/resources/plugins/simln/README.md +++ b/resources/plugins/simln/README.md @@ -13,7 +13,7 @@ SimLN also requires access details for each node; however, the SimLN plugin will ```` JSON { - "id": -lnd, + "id": , "address": https://, "macaroon": , "cert": @@ -22,11 +22,11 @@ SimLN also requires access details for each node; however, the SimLN plugin will SimLN also supports Core Lightning (CLN). CLN nodes connection details are transfered from the CLN node to SimLN node during launch-activity processing. ```` JSON { - "id": -cln, + "id": , "address": https://, - "ca_cert": /working/-cln-ca.pem, - "client_cert": /working/-cln-client.pem, - "client_key": /working/-cln-client-key.pem + "ca_cert": /working/-ca.pem, + "client_cert": /working/-client.pem, + "client_key": /working/-client-key.pem } ```` @@ -35,7 +35,7 @@ Since SimLN already has access to those LND and CLN connection details, it means ### Launch activity definitions from the command line The SimLN plugin takes "activity" definitions like so: -`./simln/plugin.py launch-activity '[{\"source\": \"tank-0003-lnd\", \"destination\": \"tank-0005-lnd\", \"interval_secs\": 1, \"amount_msat\": 2000}]'"''` +`./simln/plugin.py launch-activity '[{\"source\": \"tank-0003-ln\", \"destination\": \"tank-0005-ln\", \"interval_secs\": 1, \"amount_msat\": 2000}]'"''` ### Launch activity definitions from within `network.yaml` When you initialize a new Warnet network, Warnet will create a new `network.yaml` file. If your `network.yaml` file includes lightning nodes, then you can use SimLN to produce activity between those nodes like this: @@ -75,7 +75,7 @@ nodes: - id: block: 300 index: 1 - target: tank-0004-lnd + target: tank-0004-ln capacity: 100000 push_amt: 50000 @@ -89,7 +89,7 @@ nodes: - id: block: 300 index: 2 - target: tank-0005-lnd + target: tank-0005-ln capacity: 50000 push_amt: 25000 @@ -103,7 +103,7 @@ plugins: postDeploy: simln: entrypoint: "../../plugins/simln" # This is the path to the simln plugin folder (relative to the network.yaml file). - activity: '[{"source": "tank-0003-lnd", "destination": "tank-0005-lnd", "interval_secs": 1, "amount_msat": 2000}]' + activity: '[{"source": "tank-0003-ln", "destination": "tank-0005-ln", "interval_secs": 1, "amount_msat": 2000}]' ```` diff --git a/resources/plugins/simln/plugin.py b/resources/plugins/simln/plugin.py index f773e60df..0f11df94b 100755 --- a/resources/plugins/simln/plugin.py +++ b/resources/plugins/simln/plugin.py @@ -168,7 +168,7 @@ def _generate_activity_json(activity: Optional[list[dict]]) -> str: ln_name = i.metadata.name port = 10009 node = {"id": ln_name} - if "-cln" in ln_name: + if "cln" in i.metadata.labels["app.kubernetes.io/name"]: port = 9736 node["ca_cert"] = f"/working/{ln_name}-ca.pem" node["client_cert"] = f"/working/{ln_name}-client.pem" @@ -192,7 +192,7 @@ def transfer_cln_certs(name): cln_root = "/root/.lightning/regtest" for i in get_mission(LIGHTNING_MISSION): ln_name = i.metadata.name - if "cln" in ln_name: + if "cln" in i.metadata.labels["app.kubernetes.io/name"]: copyfile( ln_name, "cln", diff --git a/resources/scenarios/ln_framework/ln.py b/resources/scenarios/ln_framework/ln.py index 74c79196b..1e29521d9 100644 --- a/resources/scenarios/ln_framework/ln.py +++ b/resources/scenarios/ln_framework/ln.py @@ -112,8 +112,8 @@ def to_lnd_chanpolicy(self, capacity): class LNNode(ABC): @abstractmethod - def __init__(self, pod_name): - self.log = None + def __init__(self, pod_name, logger=None): + self.log = logger self.name = pod_name def setLogger(self, logger): @@ -164,8 +164,8 @@ def update(self, txid_hex: str, policy: dict, capacity: int) -> dict: class CLN(LNNode): - def __init__(self, pod_name): - super().__init__(pod_name) + def __init__(self, pod_name, logger=None): + super().__init__(pod_name, logger) self.headers = {} self.impl = "cln" @@ -329,8 +329,8 @@ def update(self, txid_hex: str, policy: dict, capacity: int, max_tries=2) -> dic class LND(LNNode): - def __init__(self, pod_name): - super().__init__(pod_name) + def __init__(self, pod_name, logger=None): + super().__init__(pod_name, logger) self.conn = http.client.HTTPSConnection( host=pod_name, port=8080, timeout=5, context=INSECURE_CONTEXT ) diff --git a/src/warnet/ln.py b/src/warnet/ln.py index d45c0a03b..d3691bd4d 100644 --- a/src/warnet/ln.py +++ b/src/warnet/ln.py @@ -32,7 +32,7 @@ def _rpc(pod_name: str, method: str, params: str = "", namespace: Optional[str] namespace = get_default_namespace_or(namespace) chain = pod.metadata.labels["chain"] ln_client = "lncli" - if "-cln" in pod_name: + if "cln" in pod.metadata.labels["app.kubernetes.io/name"]: ln_client = "lightning-cli" cmd = f"kubectl -n {namespace} exec {pod_name} -- {ln_client} --network {chain} {method} {' '.join(map(str, params))}" return run_command(cmd) @@ -49,10 +49,11 @@ def pubkey( print(_pubkey(pod)) -def _pubkey(pod: str): - info = _rpc(pod, "getinfo") +def _pubkey(pod_name: str): + info = _rpc(pod_name, "getinfo") + pod = get_pod(pod_name) pubkey_key = "identity_pubkey" - if "-cln" in pod: + if "cln" in pod.metadata.labels["app.kubernetes.io/name"]: pubkey_key = "id" return json.loads(info)[pubkey_key] @@ -68,9 +69,10 @@ def host( print(_host(pod)) -def _host(pod): - info = _rpc(pod, "getinfo") - if "-cln" in pod: +def _host(pod_name: str): + info = _rpc(pod_name, "getinfo") + pod = get_pod(pod_name) + if "cln" in pod.metadata.labels["app.kubernetes.io/name"]: return json.loads(info)["alias"] else: uris = json.loads(info)["uris"] diff --git a/test/ln_basic_test.py b/test/ln_basic_test.py index e12223553..dbbbb767a 100755 --- a/test/ln_basic_test.py +++ b/test/ln_basic_test.py @@ -16,12 +16,12 @@ def __init__(self): self.network_dir = Path(os.path.dirname(__file__)) / "data" / "ln" self.scen_dir = Path(os.path.dirname(__file__)).parent / "resources" / "scenarios" self.lns = [ - "tank-0000-lnd", - "tank-0001-lnd", - "tank-0002-lnd", - "tank-0003-lnd", - "tank-0004-lnd", - "tank-0005-lnd", + "tank-0000-ln", + "tank-0001-ln", + "tank-0002-ln", + "tank-0003-ln", + "tank-0004-ln", + "tank-0005-ln", ] def run_test(self): @@ -30,13 +30,13 @@ def run_test(self): self.setup_network() # Send a payment across channels opened automatically by ln_init - self.pay_invoice(sender="tank-0005-lnd", recipient="tank-0003-lnd") + self.pay_invoice(sender="tank-0005-ln", recipient="tank-0003-ln") # Manually open two more channels between first three nodes # and send a payment using warnet RPC self.manual_open_channels() self.wait_for_gossip_sync(self.lns[:3], 2 + 2) - self.pay_invoice(sender="tank-0000-lnd", recipient="tank-0002-lnd") + self.pay_invoice(sender="tank-0000-ln", recipient="tank-0002-ln") finally: self.cleanup() @@ -63,27 +63,27 @@ def wait_for_two_txs(self): def manual_open_channels(self): # 0 -> 1 -> 2 - pk1 = self.warnet("ln pubkey tank-0001-lnd") - pk2 = self.warnet("ln pubkey tank-0002-lnd") + pk1 = self.warnet("ln pubkey tank-0001-ln") + pk2 = self.warnet("ln pubkey tank-0002-ln") host1 = "" host2 = "" while not host1 or not host2: if not host1: - host1 = self.warnet("ln host tank-0001-lnd") + host1 = self.warnet("ln host tank-0001-ln") if not host2: - host2 = self.warnet("ln host tank-0002-lnd") + host2 = self.warnet("ln host tank-0002-ln") sleep(1) print( self.warnet( - f"ln rpc tank-0000-lnd openchannel --node_key {pk1} --local_amt 100000 --connect {host1}" + f"ln rpc tank-0000-ln openchannel --node_key {pk1} --local_amt 100000 --connect {host1}" ) ) print( self.warnet( - f"ln rpc tank-0001-lnd openchannel --node_key {pk2} --local_amt 100000 --connect {host2}" + f"ln rpc tank-0001-ln openchannel --node_key {pk2} --local_amt 100000 --connect {host2}" ) ) diff --git a/test/ln_mixed_test.py b/test/ln_mixed_test.py index c3e20c6a9..f751c7e8a 100755 --- a/test/ln_mixed_test.py +++ b/test/ln_mixed_test.py @@ -17,11 +17,11 @@ def __init__(self): self.network_dir = Path(os.path.dirname(__file__)) / "data" / "ln_mixed" self.scen_dir = Path(os.path.dirname(__file__)).parent / "resources" / "scenarios" self.lns = [ - CLN("tank-0001-ln"), - CLN("tank-0002-ln"), - LND("tank-0003-ln"), - LND("tank-0004-ln"), - LND("tank-0005-ln"), + CLN("tank-0001-ln", self.log), + CLN("tank-0002-ln", self.log), + LND("tank-0003-ln", self.log), + LND("tank-0004-ln", self.log), + LND("tank-0005-ln", self.log), ] def node(self, name: str) -> LNNode: From 3d846d3bdec530e8339b9e1c23b674a8e882a7b8 Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Thu, 24 Apr 2025 20:34:06 -0400 Subject: [PATCH 19/24] address comments reduce ln_init changes start migration of CLN to REST --- .github/workflows/test.yml | 1 - .../charts/cln/templates/_helpers.tpl | 4 +- .../charts/cln/templates/configmap.yaml | 2 + .../bitcoincore/charts/cln/templates/pod.yaml | 3 + .../charts/cln/templates/service.yaml | 4 + .../charts/bitcoincore/charts/cln/values.yaml | 1 + .../charts/lnd/templates/_helpers.tpl | 4 +- .../plugins/simln/charts/simln/values.yaml | 2 - resources/scenarios/commander.py | 1 - resources/scenarios/ln_framework/ln.py | 120 ++++++++++++++--- resources/scenarios/ln_init.py | 49 +++---- test/ln_mixed_test.py | 125 ------------------ 12 files changed, 131 insertions(+), 185 deletions(-) delete mode 100755 test/ln_mixed_test.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 553b899e1..c6cbded0e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,7 +41,6 @@ jobs: - graph_test.py - logging_test.py - ln_basic_test.py - - ln_mixed_test.py - ln_test.py - rpc_test.py - services_test.py diff --git a/resources/charts/bitcoincore/charts/cln/templates/_helpers.tpl b/resources/charts/bitcoincore/charts/cln/templates/_helpers.tpl index 3c243f897..d983355cf 100644 --- a/resources/charts/bitcoincore/charts/cln/templates/_helpers.tpl +++ b/resources/charts/bitcoincore/charts/cln/templates/_helpers.tpl @@ -23,7 +23,7 @@ If release name contains chart name it will be used as a full name. Expand the name of the chart. */}} {{- define "cln.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}-ln {{- end }} {{/* @@ -35,7 +35,7 @@ If release name contains chart name it will be used as a full name. {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} -{{- printf "%s-ln" .Release.Name | trunc 63 | trimSuffix "-" }} +{{- printf "%s" .Release.Name | trunc 63 | trimSuffix "-" }}-ln {{- end }} {{- end }} diff --git a/resources/charts/bitcoincore/charts/cln/templates/configmap.yaml b/resources/charts/bitcoincore/charts/cln/templates/configmap.yaml index 3fe68f61c..c69ac3c5d 100644 --- a/resources/charts/bitcoincore/charts/cln/templates/configmap.yaml +++ b/resources/charts/bitcoincore/charts/cln/templates/configmap.yaml @@ -20,6 +20,8 @@ data: bitcoin-retry-timeout=600 grpc-port={{ .Values.RPCPort }} grpc-host=0.0.0.0 + clnrest-host=0.0.0.0 + clnrest-port=3010 --- apiVersion: v1 kind: ConfigMap diff --git a/resources/charts/bitcoincore/charts/cln/templates/pod.yaml b/resources/charts/bitcoincore/charts/cln/templates/pod.yaml index 255be8f22..db43e4382 100644 --- a/resources/charts/bitcoincore/charts/cln/templates/pod.yaml +++ b/resources/charts/bitcoincore/charts/cln/templates/pod.yaml @@ -34,6 +34,9 @@ spec: - name: rpc containerPort: {{ .Values.RPCPort }} protocol: TCP + - name: rest + containerPort: {{ .Values.RestPort }} + protocol: TCP command: - /bin/sh - -c diff --git a/resources/charts/bitcoincore/charts/cln/templates/service.yaml b/resources/charts/bitcoincore/charts/cln/templates/service.yaml index 3adc03dc5..565f50182 100644 --- a/resources/charts/bitcoincore/charts/cln/templates/service.yaml +++ b/resources/charts/bitcoincore/charts/cln/templates/service.yaml @@ -16,5 +16,9 @@ spec: targetPort: rpc protocol: TCP name: rpc + - port: {{ .Values.RestPort }} + targetPort: rest + protocol: TCP + name: rest selector: {{- include "cln.selectorLabels" . | nindent 4 }} diff --git a/resources/charts/bitcoincore/charts/cln/values.yaml b/resources/charts/bitcoincore/charts/cln/values.yaml index 714fc0459..8340a9e19 100644 --- a/resources/charts/bitcoincore/charts/cln/values.yaml +++ b/resources/charts/bitcoincore/charts/cln/values.yaml @@ -33,6 +33,7 @@ service: P2PPort: 9735 RPCPort: 9736 +RestPort: 3010 # This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ ingress: diff --git a/resources/charts/bitcoincore/charts/lnd/templates/_helpers.tpl b/resources/charts/bitcoincore/charts/lnd/templates/_helpers.tpl index 8af486025..de7c0c156 100644 --- a/resources/charts/bitcoincore/charts/lnd/templates/_helpers.tpl +++ b/resources/charts/bitcoincore/charts/lnd/templates/_helpers.tpl @@ -23,7 +23,7 @@ If release name contains chart name it will be used as a full name. Expand the name of the chart. */}} {{- define "lnd.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}-ln {{- end }} {{/* @@ -35,7 +35,7 @@ If release name contains chart name it will be used as a full name. {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} -{{- printf "%s-ln" .Release.Name | trunc 63 | trimSuffix "-" }} +{{- printf "%s" .Release.Name | trunc 63 | trimSuffix "-" }}-ln {{- end }} {{- end }} diff --git a/resources/plugins/simln/charts/simln/values.yaml b/resources/plugins/simln/charts/simln/values.yaml index bea0096df..a1647a963 100644 --- a/resources/plugins/simln/charts/simln/values.yaml +++ b/resources/plugins/simln/charts/simln/values.yaml @@ -4,8 +4,6 @@ image: tag: "0.2.3" pullPolicy: IfNotPresent -restartPolicy: Never - workingVolume: name: working-volume mountPath: /working diff --git a/resources/scenarios/commander.py b/resources/scenarios/commander.py index 8c1eee4c0..665d9bd6a 100644 --- a/resources/scenarios/commander.py +++ b/resources/scenarios/commander.py @@ -228,7 +228,6 @@ def setup(self): self.tanks[tank["tank"]] = node for ln in WARNET["lightning"]: - ln.setLogger(self.log) self.lns[ln.name] = ln self.num_nodes = len(self.nodes) diff --git a/resources/scenarios/ln_framework/ln.py b/resources/scenarios/ln_framework/ln.py index 1e29521d9..518f48291 100644 --- a/resources/scenarios/ln_framework/ln.py +++ b/resources/scenarios/ln_framework/ln.py @@ -1,6 +1,7 @@ import base64 import http.client import json +import logging import os import ssl from abc import ABC, abstractmethod @@ -112,12 +113,14 @@ def to_lnd_chanpolicy(self, capacity): class LNNode(ABC): @abstractmethod - def __init__(self, pod_name, logger=None): - self.log = logger + def __init__(self, pod_name): self.name = pod_name - - def setLogger(self, logger): - self.log = logger + self.log = logging.getLogger(pod_name) + handler = logging.StreamHandler() + formatter = logging.Formatter(f'%(name)-8s - %(levelname)s: %(message)s') + handler.setFormatter(formatter) + self.log.addHandler(handler) + self.log.setLevel(logging.INFO) @staticmethod def param_dict_to_list(params: dict) -> list[str]: @@ -164,11 +167,78 @@ def update(self, txid_hex: str, policy: dict, capacity: int) -> dict: class CLN(LNNode): - def __init__(self, pod_name, logger=None): - super().__init__(pod_name, logger) + def __init__(self, pod_name): + super().__init__(pod_name) + self.conn = http.client.HTTPSConnection( + host=pod_name, port=8080, timeout=5, context=INSECURE_CONTEXT + ) self.headers = {} self.impl = "cln" + def reset_connection(self): + self.conn = http.client.HTTPSConnection( + host=self.name, port=3010, timeout=5, context=INSECURE_CONTEXT + ) + + def setRune(self, rune): + self.headers = { + "Rune": rune + } + + 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 CLN GET, Abort: {e}") + return None + sleep(1) + + def post(self, uri, data={}): + body = json.dumps(data) + post_header = self.headers + post_header["Content-Length"] = str(len(body)) + post_header["Content-Type"] = "application/json" + 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 CLN POST, Abort: {e}") + return None + sleep(1) + def rpc( self, method: str, @@ -191,12 +261,26 @@ def rpc( self.log.error(f"CLN rpc error: {e}, wait and retry...") sleep(2) return None + + def createrune(self, max_tries=2): + attempt = 0 + while attempt < max_tries: + attempt += 1 + response = self.rpc("createrune") + if not response: + sleep(2) + continue + res = json.loads(response) + self.setRune(res["rune"]) + return + raise Exception(f"Unable to fetch rune from {self.name}") def newaddress(self, max_tries=2): + self.createrune() attempt = 0 while attempt < max_tries: attempt += 1 - response = self.rpc("newaddr") + response = self.post("/v1/newaddr") if not response: sleep(2) continue @@ -211,7 +295,7 @@ def newaddress(self, max_tries=2): return False, "" def uri(self): - res = json.loads(self.rpc("getinfo")) + res = json.loads(self.post("/v1/getinfo")) if len(res["address"]) < 1: return None return f"{res['id']}@{res['address'][0]['address']}:{res['address'][0]['port']}" @@ -220,7 +304,7 @@ def walletbalance(self, max_tries=2) -> int: attempt = 0 while attempt < max_tries: attempt += 1 - response = self.rpc("listfunds") + response = self.post("/v1/listfunds") if not response: sleep(2) continue @@ -232,7 +316,7 @@ def channelbalance(self, max_tries=2) -> int: attempt = 0 while attempt < max_tries: attempt += 1 - response = self.rpc("listfunds") + response = self.post("/v1/listfunds") if not response: sleep(2) continue @@ -244,7 +328,7 @@ def connect(self, target_uri, max_tries=5) -> dict: attempt = 0 while attempt < max_tries: attempt += 1 - response = self.rpc("connect", [target_uri]) + response = self.post("/v1/connect", {"id": target_uri}) if response: res = json.loads(response) if "id" in res: @@ -269,7 +353,7 @@ def channel(self, pk, capacity, push_amt, fee_rate, max_tries=5) -> dict: attempt = 0 while attempt < max_tries: attempt += 1 - response = self.rpc("fundchannel", self.param_dict_to_list(data)) + response = self.post("/v1/fundchannel", data) if response: res = json.loads(response) if "txid" in res: @@ -283,14 +367,14 @@ def channel(self, pk, capacity, push_amt, fee_rate, max_tries=5) -> dict: return None def createinvoice(self, sats, label, description="new invoice") -> str: - response = self.rpc("invoice", [sats * 1000, label, description]) + response = self.post("invoice", {"amount_msat": sats * 1000, "label": label, "description": description}) if response: res = json.loads(response) return res["bolt11"] return None def payinvoice(self, payment_request) -> str: - response = self.rpc("pay", [payment_request]) + response = self.rpc("pay", {"bolt11": payment_request}) if response: res = json.loads(response) if "code" in res: @@ -303,7 +387,7 @@ def graph(self, max_tries=2) -> dict: attempt = 0 while attempt < max_tries: attempt += 1 - response = self.rpc("listchannels") + response = self.post("/v1/listchannels") if response: res = json.loads(response) if "channels" in res: @@ -329,8 +413,8 @@ def update(self, txid_hex: str, policy: dict, capacity: int, max_tries=2) -> dic class LND(LNNode): - def __init__(self, pod_name, logger=None): - super().__init__(pod_name, logger) + def __init__(self, pod_name): + super().__init__(pod_name) self.conn = http.client.HTTPSConnection( host=pod_name, port=8080, timeout=5, context=INSECURE_CONTEXT ) diff --git a/resources/scenarios/ln_init.py b/resources/scenarios/ln_init.py index aa8157d06..7fd519106 100644 --- a/resources/scenarios/ln_init.py +++ b/resources/scenarios/ln_init.py @@ -6,8 +6,6 @@ from commander import Commander from ln_framework.ln import Policy -THREAD_JOIN_TIMEOUT = 20 - class LNInit(Commander): def set_test_params(self): @@ -17,9 +15,6 @@ def add_options(self, parser): parser.description = "Fund LN wallets and open channels" parser.usage = "warnet run /path/to/ln_init.py" - def node_names(self, nodes): - return [item.name for item in nodes] - def run_test(self): ## # L1 P2P @@ -45,13 +40,11 @@ def gen(n): ## self.log.info("Getting LN wallet addresses...") ln_addrs = [] - ln_nodes = [] def get_ln_addr(self, ln): success, address = ln.newaddress() if success: ln_addrs.append(address) - ln_nodes.append(ln) self.log.info(f"Got wallet address {address} from {ln.name}") else: self.log.info(f"Couldn't get wallet address from {ln.name}") @@ -62,7 +55,7 @@ def get_ln_addr(self, ln): for thread in addr_threads: thread.start() - all(thread.join(timeout=THREAD_JOIN_TIMEOUT) is None for thread in addr_threads) + all(thread.join() is None for thread in addr_threads) self.log.info(f"Got {len(ln_addrs)} addresses from {len(self.lns)} LN nodes") ## @@ -96,12 +89,12 @@ def confirm_ln_balance(self, ln): sleep(1) fund_threads = [ - threading.Thread(target=confirm_ln_balance, args=(self, ln)) for ln in ln_nodes + threading.Thread(target=confirm_ln_balance, args=(self, ln)) for ln in self.lns.values() ] for thread in fund_threads: thread.start() - all(thread.join(timeout=THREAD_JOIN_TIMEOUT) is None for thread in fund_threads) + all(thread.join() is None for thread in fund_threads) self.log.info("All LN nodes are funded") ## @@ -120,11 +113,11 @@ def get_ln_uri(self, ln): break sleep(1) - uri_threads = [threading.Thread(target=get_ln_uri, args=(self, ln)) for ln in ln_nodes] + uri_threads = [threading.Thread(target=get_ln_uri, args=(self, ln)) for ln in self.lns.values()] for thread in uri_threads: thread.start() - all(thread.join(timeout=THREAD_JOIN_TIMEOUT) is None for thread in uri_threads) + all(thread.join() is None for thread in uri_threads) self.log.info("Got URIs from all LN nodes") ## @@ -134,19 +127,13 @@ def get_ln_uri(self, ln): # (source: LND, target_uri: str) tuples of LND instances connections = [] # Cycle graph through all LN nodes - nodes = list(ln_nodes) + nodes = list(self.lns.values()) prev_node = nodes[-1] for node in nodes: - if node != prev_node: - connections.append((node, prev_node)) + connections.append((node, prev_node)) prev_node = node # Explicit connections between every pair of channel partners for ch in self.channels: - node_names = self.node_names(ln_nodes) - if ch["source"] not in node_names or ch["target"] not in node_names: - self.log.error(f"LN Channel {ch} not available, removing") - self.channels.remove(ch) - continue src = self.lns[ch["source"]] tgt = self.lns[ch["target"]] # Avoid duplicates and reciprocals @@ -155,9 +142,6 @@ def get_ln_uri(self, ln): def connect_ln(self, pair): while True: - if pair[1].name not in ln_uris: - self.log.info(f"LN URIs for {pair[1].name} not found") - break res = pair[0].connect(ln_uris[pair[1].name]) if res == {}: self.log.info(f"Connected LN nodes {pair[0].name} -> {pair[1].name}") @@ -188,7 +172,7 @@ def connect_ln(self, pair): sleep(0.25) thread.start() - all(thread.join(timeout=THREAD_JOIN_TIMEOUT) is None for thread in p2p_threads) + all(thread.join() is None for thread in p2p_threads) self.log.info("Established all LN p2p connections") ## @@ -200,8 +184,7 @@ def connect_ln(self, pair): ch_by_block = {} for ch in self.channels: if "id" not in ch or "block" not in ch["id"]: - self.log.info(f"LN Channel {ch} not found") - continue + raise Exception(f"LN Channel {ch} not found") block = ch["id"]["block"] if block not in ch_by_block: ch_by_block[block] = [ch] @@ -220,8 +203,6 @@ def connect_ln(self, pair): gen(need - 1) def open_channel(self, ch, fee_rate): - if ch["source"] not in self.lns or ch["target"] not in ln_uris: - return src = self.lns[ch["source"]] tgt_uri = ln_uris[ch["target"]] tgt_pk, _ = tgt_uri.split("@") @@ -263,7 +244,7 @@ def open_channel(self, ch, fee_rate): t.start() ch_threads.append(t) - all(thread.join(timeout=THREAD_JOIN_TIMEOUT) is None for thread in ch_threads) + all(thread.join() is None for thread in ch_threads) self.log.info(f"Waiting for {len(channels)} channel opens in mempool...") self.wait_until( lambda channels=channels: self.nodes[0].getmempoolinfo()["size"] >= len(channels), @@ -305,12 +286,12 @@ def ln_all_chs(self, ln): f"LN {ln.name} graph is INCOMPLETE - {actual} of {expected} channels" ) - ch_ann_threads = [threading.Thread(target=ln_all_chs, args=(self, ln)) for ln in ln_nodes] + ch_ann_threads = [threading.Thread(target=ln_all_chs, args=(self, ln)) for ln in self.lns.values()] for thread in ch_ann_threads: sleep(0.25) thread.start() - all(thread.join(timeout=THREAD_JOIN_TIMEOUT * 2) is None for thread in ch_ann_threads) + all(thread.join() is None for thread in ch_ann_threads) self.log.info("All LN nodes have complete graph") ## @@ -357,7 +338,7 @@ def update_policy(self, ln, txid_hex, policy, capacity): update_threads.append(tt) count = len(update_threads) - all(thread.join(timeout=THREAD_JOIN_TIMEOUT) is None for thread in update_threads) + all(thread.join() is None for thread in update_threads) self.log.info(f"Sent {count} channel policy updates") self.log.info("Waiting for all channel policy gossip to synchronize...") @@ -410,13 +391,13 @@ def matching_graph(self, expected, ln): expected = sorted(self.channels, key=lambda ch: (ch["id"]["block"], ch["id"]["index"])) policy_threads = [ - threading.Thread(target=matching_graph, args=(self, expected, ln)) for ln in ln_nodes + threading.Thread(target=matching_graph, args=(self, expected, ln)) for ln in self.lns.values() ] for thread in policy_threads: sleep(0.25) thread.start() - all(thread.join(timeout=THREAD_JOIN_TIMEOUT) is None for thread in policy_threads) + all(thread.join() is None for thread in policy_threads) self.log.info("All LN nodes have matching graph!") diff --git a/test/ln_mixed_test.py b/test/ln_mixed_test.py deleted file mode 100755 index f751c7e8a..000000000 --- a/test/ln_mixed_test.py +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env python3 - -import json -import os -from pathlib import Path -from time import sleep - -from test_base import TestBase - -from resources.scenarios.ln_framework.ln import CLN, LND, LNNode -from warnet.process import stream_command - - -class LNMultiTest(TestBase): - def __init__(self): - super().__init__() - self.network_dir = Path(os.path.dirname(__file__)) / "data" / "ln_mixed" - self.scen_dir = Path(os.path.dirname(__file__)).parent / "resources" / "scenarios" - self.lns = [ - CLN("tank-0001-ln", self.log), - CLN("tank-0002-ln", self.log), - LND("tank-0003-ln", self.log), - LND("tank-0004-ln", self.log), - LND("tank-0005-ln", self.log), - ] - - def node(self, name: str) -> LNNode: - matching_nodes = [n for n in self.lns if n.name == name] - if not matching_nodes: - raise ValueError(f"No node found with name: {name}") - return matching_nodes[0] - - def run_test(self): - try: - # Wait for all nodes to wake up. ln_init will start automatically - self.setup_network() - - assert self.lns[0].walletbalance() > 0, ( - f"{self.lns[0]} has does not have a wallet balance" - ) - # Send a payment across channels opened automatically by ln_init - self.pay_invoice_rpc(sender="tank-0003-ln", recipient="tank-0001-ln") - # self.pay_invoice_node(sender="tank-0001-ln", recipient="tank-0003-ln") - - # Manually open more channels between first three nodes - # and send a payment using warnet RPC - self.manual_open_channels() - # FIXME: need to decide how to interact with LND via REST outside cluster - # self.wait_for_gossip_sync(self.lns, 5) - # self.pay_invoice(sender="tank-0004-ln", recipient="tank-0002-ln") - - finally: - self.cleanup() - - def setup_network(self): - self.log.info("Setting up network") - stream_command(f"warnet deploy {self.network_dir}") - - def wait_for_txs(self, count): - self.wait_for_predicate( - lambda: json.loads(self.warnet("bitcoin rpc tank-0001 getmempoolinfo"))["size"] == count - ) - - def manual_open_channels(self): - # 1 -> 4 - pk1 = self.warnet("ln pubkey tank-0004-ln") # prefer -> self.node("tank-0004-ln").uri() - channel = self.node("tank-0001-ln").channel(pk1, 444444, 200000, 5000) - assert "txid" in channel, "Failed to create channel between CLN and LND" - self.log.info(f"Channel txid {channel['txid']}") - - # 4 -> 2 - # channel = self.node("tank-0004-ln").channel( - # self.lns[1].uri(), 333333, 150000, 5000 - # ) - # assert "txid" in channel, "Failed to create channel between LND and CLN" - # self.log.info(f'Channel txid {channel["txid"]}') - - self.wait_for_txs(1) - - self.warnet("bitcoin rpc tank-0001 -generate 10") - - def wait_for_gossip_sync(self, nodes, expected): - while len(nodes) > 0: - for node in nodes: - chs = node.graph()["edges"] - if len(chs) >= expected: - self.log.info(f"Too many edges for {node}") - sleep(1) - - def pay_invoice_rpc(self, sender: str, recipient: str): - self.log.info("pay invoice using ln rpc") - init_balance = self.node(recipient).channelbalance() - self.log.info(f"initial balance {init_balance}") - # create cln invoice - inv = json.loads(self.warnet(f"ln rpc {recipient} invoice 1000000 label description")) - self.log.info(inv) - # pay from lightning - self.log.info(self.warnet(f"ln rpc {sender} payinvoice -f {inv['bolt11']}")) - - def wait_for_success(): - return self.node(recipient).channelbalance() == init_balance + 1000 - - self.wait_for_predicate(wait_for_success) - - # def pay_invoice_node(self, sender: str, recipient: str): - # print("pay invoice using ln framework") - # #FIXME: LND Node is not accessible outside the cluster - # init_balance = self.node(recipient).channelbalance() - # print("initial balance", init_balance) - # # create invoice - # inv = self.node(recipient).createinvoice(1000, "label2") - # print(inv) - # # pay invoie - # print(self.node(sender).payinvoice(inv)) - - # def wait_for_success(): - # print(self.node(recipient).channelbalance()) - # return self.node(recipient).channelbalance() == init_balance + 1000 - - # self.wait_for_predicate(wait_for_success) - - -if __name__ == "__main__": - test = LNMultiTest() - test.run_test() From f5aba7cbeee46100d201781b4fc6978f6dcf89dc Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Fri, 25 Apr 2025 08:36:41 -0400 Subject: [PATCH 20/24] remove RPC requirements for CLN - fetch rune via http transfer from pod other clean up for comments remove rbac exec change --- .../bitcoincore/charts/cln/templates/pod.yaml | 21 +++- .../charts/bitcoincore/charts/cln/values.yaml | 29 ++---- .../charts/commander/templates/rbac.yaml | 4 +- resources/plugins/simln/plugin.py | 20 +--- resources/scenarios/commander.py | 4 +- resources/scenarios/ln_framework/ln.py | 97 ++++--------------- resources/scenarios/ln_init.py | 11 ++- src/warnet/k8s.py | 16 +++ 8 files changed, 78 insertions(+), 124 deletions(-) diff --git a/resources/charts/bitcoincore/charts/cln/templates/pod.yaml b/resources/charts/bitcoincore/charts/cln/templates/pod.yaml index db43e4382..b2d651173 100644 --- a/resources/charts/bitcoincore/charts/cln/templates/pod.yaml +++ b/resources/charts/bitcoincore/charts/cln/templates/pod.yaml @@ -41,7 +41,12 @@ spec: - /bin/sh - -c - | - lightningd --conf=/root/.lightning/config + lightningd --conf=/root/.lightning/config & + sleep 1 + lightning-cli createrune > /working/rune.json + echo "Here is the rune file contents" + cat /working/rune.json + wait livenessProbe: {{- toYaml .Values.livenessProbe | nindent 8 }} readinessProbe: @@ -60,6 +65,20 @@ spec: {{- with .Values.extraContainers }} {{- toYaml . | nindent 4 }} {{- end }} + - name: http-server + image: busybox + command: ["/bin/sh", "-c"] + args: + - | + echo "Starting HTTP server..." + busybox httpd -f -p 8080 -h /working + ports: + - containerPort: 8080 + name: http + volumeMounts: + {{- with .Values.volumeMounts }} + {{- toYaml . | nindent 8 }} + {{- end }} volumes: {{- with .Values.volumes }} {{- toYaml . | nindent 4 }} diff --git a/resources/charts/bitcoincore/charts/cln/values.yaml b/resources/charts/bitcoincore/charts/cln/values.yaml index 8340a9e19..d34144100 100644 --- a/resources/charts/bitcoincore/charts/cln/values.yaml +++ b/resources/charts/bitcoincore/charts/cln/values.yaml @@ -8,7 +8,6 @@ image: pullPolicy: IfNotPresent tag: "v25.02" -imagePullSecrets: [] nameOverride: "" fullnameOverride: "" @@ -18,15 +17,8 @@ podLabels: mission: "lightning" podSecurityContext: {} - # fsGroup: 2000 securityContext: {} - # capabilities: - # drop: - # - ALL - # readOnlyRootFilesystem: true - # runAsNonRoot: true - # runAsUser: 1000 service: type: ClusterIP @@ -35,22 +27,16 @@ P2PPort: 9735 RPCPort: 9736 RestPort: 3010 -# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ ingress: enabled: false className: "" annotations: {} - # kubernetes.io/ingress.class: nginx - # kubernetes.io/tls-acme: "true" hosts: - host: chart-example.local paths: - path: / pathType: ImplementationSpecific tls: [] - # - secretName: chart-example-tls - # hosts: - # - chart-example.local resources: {} # We usually recommend not to specify default resources and to leave this as a conscious @@ -69,7 +55,7 @@ livenessProbe: command: - "/bin/sh" - "-c" - - "lightning-cli --network=regtest getinfo >/dev/null 2>&1" + - "lightning-cli getinfo >/dev/null 2>&1" failureThreshold: 3 initialDelaySeconds: 5 periodSeconds: 5 @@ -84,16 +70,17 @@ readinessProbe: command: - "/bin/sh" - "-c" - - "lightning-cli --network=regtest getinfo 2>/dev/null | grep -q 'id' || exit 1" + - "lightning-cli getinfo 2>/dev/null | grep -q 'id' || exit 1" # Additional volumes on the output Deployment definition. -volumes: [] +volumes: + - name: working + emptyDir: {} # Additional volumeMounts on the output Deployment definition. -volumeMounts: [] -# - name: foo -# mountPath: "/etc/foo" -# readOnly: true +volumeMounts: + - name: working + mountPath: "/working" nodeSelector: {} diff --git a/resources/charts/commander/templates/rbac.yaml b/resources/charts/commander/templates/rbac.yaml index 700bbe143..365ec62ff 100644 --- a/resources/charts/commander/templates/rbac.yaml +++ b/resources/charts/commander/templates/rbac.yaml @@ -15,8 +15,8 @@ metadata: app.kubernetes.io/name: {{ .Chart.Name }} rules: - apiGroups: [""] - resources: ["pods", "configmaps", "pods/exec"] - verbs: ["get", "list", "watch", "exec"] + resources: ["pods", "configmaps"] + verbs: ["get", "list", "watch"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding diff --git a/resources/plugins/simln/plugin.py b/resources/plugins/simln/plugin.py index 0f11df94b..c62d1b99b 100755 --- a/resources/plugins/simln/plugin.py +++ b/resources/plugins/simln/plugin.py @@ -11,11 +11,11 @@ from warnet.constants import LIGHTNING_MISSION, PLUGIN_ANNEX, AnnexMember, HookValue, WarnetContent from warnet.k8s import ( + copyfile, download, get_default_namespace, get_mission, get_static_client, - read_file_from_container, wait_for_init, write_file_to_container, ) @@ -189,7 +189,7 @@ def _generate_activity_json(activity: Optional[list[dict]]) -> str: def transfer_cln_certs(name): dst_container = "init" - cln_root = "/root/.lightning/regtest" + cln_root = "/root/.lightning/regtest" # FIXME: figure out chain for i in get_mission(LIGHTNING_MISSION): ln_name = i.metadata.name if "cln" in i.metadata.labels["app.kubernetes.io/name"]: @@ -219,22 +219,6 @@ def transfer_cln_certs(name): ) -def copyfile(pod_name, src_container, source_path, dst_name, dst_container, dst_path): - namespace = get_default_namespace() - file_data = read_file_from_container(pod_name, source_path, src_container, namespace) - if write_file_to_container( - dst_name, - dst_container, - dst_path, - file_data, - namespace=namespace, - quiet=True, - ): - log.info(f"Copied {source_path} to {dst_path}") - else: - log.error(f"Failed to copy {source_path} from {pod_name} to {dst_name}:{dst_path}") - - def _sh(pod, method: str, params: tuple[str, ...]) -> str: namespace = get_default_namespace() diff --git a/resources/scenarios/commander.py b/resources/scenarios/commander.py index 665d9bd6a..4a44ab4a7 100644 --- a/resources/scenarios/commander.py +++ b/resources/scenarios/commander.py @@ -70,9 +70,9 @@ ) if pod.metadata.labels["mission"] == "lightning": - lnnode = LND(pod.metadata.name) + lnnode = LND(pod.metadata.name, pod.status.pod_ip) if "cln" in pod.metadata.labels["app.kubernetes.io/name"]: - lnnode = CLN(pod.metadata.name) + lnnode = CLN(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 518f48291..f2ec4cbc2 100644 --- a/resources/scenarios/ln_framework/ln.py +++ b/resources/scenarios/ln_framework/ln.py @@ -2,14 +2,11 @@ import http.client import json import logging -import os import ssl from abc import ABC, abstractmethod from time import sleep -from typing import Optional -from kubernetes import client, config -from kubernetes.stream import stream +import requests # hard-coded deterministic lnd credentials ADMIN_MACAROON_HEX = "0201036c6e6402f801030a1062beabbf2a614b112128afa0c0b4fdd61201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e657261746512047265616400000620b17be53e367290871681055d0de15587f6d1cd47d1248fe2662ae27f62cfbdc6" @@ -19,36 +16,6 @@ INSECURE_CONTEXT.verify_mode = ssl.CERT_NONE -# execute kubernetes command -def run_command(name, command: list[str], namespace: Optional[str] = "default") -> str: - if os.getenv("KUBERNETES_SERVICE_HOST") and os.getenv("KUBERNETES_SERVICE_PORT"): - config.load_incluster_config() - else: - config.load_kube_config() - sclient = client.CoreV1Api() - resp = stream( - sclient.connect_get_namespaced_pod_exec, - name, - namespace, - command=command, - stderr=True, - stdin=False, - stdout=True, - tty=False, - _request_timeout=20, - _preload_content=False, - ) - result = "" - while resp.is_open(): - resp.update(timeout=5) - if resp.peek_stdout(): - result += resp.read_stdout() - if resp.peek_stderr(): - raise Exception(resp.read_stderr()) - resp.close() - return result - - # https://github.com/lightningcn/lightning-rfc/blob/master/07-routing-gossip.md#the-channel_update-message # We use the field names as written in the BOLT as our canonical, internal field names. # In LND, Policy objects returned by DescribeGraph have completely different labels @@ -113,19 +80,16 @@ def to_lnd_chanpolicy(self, capacity): class LNNode(ABC): @abstractmethod - def __init__(self, pod_name): + def __init__(self, pod_name, ip_address): self.name = pod_name + self.ip_address = ip_address self.log = logging.getLogger(pod_name) handler = logging.StreamHandler() - formatter = logging.Formatter(f'%(name)-8s - %(levelname)s: %(message)s') + formatter = logging.Formatter("%(name)-8s - %(levelname)s: %(message)s") handler.setFormatter(formatter) self.log.addHandler(handler) self.log.setLevel(logging.INFO) - @staticmethod - def param_dict_to_list(params: dict) -> list[str]: - return [f"{k}={v}" for k, v in params.items()] - @staticmethod def hex_to_b64(hex): return base64.b64encode(bytes.fromhex(hex)).decode() @@ -167,13 +131,12 @@ def update(self, txid_hex: str, policy: dict, capacity: int) -> dict: class CLN(LNNode): - def __init__(self, pod_name): - super().__init__(pod_name) - self.conn = http.client.HTTPSConnection( - host=pod_name, port=8080, timeout=5, context=INSECURE_CONTEXT - ) + def __init__(self, pod_name, ip_address): + super().__init__(pod_name, ip_address) + self.conn = None self.headers = {} self.impl = "cln" + self.reset_connection() def reset_connection(self): self.conn = http.client.HTTPSConnection( @@ -181,9 +144,7 @@ def reset_connection(self): ) def setRune(self, rune): - self.headers = { - "Rune": rune - } + self.headers = {"Rune": rune} def get(self, uri): attempt = 0 @@ -204,7 +165,9 @@ def get(self, uri): return None sleep(1) - def post(self, uri, data={}): + def post(self, uri, data=None): + if not data: + data = {} body = json.dumps(data) post_header = self.headers post_header["Content-Length"] = str(len(body)) @@ -239,37 +202,15 @@ def post(self, uri, data={}): return None sleep(1) - def rpc( - self, - method: str, - params: list[str] = None, - namespace: Optional[str] = "default", - max_tries=5, - ): - cmd = ["lightning-cli", method] - if params: - cmd.extend(params) - attempt = 0 - while attempt < max_tries: - attempt += 1 - try: - response = run_command(self.name, cmd, namespace) - if not response: - continue - return response - except Exception as e: - self.log.error(f"CLN rpc error: {e}, wait and retry...") - sleep(2) - return None - def createrune(self, max_tries=2): attempt = 0 while attempt < max_tries: attempt += 1 - response = self.rpc("createrune") + response = requests.get(f"http://{self.ip_address}:8080/rune.json", timeout=5).text if not response: sleep(2) continue + self.log.debug(response) res = json.loads(response) self.setRune(res["rune"]) return @@ -367,14 +308,16 @@ def channel(self, pk, capacity, push_amt, fee_rate, max_tries=5) -> dict: return None def createinvoice(self, sats, label, description="new invoice") -> str: - response = self.post("invoice", {"amount_msat": sats * 1000, "label": label, "description": description}) + response = self.post( + "invoice", {"amount_msat": sats * 1000, "label": label, "description": description} + ) if response: res = json.loads(response) return res["bolt11"] return None def payinvoice(self, payment_request) -> str: - response = self.rpc("pay", {"bolt11": payment_request}) + response = self.post("/v1/pay", {"bolt11": payment_request}) if response: res = json.loads(response) if "code" in res: @@ -413,8 +356,8 @@ def update(self, txid_hex: str, policy: dict, capacity: int, max_tries=2) -> dic class LND(LNNode): - def __init__(self, pod_name): - super().__init__(pod_name) + def __init__(self, pod_name, ip_address): + super().__init__(pod_name, ip_address) self.conn = http.client.HTTPSConnection( host=pod_name, port=8080, timeout=5, context=INSECURE_CONTEXT ) diff --git a/resources/scenarios/ln_init.py b/resources/scenarios/ln_init.py index 7fd519106..416763ed7 100644 --- a/resources/scenarios/ln_init.py +++ b/resources/scenarios/ln_init.py @@ -113,7 +113,9 @@ def get_ln_uri(self, ln): break sleep(1) - uri_threads = [threading.Thread(target=get_ln_uri, args=(self, ln)) for ln in self.lns.values()] + uri_threads = [ + threading.Thread(target=get_ln_uri, args=(self, ln)) for ln in self.lns.values() + ] for thread in uri_threads: thread.start() @@ -286,7 +288,9 @@ def ln_all_chs(self, ln): f"LN {ln.name} graph is INCOMPLETE - {actual} of {expected} channels" ) - ch_ann_threads = [threading.Thread(target=ln_all_chs, args=(self, ln)) for ln in self.lns.values()] + ch_ann_threads = [ + threading.Thread(target=ln_all_chs, args=(self, ln)) for ln in self.lns.values() + ] for thread in ch_ann_threads: sleep(0.25) thread.start() @@ -391,7 +395,8 @@ def matching_graph(self, expected, ln): expected = sorted(self.channels, key=lambda ch: (ch["id"]["block"], ch["id"]["index"])) policy_threads = [ - threading.Thread(target=matching_graph, args=(self, expected, ln)) for ln in self.lns.values() + threading.Thread(target=matching_graph, args=(self, expected, ln)) + for ln in self.lns.values() ] for thread in policy_threads: sleep(0.25) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index 26400c8b3..d5da36dc4 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -635,3 +635,19 @@ def read_file_from_container( raise Exception(resp.read_stderr()) resp.close() return result + + +def copyfile(pod_name, src_container, source_path, dst_name, dst_container, dst_path): + namespace = get_default_namespace() + file_data = read_file_from_container(pod_name, source_path, src_container, namespace) + if write_file_to_container( + dst_name, + dst_container, + dst_path, + file_data, + namespace=namespace, + quiet=True, + ): + print(f"Copied {source_path} to {dst_path}") + else: + print(f"Failed to copy {source_path} from {pod_name} to {dst_name}:{dst_path}") From 4fb02255a69482a5260c46bba38055f459d6ff50 Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Fri, 25 Apr 2025 15:09:55 -0400 Subject: [PATCH 21/24] Add debug logs to deploy step --- resources/plugins/simln/plugin.py | 3 ++- src/warnet/deploy.py | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/resources/plugins/simln/plugin.py b/resources/plugins/simln/plugin.py index c62d1b99b..78df1a917 100755 --- a/resources/plugins/simln/plugin.py +++ b/resources/plugins/simln/plugin.py @@ -189,10 +189,11 @@ def _generate_activity_json(activity: Optional[list[dict]]) -> str: def transfer_cln_certs(name): dst_container = "init" - cln_root = "/root/.lightning/regtest" # FIXME: figure out chain for i in get_mission(LIGHTNING_MISSION): ln_name = i.metadata.name if "cln" in i.metadata.labels["app.kubernetes.io/name"]: + chain = i.metadata.labels["chain"] + cln_root = f"/root/.lightning/{chain}" copyfile( ln_name, "cln", diff --git a/src/warnet/deploy.py b/src/warnet/deploy.py index bc30e1ab1..04733e1bb 100644 --- a/src/warnet/deploy.py +++ b/src/warnet/deploy.py @@ -391,6 +391,7 @@ def deploy_network(directory: Path, debug: bool = False, namespace: Optional[str p.join() if needs_ln_init: + click.echo("deploy_network->_run started") name = _run( scenario_file=SCENARIOS_DIR / "ln_init.py", debug=False, @@ -399,8 +400,11 @@ def deploy_network(directory: Path, debug: bool = False, namespace: Optional[str admin=False, namespace=namespace, ) + click.echo("deploy_network->_run finished") wait_for_pod(name, namespace=namespace) + click.echo("wait_for_pod finished") _logs(pod_name=name, follow=True, namespace=namespace) + click.echo("deploy_network finished") def deploy_single_node(node, directory: Path, debug: bool, namespace: str): From 7890267c6e9c3fce3e3dbdb5bea39665d08de5ac Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Fri, 25 Apr 2025 16:28:47 -0400 Subject: [PATCH 22/24] try wait_for_pod_ready --- src/warnet/deploy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/warnet/deploy.py b/src/warnet/deploy.py index 04733e1bb..708133cf7 100644 --- a/src/warnet/deploy.py +++ b/src/warnet/deploy.py @@ -401,7 +401,7 @@ def deploy_network(directory: Path, debug: bool = False, namespace: Optional[str namespace=namespace, ) click.echo("deploy_network->_run finished") - wait_for_pod(name, namespace=namespace) + wait_for_pod_ready(name, namespace=namespace) click.echo("wait_for_pod finished") _logs(pod_name=name, follow=True, namespace=namespace) click.echo("deploy_network finished") From 76023bb29a65194312074fe48ca7d677a7a33289 Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Thu, 1 May 2025 17:21:01 -0400 Subject: [PATCH 23/24] remove debug logging update documentation for cln --- docs/scenarios.md | 6 ------ resources/plugins/simln/README.md | 6 +++--- src/warnet/deploy.py | 5 ----- 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/docs/scenarios.md b/docs/scenarios.md index da19880de..c2892c421 100644 --- a/docs/scenarios.md +++ b/docs/scenarios.md @@ -72,9 +72,3 @@ Total Tanks: 6 | Active Scenarios: 0 ## Running a custom scenario You can write your own scenario file and run it in the same way. - -## Scenarios with lightning nodes - -When defining network.yaml all lnd nodes should be indexed in the same block before any cln nodes otherwise node responsiveness causes the expected index to get out of order with actual regardless of how the channels are opened -Review `test/data/ln_mixed/network.yaml` for an example - diff --git a/resources/plugins/simln/README.md b/resources/plugins/simln/README.md index f1acb1d94..a627813af 100644 --- a/resources/plugins/simln/README.md +++ b/resources/plugins/simln/README.md @@ -19,7 +19,7 @@ SimLN also requires access details for each node; however, the SimLN plugin will "cert": } ```` -SimLN also supports Core Lightning (CLN). CLN nodes connection details are transfered from the CLN node to SimLN node during launch-activity processing. +SimLN plugin also supports Core Lightning (CLN). CLN nodes connection details are transfered from the CLN node to SimLN node during launch-activity processing. ```` JSON { "id": , @@ -83,8 +83,8 @@ nodes: addnode: - tank-0000 ln: - lnd: true - lnd: + cln: true + cln: channels: - id: block: 300 diff --git a/src/warnet/deploy.py b/src/warnet/deploy.py index 708133cf7..b8c22d932 100644 --- a/src/warnet/deploy.py +++ b/src/warnet/deploy.py @@ -37,7 +37,6 @@ get_mission, get_namespaces_by_type, wait_for_ingress_controller, - wait_for_pod, wait_for_pod_ready, ) from .process import run_command, stream_command @@ -391,7 +390,6 @@ def deploy_network(directory: Path, debug: bool = False, namespace: Optional[str p.join() if needs_ln_init: - click.echo("deploy_network->_run started") name = _run( scenario_file=SCENARIOS_DIR / "ln_init.py", debug=False, @@ -400,11 +398,8 @@ def deploy_network(directory: Path, debug: bool = False, namespace: Optional[str admin=False, namespace=namespace, ) - click.echo("deploy_network->_run finished") wait_for_pod_ready(name, namespace=namespace) - click.echo("wait_for_pod finished") _logs(pod_name=name, follow=True, namespace=namespace) - click.echo("deploy_network finished") def deploy_single_node(node, directory: Path, debug: bool, namespace: str): From f21bfdd7d208d16e0022202913a4cb49747d9a25 Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Thu, 1 May 2025 17:55:56 -0400 Subject: [PATCH 24/24] remove unused test files --- test/data/ln_mixed/network.yaml | 66 --------------------------- test/data/ln_mixed/node-defaults.yaml | 14 ------ 2 files changed, 80 deletions(-) delete mode 100644 test/data/ln_mixed/network.yaml delete mode 100644 test/data/ln_mixed/node-defaults.yaml diff --git a/test/data/ln_mixed/network.yaml b/test/data/ln_mixed/network.yaml deleted file mode 100644 index 56a2938d1..000000000 --- a/test/data/ln_mixed/network.yaml +++ /dev/null @@ -1,66 +0,0 @@ -nodes: - - name: tank-0001 - addnode: - - tank-0003 - ln: - cln: true - cln: - channels: - - id: - block: 300 - index: 4 - target: tank-0003-ln - capacity: 50001 - push_amt: 25003 - - name: tank-0002 - addnode: - - tank-0001 - ln: - cln: true - cln: - channels: - - id: - block: 300 - index: 5 - target: tank-0001-ln - capacity: 50002 - push_amt: 25001 - - name: tank-0003 - addnode: - - tank-0004 - ln: - lnd: true - lnd: - channels: - - id: - block: 300 - index: 1 - target: tank-0004-ln - capacity: 100003 - push_amt: 50004 - - name: tank-0004 - addnode: - - tank-0003 - ln: - lnd: true - lnd: - channels: - - id: - block: 300 - index: 2 - target: tank-0005-ln - capacity: 50004 - push_amt: 25005 - - name: tank-0005 - addnode: - - tank-0003 - ln: - lnd: true - lnd: - channels: - - id: - block: 300 - index: 3 - target: tank-0002-ln - capacity: 100005 - push_amt: 50002 \ No newline at end of file diff --git a/test/data/ln_mixed/node-defaults.yaml b/test/data/ln_mixed/node-defaults.yaml deleted file mode 100644 index df6d01dbb..000000000 --- a/test/data/ln_mixed/node-defaults.yaml +++ /dev/null @@ -1,14 +0,0 @@ -image: - repository: bitcoindevproject/bitcoin - pullPolicy: IfNotPresent - tag: "27.0" - -lnd: - defaultConfig: | - color=#000000 - config: | - bitcoin.timelockdelta=33 - -cln: - defaultConfig: | - rgb=ff3155