From 73edcabdec31fdeb4d787c1c941d8bf524b56940 Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Thu, 27 Feb 2025 02:42:24 +0100 Subject: [PATCH 01/44] circuit breaker plugin readme --- resources/plugins/circuitbreaker/README.md | 151 +++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 resources/plugins/circuitbreaker/README.md diff --git a/resources/plugins/circuitbreaker/README.md b/resources/plugins/circuitbreaker/README.md new file mode 100644 index 000000000..5fb1e7f9c --- /dev/null +++ b/resources/plugins/circuitbreaker/README.md @@ -0,0 +1,151 @@ +# Circuit Breaker Plugin + +## Overview +The Circuit Breaker plugin integrates the [circuitbreaker](https://github.com/lightningequipment/circuitbreaker) tool with Warnet to protect Lightning Network nodes from being flooded with HTLCs. Circuit Breaker functions like a firewall for Lightning, allowing node operators to set limits on in-flight HTLCs and implement rate limiting on a per-peer basis. + +## What is Circuit Breaker? +Circuit Breaker is to Lightning what firewalls are to the internet. It provides protection against: +- HTLC flooding attacks +- Channel slot exhaustion (max 483 slots per channel) +- DoS/spam attacks using large numbers of fast-resolving HTLCs +- Channel balance probing attacks + +Circuit Breaker offers insights into HTLC traffic and provides configurable operating modes to handle excess traffic. + +## Usage +In your Python virtual environment with Warnet installed and set up, create a new Warnet user folder: + +``` +$ warnet new user_folder +$ cd user_folder +``` + +Deploy a network with Circuit Breaker enabled: + +``` +$ warnet deploy networks/circuitbreaker +``` + +## Configuration in `network.yaml` +You can incorporate the Circuit Breaker plugin into your `network.yaml` file as shown below: + +```yaml +nodes: + - name: tank-0000 + addnode: + - tank-0001 + ln: + lnd: true + + - name: tank-0001 + addnode: + - tank-0002 + ln: + lnd: true + + - name: tank-0002 + addnode: + - tank-0000 + ln: + lnd: true + + - name: tank-0003 + addnode: + - tank-0000 + ln: + lnd: true + lnd: + channels: + - id: + block: 300 + index: 1 + target: tank-0004-ln + capacity: 100000 + push_amt: 50000 + +plugins: + postDeploy: + circuitbreaker: + entrypoint: "../../plugins/circuitbreaker" + nodes: ["tank-0000-ln", "tank-0003-ln"] # Nodes to apply Circuit Breaker to + mode: "fail" # Operating mode: fail, queue, or queue_peer_initiated + maxPendingHtlcs: 10 # Default maximum pending HTLCs per peer + rateLimit: 1 # Minimum seconds between HTLCs (token bucket rate limit) +``` + +## Plugin Parameters + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `nodes` | List of LN node names to apply Circuit Breaker to | Required | +| `mode` | Operating mode (`fail`, `queue`, or `queue_peer_initiated`) | `fail` | +| `maxPendingHtlcs` | Default maximum number of pending HTLCs per peer | `30` | +| `rateLimit` | Minimum interval in seconds between HTLCs | `0` (disabled) | +| `port` | Port to expose the Circuit Breaker UI on | `9235` | +| `trusted_peers` | Map of node pubkeys to their individual HTLC limits | `{}` | + +## Operating Modes + +- **fail**: Fail HTLCs when limits are exceeded. Minimizes liquidity lock-up but affects routing reputation. +- **queue**: Queue HTLCs when limits are exceeded, forwarding them when space becomes available. Penalizes upstream nodes for bad traffic. +- **queue_peer_initiated**: Queue only HTLCs from channels that the remote node initiated. Uses fail mode for channels we initiated. + +**WARNING**: Queue modes require LND 0.16+ with auto-fail support to prevent force-closes. + +## Accessing the UI + +After deploying, you can port-forward to access the Circuit Breaker UI: + +``` +$ kubectl port-forward pod/circuitbreaker-tank-0000 9235:9235 +``` + +Then open http://127.0.0.1:9235 in a browser to view and configure Circuit Breaker settings. + +## Advanced Configuration Example + +```yaml +plugins: + postDeploy: + circuitbreaker: + entrypoint: "../../plugins/circuitbreaker" + nodes: ["tank-0000-ln", "tank-0003-ln"] + mode: "fail" + maxPendingHtlcs: 15 + rateLimit: 0.5 + trusted_peers: { + "03abcdef...": 50, + "02123456...": 100 + } +``` + + + +## Limitations + +- Circuit Breaker is alpha quality software. Use with caution, especially on mainnet. +- LND interfaces are not optimized for this purpose, which may lead to edge cases. +- Queue modes require LND 0.16+ to prevent channel force-closes. + +## Development + +To build your own version of the Circuit Breaker plugin: + +1. Clone the Circuit Breaker repository: `git clone https://github.com/lightningequipment/circuitbreaker.git` +2. Follow the build instructions in the repository +3. Update the plugin's `values.yaml` to point to your custom image \ No newline at end of file From 63d41bbc150c8c82ee683ed1f534e8ba40bdf965 Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Thu, 27 Feb 2025 02:42:49 +0100 Subject: [PATCH 02/44] circuit breaker plugin skeleton setup --- .../charts/circuitbreaker/.helmignore | 23 ++++ .../charts/circuitbreaker/Chart.yaml | 5 + .../circuitbreaker/templates/_helpers.tpl | 7 ++ .../charts/circuitbreaker/templates/pod.yaml | 17 +++ .../charts/circuitbreaker/values.yaml | 5 + resources/plugins/circuitbreaker/plugin.py | 117 ++++++++++++++++++ 6 files changed, 174 insertions(+) create mode 100644 resources/plugins/circuitbreaker/charts/circuitbreaker/.helmignore create mode 100644 resources/plugins/circuitbreaker/charts/circuitbreaker/Chart.yaml create mode 100644 resources/plugins/circuitbreaker/charts/circuitbreaker/templates/_helpers.tpl create mode 100644 resources/plugins/circuitbreaker/charts/circuitbreaker/templates/pod.yaml create mode 100644 resources/plugins/circuitbreaker/charts/circuitbreaker/values.yaml create mode 100644 resources/plugins/circuitbreaker/plugin.py diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/.helmignore b/resources/plugins/circuitbreaker/charts/circuitbreaker/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/resources/plugins/circuitbreaker/charts/circuitbreaker/.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/plugins/circuitbreaker/charts/circuitbreaker/Chart.yaml b/resources/plugins/circuitbreaker/charts/circuitbreaker/Chart.yaml new file mode 100644 index 000000000..ab03ed1a3 --- /dev/null +++ b/resources/plugins/circuitbreaker/charts/circuitbreaker/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: circuitbreaker +description: A Helm chart to deploy Circuit Breaker +version: 0.1.0 +appVersion: "0.1.0" diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/_helpers.tpl b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/_helpers.tpl new file mode 100644 index 000000000..a699083e5 --- /dev/null +++ b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/_helpers.tpl @@ -0,0 +1,7 @@ +{{- define "mychart.name" -}} +{{- .Chart.Name | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "mychart.fullname" -}} +{{- printf "%s-%s" (include "mychart.name" .) .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/pod.yaml b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/pod.yaml new file mode 100644 index 000000000..a15a891f2 --- /dev/null +++ b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/pod.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{ include "mychart.fullname" . }} + labels: + app: {{ include "mychart.name" . }} + mission: {{ .Values.name }} +spec: + containers: + - name: {{ .Values.name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: ["sh", "-c"] + args: + - echo "Hello {{ .Values.mode }}"; + resources: {} + \ No newline at end of file diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/values.yaml b/resources/plugins/circuitbreaker/charts/circuitbreaker/values.yaml new file mode 100644 index 000000000..a439d37f7 --- /dev/null +++ b/resources/plugins/circuitbreaker/charts/circuitbreaker/values.yaml @@ -0,0 +1,5 @@ +name: "circuitbreaker" +image: + repository: "camillarhi/circuitbreaker" + tag: "0.2.3" + pullPolicy: IfNotPresent \ No newline at end of file diff --git a/resources/plugins/circuitbreaker/plugin.py b/resources/plugins/circuitbreaker/plugin.py new file mode 100644 index 000000000..07cb3bef1 --- /dev/null +++ b/resources/plugins/circuitbreaker/plugin.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +import json +import logging +from enum import Enum +from pathlib import Path +import time +from typing import Optional + +import click + +from warnet.constants import PLUGIN_ANNEX, AnnexMember, HookValue, WarnetContent +from warnet.process import run_command + +MISSION = "circuitbreaker" +PRIMARY_CONTAINER = MISSION + +PLUGIN_DIR_TAG = "plugin_dir" + + +class PluginError(Exception): + pass + + +log = logging.getLogger(MISSION) +if not log.hasHandlers(): + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.DEBUG) + formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + console_handler.setFormatter(formatter) + log.addHandler(console_handler) +log.setLevel(logging.DEBUG) +log.propagate = True + +class PluginContent(Enum): + MODE = "mode" + MAX_PENDING_HTLCS = "maxPendingHtlcs" + RATE_LIMIT = "rateLimit" + +@click.group() +@click.pass_context +def circuitbreaker(ctx): + """Commands for the Circuit Breaker plugin""" + ctx.ensure_object(dict) + plugin_dir = Path(__file__).resolve().parent + ctx.obj[PLUGIN_DIR_TAG] = Path(plugin_dir) + + +@circuitbreaker.command() +@click.argument("plugin_content", type=str) +@click.argument("warnet_content", type=str) +@click.pass_context +def entrypoint(ctx, plugin_content: str, warnet_content: str): + """Plugin entrypoint""" + plugin_content: dict = json.loads(plugin_content) + warnet_content: dict = json.loads(warnet_content) + + hook_value = warnet_content.get(WarnetContent.HOOK_VALUE.value) + + assert hook_value in { + item.value for item in HookValue + }, f"{hook_value} is not a valid HookValue" + + if warnet_content.get(PLUGIN_ANNEX): + for annex_member in [annex_item for annex_item in warnet_content.get(PLUGIN_ANNEX)]: + assert annex_member in { + item.value for item in AnnexMember + }, f"{annex_member} is not a valid AnnexMember" + + warnet_content[WarnetContent.HOOK_VALUE.value] = HookValue(hook_value) + + _entrypoint(ctx, plugin_content, warnet_content) + + +def _entrypoint(ctx, plugin_content: dict, warnet_content: dict): + """Called by entrypoint""" + hook_value = warnet_content[WarnetContent.HOOK_VALUE.value] + + match hook_value: + case ( + HookValue.PRE_NETWORK + | HookValue.POST_NETWORK + | HookValue.PRE_DEPLOY + | HookValue.POST_DEPLOY + ): + data = get_data(plugin_content) + if data: + _launch_circuit_breaker(ctx, node_name=hook_value.value.lower()) + else: + _launch_circuit_breaker(ctx, node_name=hook_value.value.lower()) + case HookValue.PRE_NODE: + name = warnet_content[PLUGIN_ANNEX][AnnexMember.NODE_NAME.value] + "-pre-pod" + _launch_circuit_breaker(ctx, node_name=hook_value.value.lower() + "-" + name) + case HookValue.POST_NODE: + name = warnet_content[PLUGIN_ANNEX][AnnexMember.NODE_NAME.value] + "-post-pod" + _launch_circuit_breaker(ctx, node_name=hook_value.value.lower() + "-" + name) + +def get_data(plugin_content: dict) -> Optional[dict]: + data = { + key: plugin_content.get(key) + for key in (PluginContent.MAX_PENDING_HTLCS.value, PluginContent.RATE_LIMIT.value) + if plugin_content.get(key) + } + return data or None + + +def _launch_circuit_breaker(ctx, node_name: str): + timestamp = int(time.time()) + release_name = f"cb-{node_name}-{timestamp}" + + command = f"helm upgrade --install {node_name} {ctx.obj[PLUGIN_DIR_TAG]}/charts/circuitbreaker --set node={node_name}" + + log.info(command) + log.info(run_command(command)) + + +if __name__ == "__main__": + circuitbreaker() From 83f9f41551be963f50c95ddb1dc92a66b75d29f6 Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Thu, 27 Feb 2025 23:45:48 +0100 Subject: [PATCH 03/44] Circuit breaker plugin integration --- resources/networks/hello/network.yaml | 42 +++++++++++++++++++ .../circuitbreaker/templates/_helpers.tpl | 7 ---- .../charts/circuitbreaker/templates/pod.yaml | 9 ++-- .../charts/circuitbreaker/values.yaml | 2 +- resources/plugins/circuitbreaker/plugin.py | 33 +++++++++++---- 5 files changed, 71 insertions(+), 22 deletions(-) delete mode 100644 resources/plugins/circuitbreaker/charts/circuitbreaker/templates/_helpers.tpl diff --git a/resources/networks/hello/network.yaml b/resources/networks/hello/network.yaml index f5acf0a83..359dc1bfe 100644 --- a/resources/networks/hello/network.yaml +++ b/resources/networks/hello/network.yaml @@ -59,6 +59,13 @@ plugins: # Each plugin section has a number of hooks available (preDeploy, post entrypoint: "../../plugins/hello" # This entrypoint path is relative to the network.yaml file podName: "hello-pre-deploy" helloTo: "preDeploy!" + circuitbreaker: + entrypoint: "../../plugins/circuitbreaker" + url: "http://127.0.0.1:9235" + apiUrl: "/api" + mode: "fail" # Operating mode: fail, queue, or queue_peer_initiated + maxPendingHtlcs: 10 # Default maximum pending HTLCs per peer + rateLimit: 1 # Minimum seconds between HTLCs (token bucket rate limit) postDeploy: hello: entrypoint: "../../plugins/hello" @@ -67,21 +74,56 @@ plugins: # Each plugin section has a number of hooks available (preDeploy, post 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}]' + circuitbreaker: + entrypoint: "../../plugins/circuitbreaker" + url: "http://127.0.0.1:9235" + apiUrl: "/api" + mode: "fail" # Operating mode: fail, queue, or queue_peer_initiated + maxPendingHtlcs: 10 # Default maximum pending HTLCs per peer + rateLimit: 1 # Minimum seconds between HTLCs (token bucket rate limit) preNode: # preNode plugins run before each node is deployed hello: entrypoint: "../../plugins/hello" helloTo: "preNode!" + circuitbreaker: + entrypoint: "../../plugins/circuitbreaker" + url: "http://127.0.0.1:9235" + apiUrl: "/api" + mode: "fail" # Operating mode: fail, queue, or queue_peer_initiated + maxPendingHtlcs: 10 # Default maximum pending HTLCs per peer + rateLimit: 1 # Minimum seconds between HTLCs (token bucket rate limit) postNode: hello: entrypoint: "../../plugins/hello" helloTo: "postNode!" + circuitbreaker: + entrypoint: "../../plugins/circuitbreaker" + url: "http://127.0.0.1:9235" + apiUrl: "/api" + mode: "fail" # Operating mode: fail, queue, or queue_peer_initiated + maxPendingHtlcs: 10 # Default maximum pending HTLCs per peer + rateLimit: 1 # Minimum seconds between HTLCs (token bucket rate limit) preNetwork: hello: entrypoint: "../../plugins/hello" helloTo: "preNetwork!" podName: "hello-pre-network" + circuitbreaker: + entrypoint: "../../plugins/circuitbreaker" + url: "http://127.0.0.1:9235" + apiUrl: "/api" + mode: "fail" # Operating mode: fail, queue, or queue_peer_initiated + maxPendingHtlcs: 10 # Default maximum pending HTLCs per peer + rateLimit: 1 # Minimum seconds between HTLCs (token bucket rate limit) postNetwork: hello: entrypoint: "../../plugins/hello" helloTo: "postNetwork!" podName: "hello-post-network" + circuitbreaker: + entrypoint: "../../plugins/circuitbreaker" + url: "http://127.0.0.1:9235" + apiUrl: "/api" + mode: "fail" # Operating mode: fail, queue, or queue_peer_initiated + maxPendingHtlcs: 10 # Default maximum pending HTLCs per peer + rateLimit: 1 # Minimum seconds between HTLCs (token bucket rate limit) diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/_helpers.tpl b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/_helpers.tpl deleted file mode 100644 index a699083e5..000000000 --- a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/_helpers.tpl +++ /dev/null @@ -1,7 +0,0 @@ -{{- define "mychart.name" -}} -{{- .Chart.Name | trunc 63 | trimSuffix "-" -}} -{{- end -}} - -{{- define "mychart.fullname" -}} -{{- printf "%s-%s" (include "mychart.name" .) .Release.Name | trunc 63 | trimSuffix "-" -}} -{{- end -}} diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/pod.yaml b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/pod.yaml index a15a891f2..534a2b528 100644 --- a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/pod.yaml +++ b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/pod.yaml @@ -1,17 +1,16 @@ apiVersion: v1 kind: Pod metadata: - name: {{ include "mychart.fullname" . }} + name: {{ .Values.name }} labels: - app: {{ include "mychart.name" . }} - mission: {{ .Values.name }} + app: {{ .Chart.Name }} spec: containers: - - name: {{ .Values.name }} + - name: {{ .Values.name }}-container image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" imagePullPolicy: {{ .Values.image.pullPolicy }} command: ["sh", "-c"] args: - - echo "Hello {{ .Values.mode }}"; + - echo "Hello {{ .Values.name }}"; resources: {} \ No newline at end of file diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/values.yaml b/resources/plugins/circuitbreaker/charts/circuitbreaker/values.yaml index a439d37f7..7acc1a6a1 100644 --- a/resources/plugins/circuitbreaker/charts/circuitbreaker/values.yaml +++ b/resources/plugins/circuitbreaker/charts/circuitbreaker/values.yaml @@ -1,5 +1,5 @@ name: "circuitbreaker" image: repository: "camillarhi/circuitbreaker" - tag: "0.2.3" + tag: "latest" pullPolicy: IfNotPresent \ No newline at end of file diff --git a/resources/plugins/circuitbreaker/plugin.py b/resources/plugins/circuitbreaker/plugin.py index 07cb3bef1..9667a7e87 100644 --- a/resources/plugins/circuitbreaker/plugin.py +++ b/resources/plugins/circuitbreaker/plugin.py @@ -11,6 +11,15 @@ from warnet.constants import PLUGIN_ANNEX, AnnexMember, HookValue, WarnetContent from warnet.process import run_command +from warnet.k8s import ( + download, + get_default_namespace, + get_mission, + get_static_client, + wait_for_init, + write_file_to_container, +) + MISSION = "circuitbreaker" PRIMARY_CONTAINER = MISSION @@ -84,15 +93,15 @@ def _entrypoint(ctx, plugin_content: dict, warnet_content: dict): ): data = get_data(plugin_content) if data: - _launch_circuit_breaker(ctx, node_name=hook_value.value.lower()) + _launch_circuit_breaker(ctx, node_name=hook_value.value.lower()+"breaker",hook_value=hook_value.value) else: - _launch_circuit_breaker(ctx, node_name=hook_value.value.lower()) + _launch_circuit_breaker(ctx, node_name=hook_value.value.lower()+"breaker",hook_value=hook_value.value) case HookValue.PRE_NODE: name = warnet_content[PLUGIN_ANNEX][AnnexMember.NODE_NAME.value] + "-pre-pod" - _launch_circuit_breaker(ctx, node_name=hook_value.value.lower() + "-" + name) + _launch_circuit_breaker(ctx, node_name=hook_value.value.lower() + "-" + name, hook_value=hook_value.value) case HookValue.POST_NODE: name = warnet_content[PLUGIN_ANNEX][AnnexMember.NODE_NAME.value] + "-post-pod" - _launch_circuit_breaker(ctx, node_name=hook_value.value.lower() + "-" + name) + _launch_circuit_breaker(ctx, node_name=hook_value.value.lower() + "-" + name, hook_value=hook_value.value) def get_data(plugin_content: dict) -> Optional[dict]: data = { @@ -103,14 +112,20 @@ def get_data(plugin_content: dict) -> Optional[dict]: return data or None -def _launch_circuit_breaker(ctx, node_name: str): +def _launch_circuit_breaker(ctx, node_name: str, hook_value: str): timestamp = int(time.time()) - release_name = f"cb-{node_name}-{timestamp}" - - command = f"helm upgrade --install {node_name} {ctx.obj[PLUGIN_DIR_TAG]}/charts/circuitbreaker --set node={node_name}" + release_name = f"cb-{node_name}" + # command = f"helm upgrade --install {release_name} {ctx.obj[PLUGIN_DIR_TAG]}/charts/circuitbreaker" + command = ( + f"helm upgrade --install {release_name} {ctx.obj[PLUGIN_DIR_TAG]}/charts/circuitbreaker " + f"--set name={release_name}" + ) log.info(command) - log.info(run_command(command)) + run_command(command) + + if(hook_value==HookValue.POST_DEPLOY): + wait_for_init(release_name, namespace=get_default_namespace(), quiet=True) if __name__ == "__main__": From caeb5ad899539c8b1f7984b6c0407fa8ad1d8f20 Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Mon, 3 Mar 2025 00:31:05 +0100 Subject: [PATCH 04/44] circuit breaker setup --- resources/networks/hello/network.yaml | 45 +---- .../charts/circuitbreaker/Chart.yaml | 2 +- .../circuitbreaker/templates/deployment.yaml | 36 ++++ .../charts/circuitbreaker/templates/pod.yaml | 16 -- .../circuitbreaker/templates/service.yaml | 11 ++ .../charts/circuitbreaker/values.yaml | 4 +- resources/plugins/circuitbreaker/plugin.py | 159 ++++++++++++++---- 7 files changed, 181 insertions(+), 92 deletions(-) create mode 100644 resources/plugins/circuitbreaker/charts/circuitbreaker/templates/deployment.yaml delete mode 100644 resources/plugins/circuitbreaker/charts/circuitbreaker/templates/pod.yaml create mode 100644 resources/plugins/circuitbreaker/charts/circuitbreaker/templates/service.yaml diff --git a/resources/networks/hello/network.yaml b/resources/networks/hello/network.yaml index 359dc1bfe..92c3af386 100644 --- a/resources/networks/hello/network.yaml +++ b/resources/networks/hello/network.yaml @@ -59,13 +59,6 @@ plugins: # Each plugin section has a number of hooks available (preDeploy, post entrypoint: "../../plugins/hello" # This entrypoint path is relative to the network.yaml file podName: "hello-pre-deploy" helloTo: "preDeploy!" - circuitbreaker: - entrypoint: "../../plugins/circuitbreaker" - url: "http://127.0.0.1:9235" - apiUrl: "/api" - mode: "fail" # Operating mode: fail, queue, or queue_peer_initiated - maxPendingHtlcs: 10 # Default maximum pending HTLCs per peer - rateLimit: 1 # Minimum seconds between HTLCs (token bucket rate limit) postDeploy: hello: entrypoint: "../../plugins/hello" @@ -76,54 +69,24 @@ plugins: # Each plugin section has a number of hooks available (preDeploy, post activity: '[{"source": "tank-0003-ln", "destination": "tank-0005-ln", "interval_secs": 1, "amount_msat": 2000}]' circuitbreaker: entrypoint: "../../plugins/circuitbreaker" - url: "http://127.0.0.1:9235" - apiUrl: "/api" - mode: "fail" # Operating mode: fail, queue, or queue_peer_initiated - maxPendingHtlcs: 10 # Default maximum pending HTLCs per peer - rateLimit: 1 # Minimum seconds between HTLCs (token bucket rate limit) + podName: "circuitbreaker-pod" + rpcserver: "tank-0000-ln:10009" + httplisten: "0.0.0.0:9235" preNode: # preNode plugins run before each node is deployed hello: entrypoint: "../../plugins/hello" helloTo: "preNode!" - circuitbreaker: - entrypoint: "../../plugins/circuitbreaker" - url: "http://127.0.0.1:9235" - apiUrl: "/api" - mode: "fail" # Operating mode: fail, queue, or queue_peer_initiated - maxPendingHtlcs: 10 # Default maximum pending HTLCs per peer - rateLimit: 1 # Minimum seconds between HTLCs (token bucket rate limit) postNode: hello: entrypoint: "../../plugins/hello" helloTo: "postNode!" - circuitbreaker: - entrypoint: "../../plugins/circuitbreaker" - url: "http://127.0.0.1:9235" - apiUrl: "/api" - mode: "fail" # Operating mode: fail, queue, or queue_peer_initiated - maxPendingHtlcs: 10 # Default maximum pending HTLCs per peer - rateLimit: 1 # Minimum seconds between HTLCs (token bucket rate limit) preNetwork: hello: entrypoint: "../../plugins/hello" helloTo: "preNetwork!" podName: "hello-pre-network" - circuitbreaker: - entrypoint: "../../plugins/circuitbreaker" - url: "http://127.0.0.1:9235" - apiUrl: "/api" - mode: "fail" # Operating mode: fail, queue, or queue_peer_initiated - maxPendingHtlcs: 10 # Default maximum pending HTLCs per peer - rateLimit: 1 # Minimum seconds between HTLCs (token bucket rate limit) postNetwork: hello: entrypoint: "../../plugins/hello" helloTo: "postNetwork!" - podName: "hello-post-network" - circuitbreaker: - entrypoint: "../../plugins/circuitbreaker" - url: "http://127.0.0.1:9235" - apiUrl: "/api" - mode: "fail" # Operating mode: fail, queue, or queue_peer_initiated - maxPendingHtlcs: 10 # Default maximum pending HTLCs per peer - rateLimit: 1 # Minimum seconds between HTLCs (token bucket rate limit) + podName: "hello-post-network" \ No newline at end of file diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/Chart.yaml b/resources/plugins/circuitbreaker/charts/circuitbreaker/Chart.yaml index ab03ed1a3..6cb99f1b8 100644 --- a/resources/plugins/circuitbreaker/charts/circuitbreaker/Chart.yaml +++ b/resources/plugins/circuitbreaker/charts/circuitbreaker/Chart.yaml @@ -2,4 +2,4 @@ apiVersion: v2 name: circuitbreaker description: A Helm chart to deploy Circuit Breaker version: 0.1.0 -appVersion: "0.1.0" +appVersion: "0.1.0" \ No newline at end of file diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/deployment.yaml b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/deployment.yaml new file mode 100644 index 000000000..865fb3b47 --- /dev/null +++ b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/deployment.yaml @@ -0,0 +1,36 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Values.podName }} +spec: + replicas: 1 + selector: + matchLabels: + app: circuitbreaker + template: + metadata: + labels: + app: circuitbreaker + spec: + containers: + - name: circuitbreaker + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + args: + - "--rpcserver={{ .Values.rpcserver }}" + - "--httplisten={{ .Values.httplisten }}" + ports: + - containerPort: 9235 + volumeMounts: + - name: lnd-tls-cert + mountPath: /root/.lnd/tls.cert + subPath: tls.cert + - name: lnd-macaroon + mountPath: /root/.lnd/data/chain/bitcoin/regtest/admin.macaroon + subPath: admin.macaroon + volumes: + - name: lnd-tls-cert + secret: + secretName: lnd-tls-cert-{{ .Values.podName }} + - name: lnd-macaroon + secret: + secretName: lnd-macaroon-{{ .Values.podName }} \ No newline at end of file diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/pod.yaml b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/pod.yaml deleted file mode 100644 index 534a2b528..000000000 --- a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/pod.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: {{ .Values.name }} - labels: - app: {{ .Chart.Name }} -spec: - containers: - - name: {{ .Values.name }}-container - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - command: ["sh", "-c"] - args: - - echo "Hello {{ .Values.name }}"; - resources: {} - \ No newline at end of file diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/service.yaml b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/service.yaml new file mode 100644 index 000000000..45ddc5397 --- /dev/null +++ b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/service.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.podName }} +spec: + selector: + app: circuitbreaker + ports: + - protocol: TCP + port: 9235 + targetPort: 9235 \ No newline at end of file diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/values.yaml b/resources/plugins/circuitbreaker/charts/circuitbreaker/values.yaml index 7acc1a6a1..f9c66f7e8 100644 --- a/resources/plugins/circuitbreaker/charts/circuitbreaker/values.yaml +++ b/resources/plugins/circuitbreaker/charts/circuitbreaker/values.yaml @@ -1,4 +1,6 @@ -name: "circuitbreaker" +podName: "circuitbreaker-pod" +rpcserver: "localhost:10009" +httplisten: "0.0.0.0:9235" image: repository: "camillarhi/circuitbreaker" tag: "latest" diff --git a/resources/plugins/circuitbreaker/plugin.py b/resources/plugins/circuitbreaker/plugin.py index 9667a7e87..3de4147b9 100644 --- a/resources/plugins/circuitbreaker/plugin.py +++ b/resources/plugins/circuitbreaker/plugin.py @@ -3,6 +3,7 @@ import logging from enum import Enum from pathlib import Path +import subprocess import time from typing import Optional @@ -25,11 +26,9 @@ PLUGIN_DIR_TAG = "plugin_dir" - class PluginError(Exception): pass - log = logging.getLogger(MISSION) if not log.hasHandlers(): console_handler = logging.StreamHandler() @@ -41,9 +40,9 @@ class PluginError(Exception): log.propagate = True class PluginContent(Enum): - MODE = "mode" - MAX_PENDING_HTLCS = "maxPendingHtlcs" - RATE_LIMIT = "rateLimit" + POD_NAME = "podName" + LND_RPC_SERVER = "rpcserver" + HTTP_LISTEN = "httplisten" @click.group() @click.pass_context @@ -85,47 +84,141 @@ def _entrypoint(ctx, plugin_content: dict, warnet_content: dict): hook_value = warnet_content[WarnetContent.HOOK_VALUE.value] match hook_value: - case ( - HookValue.PRE_NETWORK - | HookValue.POST_NETWORK - | HookValue.PRE_DEPLOY - | HookValue.POST_DEPLOY - ): - data = get_data(plugin_content) - if data: - _launch_circuit_breaker(ctx, node_name=hook_value.value.lower()+"breaker",hook_value=hook_value.value) - else: - _launch_circuit_breaker(ctx, node_name=hook_value.value.lower()+"breaker",hook_value=hook_value.value) - case HookValue.PRE_NODE: - name = warnet_content[PLUGIN_ANNEX][AnnexMember.NODE_NAME.value] + "-pre-pod" - _launch_circuit_breaker(ctx, node_name=hook_value.value.lower() + "-" + name, hook_value=hook_value.value) - case HookValue.POST_NODE: - name = warnet_content[PLUGIN_ANNEX][AnnexMember.NODE_NAME.value] + "-post-pod" - _launch_circuit_breaker(ctx, node_name=hook_value.value.lower() + "-" + name, hook_value=hook_value.value) + case HookValue.POST_DEPLOY: + # data = get_data(plugin_content) + # if data: + # log.info(f"Launching circuit breaker with data: {data}") + # _create_secrets() + _launch_circuit_breaker(ctx, plugin_content) + # else: + # _launch_circuit_breaker(ctx, install_name="circuitbreaker") + case _: + log.info(f"No action required for hook {hook_value}") def get_data(plugin_content: dict) -> Optional[dict]: data = { key: plugin_content.get(key) - for key in (PluginContent.MAX_PENDING_HTLCS.value, PluginContent.RATE_LIMIT.value) + for key in (PluginContent.POD_NAME.value, PluginContent.LND_RPC_SERVER.value, PluginContent.HTTP_LISTEN.value) if plugin_content.get(key) } return data or None +# def _create_secrets(): +# """Use local LND files for testing""" +# log.info("Using local LND files for testing") +# tls_cert_path = Path.home() / ".lnd" / "tls.cert" +# admin_macaroon_path = Path.home() / ".lnd" / "data" / "chain" / "bitcoin" / "signet" / "admin.macaroon" -def _launch_circuit_breaker(ctx, node_name: str, hook_value: str): +# if not tls_cert_path.exists(): +# raise PluginError(f"TLS certificate not found at {tls_cert_path}") +# if not admin_macaroon_path.exists(): +# raise PluginError(f"Admin macaroon not found at {admin_macaroon_path}") + +# log.info(f"Using TLS certificate: {tls_cert_path}") +# log.info(f"Using admin macaroon: {admin_macaroon_path}") + +# def _create_secrets(): +# """Create Kubernetes secrets for each LND node""" +# lnd_pods = subprocess.check_output(["kubectl", "get", "pods", "-l", "mission=lightning", "-o", "name"]).decode().splitlines() +# # lnd_pods = subprocess.check_output(["kubectl", "get", "pods", "-l", "app=warnet", "-l", "mission=lightning", "-o", "name"]).decode().splitlines() +# for node in lnd_pods: +# node_name = node.split('/')[-1] +# log.info(f"Waiting for {node_name} to be ready...") +# wait_for_init(node_name, namespace=get_default_namespace(), quiet=True) +# log.info(f"Creating secrets for {node_name}") +# subprocess.run(["kubectl", "cp", f"{node}:/root/.lnd/tls.cert", "./tls.cert"], check=True) +# subprocess.run(["kubectl", "cp", f"{node}:/root/.lnd/data/chain/bitcoin/regtest/admin.macaroon", "./admin.macaroon"], check=True) +# subprocess.run(["kubectl", "create", "secret", "generic", f"lnd-tls-cert-{node_name}", "--from-file=tls.cert=./tls.cert"], check=True) +# subprocess.run(["kubectl", "create", "secret", "generic", f"lnd-macaroon-{node_name}", "--from-file=admin.macaroon=./admin.macaroon"], check=True) + +def _create_secrets(): + """Create Kubernetes secrets for each LND node""" + lnd_pods = subprocess.check_output( + ["kubectl", "get", "pods", "-l", "mission=lightning", "-o", "name"] + ).decode().splitlines() + + for node in lnd_pods: + node_name = node.split('/')[-1] + log.info(f"Waiting for {node_name} to be ready...") + + # Wait for the pod to be ready + max_retries = 10 + retry_delay = 10 # seconds + for attempt in range(max_retries): + try: + # Check if the pod is ready + pod_status = subprocess.check_output( + ["kubectl", "get", "pod", node_name, "-o", "jsonpath='{.status.phase}'"] + ).decode().strip("'") + + if pod_status == "Running": + log.info(f"{node_name} is ready.") + break + else: + log.info(f"{node_name} is not ready yet (status: {pod_status}). Retrying in {retry_delay} seconds...") + except subprocess.CalledProcessError as e: + log.error(f"Failed to check pod status for {node_name}: {e}") + if attempt == max_retries - 1: + raise PluginError(f"Pod {node_name} did not become ready after {max_retries} attempts.") + + time.sleep(retry_delay) + + # Create secrets for the pod + log.info(f"Creating secrets for {node_name}") + try: + subprocess.run( + ["kubectl", "cp", f"{node_name}:/root/.lnd/tls.cert", "./tls.cert"], + check=True + ) + subprocess.run( + ["kubectl", "cp", f"{node_name}:/root/.lnd/data/chain/bitcoin/regtest/admin.macaroon", "./admin.macaroon"], + check=True + ) + subprocess.run( + ["kubectl", "create", "secret", "generic", f"lnd-tls-cert-{node_name}", "--from-file=tls.cert=./tls.cert"], + check=True + ) + subprocess.run( + ["kubectl", "create", "secret", "generic", f"lnd-macaroon-{node_name}", "--from-file=admin.macaroon=./admin.macaroon"], + check=True + ) + except subprocess.CalledProcessError as e: + log.error(f"Failed to create secrets for {node_name}: {e}") + raise PluginError(f"Failed to create secrets for {node_name}.") + +def _launch_circuit_breaker(ctx, + plugin_content: dict, + install_name: str="circuitbreaker", + podName: str ="circuitbreaker-pod", + rpcserver: str = "localhost:10009", + httplisten: str = "0.0.0.0:9235"): timestamp = int(time.time()) - release_name = f"cb-{node_name}" + # release_name = f"cb-{install_name}" + lnd_pods = subprocess.check_output(["kubectl", "get", "pods", "-l", "app=warnet", "-l", "mission=lightning", "-o", "name"]).decode().splitlines() + for node in lnd_pods: + node_name = node.split('/')[-1] + log.info(f"Launching Circuit Breaker for {node_name}") + release_name = f"circuitbreaker-{node_name}" + + command = ( + f"helm upgrade --install {release_name} {ctx.obj[PLUGIN_DIR_TAG]}/charts/circuitbreaker " + f"--set podName={release_name} --set rpcserver=localhost:10009 --set httplisten=0.0.0.0:9235" + ) + # command = f"helm upgrade --install {release_name} {ctx.obj[PLUGIN_DIR_TAG]}/charts/circuitbreaker" - command = ( - f"helm upgrade --install {release_name} {ctx.obj[PLUGIN_DIR_TAG]}/charts/circuitbreaker " - f"--set name={release_name}" - ) - log.info(command) - run_command(command) + # command = ( + # f"helm upgrade --install {install_name} {ctx.obj[PLUGIN_DIR_TAG]}/charts/circuitbreaker " + # f"--set podName={podName} --set rpcserver={rpcserver} --set httplisten={httplisten}" + # ) + log.info(command) + try: + run_command(command) - if(hook_value==HookValue.POST_DEPLOY): - wait_for_init(release_name, namespace=get_default_namespace(), quiet=True) + # if(hook_value==HookValue.POST_DEPLOY): + wait_for_init(release_name, namespace=get_default_namespace(), quiet=True) + except Exception as e: + log.error(f"Failed to launch Circuit Breaker for {node_name}: {e}") if __name__ == "__main__": From 9d42a4591236e2d75974d4e3b205bfa05300c6f1 Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Mon, 3 Mar 2025 02:00:52 +0100 Subject: [PATCH 05/44] circuit breaker setup --- .../circuitbreaker/templates/_helpers.tpl | 7 +++ .../circuitbreaker/templates/configmap.yaml | 21 ++++++++ .../circuitbreaker/templates/deployment.yaml | 36 ------------- .../charts/circuitbreaker/templates/pod.yaml | 46 ++++++++++++++++ .../circuitbreaker/templates/service.yaml | 11 ---- .../charts/circuitbreaker/values.yaml | 15 ++++-- resources/plugins/circuitbreaker/plugin.py | 53 ++++++------------- 7 files changed, 101 insertions(+), 88 deletions(-) create mode 100644 resources/plugins/circuitbreaker/charts/circuitbreaker/templates/_helpers.tpl create mode 100644 resources/plugins/circuitbreaker/charts/circuitbreaker/templates/configmap.yaml delete mode 100644 resources/plugins/circuitbreaker/charts/circuitbreaker/templates/deployment.yaml create mode 100644 resources/plugins/circuitbreaker/charts/circuitbreaker/templates/pod.yaml delete mode 100644 resources/plugins/circuitbreaker/charts/circuitbreaker/templates/service.yaml diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/_helpers.tpl b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/_helpers.tpl new file mode 100644 index 000000000..a699083e5 --- /dev/null +++ b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/_helpers.tpl @@ -0,0 +1,7 @@ +{{- define "mychart.name" -}} +{{- .Chart.Name | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "mychart.fullname" -}} +{{- printf "%s-%s" (include "mychart.name" .) .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/configmap.yaml b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/configmap.yaml new file mode 100644 index 000000000..9688722b6 --- /dev/null +++ b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/configmap.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "mychart.fullname" . }}-data +data: + tls.cert: | + -----BEGIN CERTIFICATE----- + MIIB8TCCAZagAwIBAgIUJDsR6mmY+TaO9pCfjtotlbOkzJMwCgYIKoZIzj0EAwIw + MjEfMB0GA1UECgwWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEPMA0GA1UEAwwGd2Fy + bmV0MB4XDTI0MTExMTE2NTM1MFoXDTM0MTEwOTE2NTM1MFowMjEfMB0GA1UECgwW + bG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEPMA0GA1UEAwwGd2FybmV0MFkwEwYHKoZI + zj0CAQYIKoZIzj0DAQcDQgAEBVltIvaTlAQI/3FFatTqVflZuZdRJ0SmRMSJrFLP + tp0fxE7hmteSt6gjQriy90fP8j9OJXBNAjt915kLY4zVvqOBiTCBhjAOBgNVHQ8B + Af8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUwAwEB/zAd + BgNVHQ4EFgQU5d8QMrwhLgTkDjWA+eXZGz+dybUwLwYDVR0RBCgwJoIJbG9jYWxo + b3N0ggEqhwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMAoGCCqGSM49BAMCA0kAMEYC + IQDPofN0fEl5gTwCYhk3nZbjMqJhZ8BsSJ6K8XRhxr7zbwIhAPsgQCFOqUWg632O + NEO53OQ6CIqnpxSskjsFNH4ZBQOE + -----END CERTIFICATE----- + admin.macaroon.hex: | + 0201036c6e6402f801030a1062beabbf2a614b112128afa0c0b4fdd61201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e657261746512047265616400000620b17be53e367290871681055d0de15587f6d1cd47d1248fe2662ae27f62cfbdc6 diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/deployment.yaml b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/deployment.yaml deleted file mode 100644 index 865fb3b47..000000000 --- a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/deployment.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ .Values.podName }} -spec: - replicas: 1 - selector: - matchLabels: - app: circuitbreaker - template: - metadata: - labels: - app: circuitbreaker - spec: - containers: - - name: circuitbreaker - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" - args: - - "--rpcserver={{ .Values.rpcserver }}" - - "--httplisten={{ .Values.httplisten }}" - ports: - - containerPort: 9235 - volumeMounts: - - name: lnd-tls-cert - mountPath: /root/.lnd/tls.cert - subPath: tls.cert - - name: lnd-macaroon - mountPath: /root/.lnd/data/chain/bitcoin/regtest/admin.macaroon - subPath: admin.macaroon - volumes: - - name: lnd-tls-cert - secret: - secretName: lnd-tls-cert-{{ .Values.podName }} - - name: lnd-macaroon - secret: - secretName: lnd-macaroon-{{ .Values.podName }} \ No newline at end of file diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/pod.yaml b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/pod.yaml new file mode 100644 index 000000000..459371b33 --- /dev/null +++ b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/pod.yaml @@ -0,0 +1,46 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{ include "mychart.fullname" . }} + labels: + app: {{ include "mychart.name" . }} + mission: {{ .Values.name }} +spec: + initContainers: + - name: "init" + image: "busybox" + command: + - "sh" + - "-c" + args: + - > + mkdir -p /shared/.lnd/data/chain/bitcoin/mainnet && + cp /configmap/tls.cert /shared/.lnd/tls.cert && + cat /configmap/admin.macaroon.hex | xxd -r -p > /shared/.lnd/data/chain/bitcoin/mainnet/admin.macaroon + volumeMounts: + - name: shared-volume + mountPath: /shared + - name: configmap-volume + mountPath: /configmap + containers: + - name: {{ .Values.name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - "sh" + - "-c" + args: + - > + mkdir -p /root/.lnd/data/chain/bitcoin/mainnet && + ln -s /shared/.lnd/tls.cert /root/.lnd/tls.cert && + ln -s /shared/.lnd/data/chain/bitcoin/mainnet/admin.macaroon /root/.lnd/data/chain/bitcoin/mainnet/admin.macaroon && + circuitbreaker --rpcserver={{ .Values.lnd.rpcserver }} --httplisten={{ .Values.lnd.httplisten }} + volumeMounts: + - name: shared-volume + mountPath: /shared + volumes: + - name: configmap-volume + configMap: + name: {{ include "mychart.fullname" . }}-data + - name: shared-volume + emptyDir: {} \ No newline at end of file diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/service.yaml b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/service.yaml deleted file mode 100644 index 45ddc5397..000000000 --- a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/service.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ .Values.podName }} -spec: - selector: - app: circuitbreaker - ports: - - protocol: TCP - port: 9235 - targetPort: 9235 \ No newline at end of file diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/values.yaml b/resources/plugins/circuitbreaker/charts/circuitbreaker/values.yaml index f9c66f7e8..e539f0010 100644 --- a/resources/plugins/circuitbreaker/charts/circuitbreaker/values.yaml +++ b/resources/plugins/circuitbreaker/charts/circuitbreaker/values.yaml @@ -1,7 +1,14 @@ -podName: "circuitbreaker-pod" -rpcserver: "localhost:10009" -httplisten: "0.0.0.0:9235" +name: "circuitbreaker" image: repository: "camillarhi/circuitbreaker" tag: "latest" - pullPolicy: IfNotPresent \ No newline at end of file + pullPolicy: IfNotPresent +workingVolume: + name: working-volume + mountPath: /working +configmapVolume: + name: configmap-volume + mountPath: /configmap +lnd: + rpcserver: "host.docker.internal:10009" # Default LND RPC server address + httplisten: "0.0.0.0:9235" # Default HTTP listen address \ No newline at end of file diff --git a/resources/plugins/circuitbreaker/plugin.py b/resources/plugins/circuitbreaker/plugin.py index 3de4147b9..6700c0b88 100644 --- a/resources/plugins/circuitbreaker/plugin.py +++ b/resources/plugins/circuitbreaker/plugin.py @@ -85,13 +85,11 @@ def _entrypoint(ctx, plugin_content: dict, warnet_content: dict): match hook_value: case HookValue.POST_DEPLOY: - # data = get_data(plugin_content) - # if data: - # log.info(f"Launching circuit breaker with data: {data}") - # _create_secrets() - _launch_circuit_breaker(ctx, plugin_content) - # else: - # _launch_circuit_breaker(ctx, install_name="circuitbreaker") + data = get_data(plugin_content) + if data: + _launch_pod(ctx, install_name="circuitbreaker", **data) + else: + _launch_pod(ctx, install_name="circuitbreaker") case _: log.info(f"No action required for hook {hook_value}") @@ -186,40 +184,21 @@ def _create_secrets(): log.error(f"Failed to create secrets for {node_name}: {e}") raise PluginError(f"Failed to create secrets for {node_name}.") -def _launch_circuit_breaker(ctx, - plugin_content: dict, - install_name: str="circuitbreaker", - podName: str ="circuitbreaker-pod", - rpcserver: str = "localhost:10009", - httplisten: str = "0.0.0.0:9235"): +def _launch_pod(ctx, + install_name: str = "circuitbreaker", + podName: str = "circuitbreaker-pod", + rpcserver: str = "localhost:10009", + httplisten: str = "0.0.0.0:9235"): timestamp = int(time.time()) # release_name = f"cb-{install_name}" - lnd_pods = subprocess.check_output(["kubectl", "get", "pods", "-l", "app=warnet", "-l", "mission=lightning", "-o", "name"]).decode().splitlines() - for node in lnd_pods: - node_name = node.split('/')[-1] - log.info(f"Launching Circuit Breaker for {node_name}") - release_name = f"circuitbreaker-{node_name}" - - command = ( - f"helm upgrade --install {release_name} {ctx.obj[PLUGIN_DIR_TAG]}/charts/circuitbreaker " - f"--set podName={release_name} --set rpcserver=localhost:10009 --set httplisten=0.0.0.0:9235" - ) - - # command = f"helm upgrade --install {release_name} {ctx.obj[PLUGIN_DIR_TAG]}/charts/circuitbreaker" - # command = ( - # f"helm upgrade --install {install_name} {ctx.obj[PLUGIN_DIR_TAG]}/charts/circuitbreaker " - # f"--set podName={podName} --set rpcserver={rpcserver} --set httplisten={httplisten}" - # ) - log.info(command) - try: - run_command(command) + command = ( + f"helm upgrade --install {install_name} {ctx.obj[PLUGIN_DIR_TAG]}/charts/circuitbreaker " + f"--set podName={podName} --set rpcserver={rpcserver} --set httplisten={httplisten}" + ) - # if(hook_value==HookValue.POST_DEPLOY): - wait_for_init(release_name, namespace=get_default_namespace(), quiet=True) - except Exception as e: - log.error(f"Failed to launch Circuit Breaker for {node_name}: {e}") - + log.info(command) + log.info(run_command(command)) if __name__ == "__main__": circuitbreaker() From bccc1114276d79ee028b873f67e718d4170d39f5 Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Tue, 4 Mar 2025 00:49:25 +0100 Subject: [PATCH 06/44] connect to the local lnd node and access circuit breaker from the UI --- resources/networks/hello/network.yaml | 2 +- .../circuitbreaker/templates/configmap.yaml | 26 ++++++++++--------- .../charts/circuitbreaker/values.yaml | 2 +- resources/plugins/circuitbreaker/plugin.py | 3 +++ 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/resources/networks/hello/network.yaml b/resources/networks/hello/network.yaml index 92c3af386..75204a641 100644 --- a/resources/networks/hello/network.yaml +++ b/resources/networks/hello/network.yaml @@ -70,7 +70,7 @@ plugins: # Each plugin section has a number of hooks available (preDeploy, post circuitbreaker: entrypoint: "../../plugins/circuitbreaker" podName: "circuitbreaker-pod" - rpcserver: "tank-0000-ln:10009" + rpcserver: "172.29.34.166:10009" httplisten: "0.0.0.0:9235" preNode: # preNode plugins run before each node is deployed hello: diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/configmap.yaml b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/configmap.yaml index 9688722b6..3216c79ea 100644 --- a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/configmap.yaml +++ b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/configmap.yaml @@ -5,17 +5,19 @@ metadata: data: tls.cert: | -----BEGIN CERTIFICATE----- - MIIB8TCCAZagAwIBAgIUJDsR6mmY+TaO9pCfjtotlbOkzJMwCgYIKoZIzj0EAwIw - MjEfMB0GA1UECgwWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEPMA0GA1UEAwwGd2Fy - bmV0MB4XDTI0MTExMTE2NTM1MFoXDTM0MTEwOTE2NTM1MFowMjEfMB0GA1UECgwW - bG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEPMA0GA1UEAwwGd2FybmV0MFkwEwYHKoZI - zj0CAQYIKoZIzj0DAQcDQgAEBVltIvaTlAQI/3FFatTqVflZuZdRJ0SmRMSJrFLP - tp0fxE7hmteSt6gjQriy90fP8j9OJXBNAjt915kLY4zVvqOBiTCBhjAOBgNVHQ8B - Af8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUwAwEB/zAd - BgNVHQ4EFgQU5d8QMrwhLgTkDjWA+eXZGz+dybUwLwYDVR0RBCgwJoIJbG9jYWxo - b3N0ggEqhwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMAoGCCqGSM49BAMCA0kAMEYC - IQDPofN0fEl5gTwCYhk3nZbjMqJhZ8BsSJ6K8XRhxr7zbwIhAPsgQCFOqUWg632O - NEO53OQ6CIqnpxSskjsFNH4ZBQOE + MIICRTCCAeygAwIBAgIRAMe5IfFsBM9nqG1hwA1tswAwCgYIKoZIzj0EAwIwOzEf + MB0GA1UEChMWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEYMBYGA1UEAxMPREVTS1RP + UC00OEJVR0xTMB4XDTI1MDIwMTE2NDAyNFoXDTI2MDMyOTE2NDAyNFowOzEfMB0G + A1UEChMWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEYMBYGA1UEAxMPREVTS1RPUC00 + OEJVR0xTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIl4bWvtGVb1T4iUyjLfj + U2IVnF1yJBwbTa2diRJh+a0UbwjUSdn/hIVkNALr9f3NKYWmotyq8IGOmjwhAFis + HKOB0DCBzTAOBgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYD + VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU38wWLmz1lsVv7vtZZamSgkcoQUcwdgYD + VR0RBG8wbYIPREVTS1RPUC00OEJVR0xTgglsb2NhbGhvc3SCBHVuaXiCCnVuaXhw + YWNrZXSCB2J1ZmNvbm6HBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAGHBAr///6HBKwd + IqaHEP6AAAAAAAAAAhVd//6GM+MwCgYIKoZIzj0EAwIDRwAwRAIgNe9zoH9iz7Tw + 1j8+Jk05DU6nJ48a5mbP0viZ50UGu7sCIEK0AoPBrqxnicdhEEInONWyIm5VUR/l + YURZZyNuJ8lJ -----END CERTIFICATE----- admin.macaroon.hex: | - 0201036c6e6402f801030a1062beabbf2a614b112128afa0c0b4fdd61201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e657261746512047265616400000620b17be53e367290871681055d0de15587f6d1cd47d1248fe2662ae27f62cfbdc6 + 0201036C6E6402F801030A107EC4D3E96DE93FA58F70968A1729AE6C1201301A160A0761646472657373120472656164120577726974651A130A04696E666F120472656164120577726974651A170A08696E766F69636573120472656164120577726974651A210A086D616361726F6F6E120867656E6572617465120472656164120577726974651A160A076D657373616765120472656164120577726974651A170A086F6666636861696E120472656164120577726974651A160A076F6E636861696E120472656164120577726974651A140A057065657273120472656164120577726974651A180A067369676E6572120867656E65726174651204726561640000062023AFC3BF7DB1D186342905D79461793FFCB59F583858F495C253F0A1EB4D33C2 diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/values.yaml b/resources/plugins/circuitbreaker/charts/circuitbreaker/values.yaml index e539f0010..e052a8f91 100644 --- a/resources/plugins/circuitbreaker/charts/circuitbreaker/values.yaml +++ b/resources/plugins/circuitbreaker/charts/circuitbreaker/values.yaml @@ -10,5 +10,5 @@ configmapVolume: name: configmap-volume mountPath: /configmap lnd: - rpcserver: "host.docker.internal:10009" # Default LND RPC server address + rpcserver: "172.29.34.166:10009" # Default LND RPC server address httplisten: "0.0.0.0:9235" # Default HTTP listen address \ No newline at end of file diff --git a/resources/plugins/circuitbreaker/plugin.py b/resources/plugins/circuitbreaker/plugin.py index 6700c0b88..d0111cd42 100644 --- a/resources/plugins/circuitbreaker/plugin.py +++ b/resources/plugins/circuitbreaker/plugin.py @@ -197,6 +197,9 @@ def _launch_pod(ctx, f"--set podName={podName} --set rpcserver={rpcserver} --set httplisten={httplisten}" ) + # Use this to port-forward the circuitbreaker pod to localhost + # kubectl port-forward pod/circuitbreaker-circuitbreaker 9235:9235 + log.info(command) log.info(run_command(command)) From 9bcadb6dea358735ee3a4d95733061fa8c17764e Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Tue, 4 Mar 2025 02:02:43 +0100 Subject: [PATCH 07/44] use nodeport to expose circuit breaker UI --- .../charts/circuitbreaker/templates/service.yaml | 14 ++++++++++++++ resources/plugins/circuitbreaker/plugin.py | 3 --- 2 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 resources/plugins/circuitbreaker/charts/circuitbreaker/templates/service.yaml diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/service.yaml b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/service.yaml new file mode 100644 index 000000000..42d17b602 --- /dev/null +++ b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "mychart.fullname" . }}-service + labels: + app: {{ include "mychart.name" . }} +spec: + type: NodePort + ports: + - port: 9235 + targetPort: 9235 + nodePort: 30000 # Choose a port between 30000-32767 + selector: + app: {{ include "mychart.name" . }} \ No newline at end of file diff --git a/resources/plugins/circuitbreaker/plugin.py b/resources/plugins/circuitbreaker/plugin.py index d0111cd42..6700c0b88 100644 --- a/resources/plugins/circuitbreaker/plugin.py +++ b/resources/plugins/circuitbreaker/plugin.py @@ -197,9 +197,6 @@ def _launch_pod(ctx, f"--set podName={podName} --set rpcserver={rpcserver} --set httplisten={httplisten}" ) - # Use this to port-forward the circuitbreaker pod to localhost - # kubectl port-forward pod/circuitbreaker-circuitbreaker 9235:9235 - log.info(command) log.info(run_command(command)) From 370a83043c104f35416d2200135dce956abfb2cc Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Sun, 9 Mar 2025 18:34:22 +0100 Subject: [PATCH 08/44] update lnd pod to run circuit breaker --- .../bitcoincore/charts/lnd/templates/pod.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml b/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml index c5d66851a..d08850a44 100644 --- a/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml +++ b/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml @@ -21,6 +21,21 @@ spec: {{- end }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 4 }} + initContainers: + - name: prepare-files + image: busybox + command: + - sh + - -c + - | + cp /root/.lnd/tls.cert /root/.lnd/tls_copy.cert + chmod 644 /root/.lnd/tls_copy.cert + volumeMounts: + - name: lnd-data + mountPath: /root/.lnd + - name: config + mountPath: /root/.lnd/tls.cert + subPath: tls.cert containers: - name: {{ .Chart.Name }} securityContext: @@ -46,6 +61,8 @@ spec: resources: {{- toYaml .Values.resources | nindent 8 }} volumeMounts: + - name: lnd-data + mountPath: /root/.lnd {{- with .Values.volumeMounts }} {{- toYaml . | nindent 8 }} {{- end }} @@ -62,6 +79,8 @@ spec: {{- toYaml . | nindent 4 }} {{- end }} volumes: + - name: lnd-data + emptyDir: {} {{- with .Values.volumes }} {{- toYaml . | nindent 4 }} {{- end }} From 01c04f180535979d9f6e99d2babefca9a5e0ef70 Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Sun, 9 Mar 2025 21:28:36 +0100 Subject: [PATCH 09/44] plugin: remove circuit-breaker from the plugin section --- resources/networks/hello/network.yaml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/resources/networks/hello/network.yaml b/resources/networks/hello/network.yaml index 75204a641..313126147 100644 --- a/resources/networks/hello/network.yaml +++ b/resources/networks/hello/network.yaml @@ -67,11 +67,6 @@ plugins: # Each plugin section has a number of hooks available (preDeploy, post 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}]' - circuitbreaker: - entrypoint: "../../plugins/circuitbreaker" - podName: "circuitbreaker-pod" - rpcserver: "172.29.34.166:10009" - httplisten: "0.0.0.0:9235" preNode: # preNode plugins run before each node is deployed hello: entrypoint: "../../plugins/hello" From a1c8645c3c5240a5508e9a96036de8d360011778 Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Mon, 10 Mar 2025 22:30:08 +0100 Subject: [PATCH 10/44] plugin: remove circuit-breaker from the plugin section --- resources/plugins/circuitbreaker/README.md | 151 ------------- .../charts/circuitbreaker/.helmignore | 23 -- .../charts/circuitbreaker/Chart.yaml | 5 - .../circuitbreaker/templates/_helpers.tpl | 7 - .../circuitbreaker/templates/configmap.yaml | 23 -- .../charts/circuitbreaker/templates/pod.yaml | 46 ---- .../circuitbreaker/templates/service.yaml | 14 -- .../charts/circuitbreaker/values.yaml | 14 -- resources/plugins/circuitbreaker/plugin.py | 204 ------------------ 9 files changed, 487 deletions(-) delete mode 100644 resources/plugins/circuitbreaker/README.md delete mode 100644 resources/plugins/circuitbreaker/charts/circuitbreaker/.helmignore delete mode 100644 resources/plugins/circuitbreaker/charts/circuitbreaker/Chart.yaml delete mode 100644 resources/plugins/circuitbreaker/charts/circuitbreaker/templates/_helpers.tpl delete mode 100644 resources/plugins/circuitbreaker/charts/circuitbreaker/templates/configmap.yaml delete mode 100644 resources/plugins/circuitbreaker/charts/circuitbreaker/templates/pod.yaml delete mode 100644 resources/plugins/circuitbreaker/charts/circuitbreaker/templates/service.yaml delete mode 100644 resources/plugins/circuitbreaker/charts/circuitbreaker/values.yaml delete mode 100644 resources/plugins/circuitbreaker/plugin.py diff --git a/resources/plugins/circuitbreaker/README.md b/resources/plugins/circuitbreaker/README.md deleted file mode 100644 index 5fb1e7f9c..000000000 --- a/resources/plugins/circuitbreaker/README.md +++ /dev/null @@ -1,151 +0,0 @@ -# Circuit Breaker Plugin - -## Overview -The Circuit Breaker plugin integrates the [circuitbreaker](https://github.com/lightningequipment/circuitbreaker) tool with Warnet to protect Lightning Network nodes from being flooded with HTLCs. Circuit Breaker functions like a firewall for Lightning, allowing node operators to set limits on in-flight HTLCs and implement rate limiting on a per-peer basis. - -## What is Circuit Breaker? -Circuit Breaker is to Lightning what firewalls are to the internet. It provides protection against: -- HTLC flooding attacks -- Channel slot exhaustion (max 483 slots per channel) -- DoS/spam attacks using large numbers of fast-resolving HTLCs -- Channel balance probing attacks - -Circuit Breaker offers insights into HTLC traffic and provides configurable operating modes to handle excess traffic. - -## Usage -In your Python virtual environment with Warnet installed and set up, create a new Warnet user folder: - -``` -$ warnet new user_folder -$ cd user_folder -``` - -Deploy a network with Circuit Breaker enabled: - -``` -$ warnet deploy networks/circuitbreaker -``` - -## Configuration in `network.yaml` -You can incorporate the Circuit Breaker plugin into your `network.yaml` file as shown below: - -```yaml -nodes: - - name: tank-0000 - addnode: - - tank-0001 - ln: - lnd: true - - - name: tank-0001 - addnode: - - tank-0002 - ln: - lnd: true - - - name: tank-0002 - addnode: - - tank-0000 - ln: - lnd: true - - - name: tank-0003 - addnode: - - tank-0000 - ln: - lnd: true - lnd: - channels: - - id: - block: 300 - index: 1 - target: tank-0004-ln - capacity: 100000 - push_amt: 50000 - -plugins: - postDeploy: - circuitbreaker: - entrypoint: "../../plugins/circuitbreaker" - nodes: ["tank-0000-ln", "tank-0003-ln"] # Nodes to apply Circuit Breaker to - mode: "fail" # Operating mode: fail, queue, or queue_peer_initiated - maxPendingHtlcs: 10 # Default maximum pending HTLCs per peer - rateLimit: 1 # Minimum seconds between HTLCs (token bucket rate limit) -``` - -## Plugin Parameters - -| Parameter | Description | Default | -|-----------|-------------|---------| -| `nodes` | List of LN node names to apply Circuit Breaker to | Required | -| `mode` | Operating mode (`fail`, `queue`, or `queue_peer_initiated`) | `fail` | -| `maxPendingHtlcs` | Default maximum number of pending HTLCs per peer | `30` | -| `rateLimit` | Minimum interval in seconds between HTLCs | `0` (disabled) | -| `port` | Port to expose the Circuit Breaker UI on | `9235` | -| `trusted_peers` | Map of node pubkeys to their individual HTLC limits | `{}` | - -## Operating Modes - -- **fail**: Fail HTLCs when limits are exceeded. Minimizes liquidity lock-up but affects routing reputation. -- **queue**: Queue HTLCs when limits are exceeded, forwarding them when space becomes available. Penalizes upstream nodes for bad traffic. -- **queue_peer_initiated**: Queue only HTLCs from channels that the remote node initiated. Uses fail mode for channels we initiated. - -**WARNING**: Queue modes require LND 0.16+ with auto-fail support to prevent force-closes. - -## Accessing the UI - -After deploying, you can port-forward to access the Circuit Breaker UI: - -``` -$ kubectl port-forward pod/circuitbreaker-tank-0000 9235:9235 -``` - -Then open http://127.0.0.1:9235 in a browser to view and configure Circuit Breaker settings. - -## Advanced Configuration Example - -```yaml -plugins: - postDeploy: - circuitbreaker: - entrypoint: "../../plugins/circuitbreaker" - nodes: ["tank-0000-ln", "tank-0003-ln"] - mode: "fail" - maxPendingHtlcs: 15 - rateLimit: 0.5 - trusted_peers: { - "03abcdef...": 50, - "02123456...": 100 - } -``` - - - -## Limitations - -- Circuit Breaker is alpha quality software. Use with caution, especially on mainnet. -- LND interfaces are not optimized for this purpose, which may lead to edge cases. -- Queue modes require LND 0.16+ to prevent channel force-closes. - -## Development - -To build your own version of the Circuit Breaker plugin: - -1. Clone the Circuit Breaker repository: `git clone https://github.com/lightningequipment/circuitbreaker.git` -2. Follow the build instructions in the repository -3. Update the plugin's `values.yaml` to point to your custom image \ No newline at end of file diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/.helmignore b/resources/plugins/circuitbreaker/charts/circuitbreaker/.helmignore deleted file mode 100644 index 0e8a0eb36..000000000 --- a/resources/plugins/circuitbreaker/charts/circuitbreaker/.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/plugins/circuitbreaker/charts/circuitbreaker/Chart.yaml b/resources/plugins/circuitbreaker/charts/circuitbreaker/Chart.yaml deleted file mode 100644 index 6cb99f1b8..000000000 --- a/resources/plugins/circuitbreaker/charts/circuitbreaker/Chart.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: v2 -name: circuitbreaker -description: A Helm chart to deploy Circuit Breaker -version: 0.1.0 -appVersion: "0.1.0" \ No newline at end of file diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/_helpers.tpl b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/_helpers.tpl deleted file mode 100644 index a699083e5..000000000 --- a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/_helpers.tpl +++ /dev/null @@ -1,7 +0,0 @@ -{{- define "mychart.name" -}} -{{- .Chart.Name | trunc 63 | trimSuffix "-" -}} -{{- end -}} - -{{- define "mychart.fullname" -}} -{{- printf "%s-%s" (include "mychart.name" .) .Release.Name | trunc 63 | trimSuffix "-" -}} -{{- end -}} diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/configmap.yaml b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/configmap.yaml deleted file mode 100644 index 3216c79ea..000000000 --- a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/configmap.yaml +++ /dev/null @@ -1,23 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ include "mychart.fullname" . }}-data -data: - tls.cert: | - -----BEGIN CERTIFICATE----- - MIICRTCCAeygAwIBAgIRAMe5IfFsBM9nqG1hwA1tswAwCgYIKoZIzj0EAwIwOzEf - MB0GA1UEChMWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEYMBYGA1UEAxMPREVTS1RP - UC00OEJVR0xTMB4XDTI1MDIwMTE2NDAyNFoXDTI2MDMyOTE2NDAyNFowOzEfMB0G - A1UEChMWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEYMBYGA1UEAxMPREVTS1RPUC00 - OEJVR0xTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIl4bWvtGVb1T4iUyjLfj - U2IVnF1yJBwbTa2diRJh+a0UbwjUSdn/hIVkNALr9f3NKYWmotyq8IGOmjwhAFis - HKOB0DCBzTAOBgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYD - VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU38wWLmz1lsVv7vtZZamSgkcoQUcwdgYD - VR0RBG8wbYIPREVTS1RPUC00OEJVR0xTgglsb2NhbGhvc3SCBHVuaXiCCnVuaXhw - YWNrZXSCB2J1ZmNvbm6HBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAGHBAr///6HBKwd - IqaHEP6AAAAAAAAAAhVd//6GM+MwCgYIKoZIzj0EAwIDRwAwRAIgNe9zoH9iz7Tw - 1j8+Jk05DU6nJ48a5mbP0viZ50UGu7sCIEK0AoPBrqxnicdhEEInONWyIm5VUR/l - YURZZyNuJ8lJ - -----END CERTIFICATE----- - admin.macaroon.hex: | - 0201036C6E6402F801030A107EC4D3E96DE93FA58F70968A1729AE6C1201301A160A0761646472657373120472656164120577726974651A130A04696E666F120472656164120577726974651A170A08696E766F69636573120472656164120577726974651A210A086D616361726F6F6E120867656E6572617465120472656164120577726974651A160A076D657373616765120472656164120577726974651A170A086F6666636861696E120472656164120577726974651A160A076F6E636861696E120472656164120577726974651A140A057065657273120472656164120577726974651A180A067369676E6572120867656E65726174651204726561640000062023AFC3BF7DB1D186342905D79461793FFCB59F583858F495C253F0A1EB4D33C2 diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/pod.yaml b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/pod.yaml deleted file mode 100644 index 459371b33..000000000 --- a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/pod.yaml +++ /dev/null @@ -1,46 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: {{ include "mychart.fullname" . }} - labels: - app: {{ include "mychart.name" . }} - mission: {{ .Values.name }} -spec: - initContainers: - - name: "init" - image: "busybox" - command: - - "sh" - - "-c" - args: - - > - mkdir -p /shared/.lnd/data/chain/bitcoin/mainnet && - cp /configmap/tls.cert /shared/.lnd/tls.cert && - cat /configmap/admin.macaroon.hex | xxd -r -p > /shared/.lnd/data/chain/bitcoin/mainnet/admin.macaroon - volumeMounts: - - name: shared-volume - mountPath: /shared - - name: configmap-volume - mountPath: /configmap - containers: - - name: {{ .Values.name }} - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - command: - - "sh" - - "-c" - args: - - > - mkdir -p /root/.lnd/data/chain/bitcoin/mainnet && - ln -s /shared/.lnd/tls.cert /root/.lnd/tls.cert && - ln -s /shared/.lnd/data/chain/bitcoin/mainnet/admin.macaroon /root/.lnd/data/chain/bitcoin/mainnet/admin.macaroon && - circuitbreaker --rpcserver={{ .Values.lnd.rpcserver }} --httplisten={{ .Values.lnd.httplisten }} - volumeMounts: - - name: shared-volume - mountPath: /shared - volumes: - - name: configmap-volume - configMap: - name: {{ include "mychart.fullname" . }}-data - - name: shared-volume - emptyDir: {} \ No newline at end of file diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/service.yaml b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/service.yaml deleted file mode 100644 index 42d17b602..000000000 --- a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/service.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ include "mychart.fullname" . }}-service - labels: - app: {{ include "mychart.name" . }} -spec: - type: NodePort - ports: - - port: 9235 - targetPort: 9235 - nodePort: 30000 # Choose a port between 30000-32767 - selector: - app: {{ include "mychart.name" . }} \ No newline at end of file diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/values.yaml b/resources/plugins/circuitbreaker/charts/circuitbreaker/values.yaml deleted file mode 100644 index e052a8f91..000000000 --- a/resources/plugins/circuitbreaker/charts/circuitbreaker/values.yaml +++ /dev/null @@ -1,14 +0,0 @@ -name: "circuitbreaker" -image: - repository: "camillarhi/circuitbreaker" - tag: "latest" - pullPolicy: IfNotPresent -workingVolume: - name: working-volume - mountPath: /working -configmapVolume: - name: configmap-volume - mountPath: /configmap -lnd: - rpcserver: "172.29.34.166:10009" # Default LND RPC server address - httplisten: "0.0.0.0:9235" # Default HTTP listen address \ No newline at end of file diff --git a/resources/plugins/circuitbreaker/plugin.py b/resources/plugins/circuitbreaker/plugin.py deleted file mode 100644 index 6700c0b88..000000000 --- a/resources/plugins/circuitbreaker/plugin.py +++ /dev/null @@ -1,204 +0,0 @@ -#!/usr/bin/env python3 -import json -import logging -from enum import Enum -from pathlib import Path -import subprocess -import time -from typing import Optional - -import click - -from warnet.constants import PLUGIN_ANNEX, AnnexMember, HookValue, WarnetContent -from warnet.process import run_command - -from warnet.k8s import ( - download, - get_default_namespace, - get_mission, - get_static_client, - wait_for_init, - write_file_to_container, -) - -MISSION = "circuitbreaker" -PRIMARY_CONTAINER = MISSION - -PLUGIN_DIR_TAG = "plugin_dir" - -class PluginError(Exception): - pass - -log = logging.getLogger(MISSION) -if not log.hasHandlers(): - console_handler = logging.StreamHandler() - console_handler.setLevel(logging.DEBUG) - formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") - console_handler.setFormatter(formatter) - log.addHandler(console_handler) -log.setLevel(logging.DEBUG) -log.propagate = True - -class PluginContent(Enum): - POD_NAME = "podName" - LND_RPC_SERVER = "rpcserver" - HTTP_LISTEN = "httplisten" - -@click.group() -@click.pass_context -def circuitbreaker(ctx): - """Commands for the Circuit Breaker plugin""" - ctx.ensure_object(dict) - plugin_dir = Path(__file__).resolve().parent - ctx.obj[PLUGIN_DIR_TAG] = Path(plugin_dir) - - -@circuitbreaker.command() -@click.argument("plugin_content", type=str) -@click.argument("warnet_content", type=str) -@click.pass_context -def entrypoint(ctx, plugin_content: str, warnet_content: str): - """Plugin entrypoint""" - plugin_content: dict = json.loads(plugin_content) - warnet_content: dict = json.loads(warnet_content) - - hook_value = warnet_content.get(WarnetContent.HOOK_VALUE.value) - - assert hook_value in { - item.value for item in HookValue - }, f"{hook_value} is not a valid HookValue" - - if warnet_content.get(PLUGIN_ANNEX): - for annex_member in [annex_item for annex_item in warnet_content.get(PLUGIN_ANNEX)]: - assert annex_member in { - item.value for item in AnnexMember - }, f"{annex_member} is not a valid AnnexMember" - - warnet_content[WarnetContent.HOOK_VALUE.value] = HookValue(hook_value) - - _entrypoint(ctx, plugin_content, warnet_content) - - -def _entrypoint(ctx, plugin_content: dict, warnet_content: dict): - """Called by entrypoint""" - hook_value = warnet_content[WarnetContent.HOOK_VALUE.value] - - match hook_value: - case HookValue.POST_DEPLOY: - data = get_data(plugin_content) - if data: - _launch_pod(ctx, install_name="circuitbreaker", **data) - else: - _launch_pod(ctx, install_name="circuitbreaker") - case _: - log.info(f"No action required for hook {hook_value}") - -def get_data(plugin_content: dict) -> Optional[dict]: - data = { - key: plugin_content.get(key) - for key in (PluginContent.POD_NAME.value, PluginContent.LND_RPC_SERVER.value, PluginContent.HTTP_LISTEN.value) - if plugin_content.get(key) - } - return data or None - -# def _create_secrets(): -# """Use local LND files for testing""" -# log.info("Using local LND files for testing") -# tls_cert_path = Path.home() / ".lnd" / "tls.cert" -# admin_macaroon_path = Path.home() / ".lnd" / "data" / "chain" / "bitcoin" / "signet" / "admin.macaroon" - -# if not tls_cert_path.exists(): -# raise PluginError(f"TLS certificate not found at {tls_cert_path}") -# if not admin_macaroon_path.exists(): -# raise PluginError(f"Admin macaroon not found at {admin_macaroon_path}") - -# log.info(f"Using TLS certificate: {tls_cert_path}") -# log.info(f"Using admin macaroon: {admin_macaroon_path}") - -# def _create_secrets(): -# """Create Kubernetes secrets for each LND node""" -# lnd_pods = subprocess.check_output(["kubectl", "get", "pods", "-l", "mission=lightning", "-o", "name"]).decode().splitlines() -# # lnd_pods = subprocess.check_output(["kubectl", "get", "pods", "-l", "app=warnet", "-l", "mission=lightning", "-o", "name"]).decode().splitlines() -# for node in lnd_pods: -# node_name = node.split('/')[-1] -# log.info(f"Waiting for {node_name} to be ready...") -# wait_for_init(node_name, namespace=get_default_namespace(), quiet=True) -# log.info(f"Creating secrets for {node_name}") -# subprocess.run(["kubectl", "cp", f"{node}:/root/.lnd/tls.cert", "./tls.cert"], check=True) -# subprocess.run(["kubectl", "cp", f"{node}:/root/.lnd/data/chain/bitcoin/regtest/admin.macaroon", "./admin.macaroon"], check=True) -# subprocess.run(["kubectl", "create", "secret", "generic", f"lnd-tls-cert-{node_name}", "--from-file=tls.cert=./tls.cert"], check=True) -# subprocess.run(["kubectl", "create", "secret", "generic", f"lnd-macaroon-{node_name}", "--from-file=admin.macaroon=./admin.macaroon"], check=True) - -def _create_secrets(): - """Create Kubernetes secrets for each LND node""" - lnd_pods = subprocess.check_output( - ["kubectl", "get", "pods", "-l", "mission=lightning", "-o", "name"] - ).decode().splitlines() - - for node in lnd_pods: - node_name = node.split('/')[-1] - log.info(f"Waiting for {node_name} to be ready...") - - # Wait for the pod to be ready - max_retries = 10 - retry_delay = 10 # seconds - for attempt in range(max_retries): - try: - # Check if the pod is ready - pod_status = subprocess.check_output( - ["kubectl", "get", "pod", node_name, "-o", "jsonpath='{.status.phase}'"] - ).decode().strip("'") - - if pod_status == "Running": - log.info(f"{node_name} is ready.") - break - else: - log.info(f"{node_name} is not ready yet (status: {pod_status}). Retrying in {retry_delay} seconds...") - except subprocess.CalledProcessError as e: - log.error(f"Failed to check pod status for {node_name}: {e}") - if attempt == max_retries - 1: - raise PluginError(f"Pod {node_name} did not become ready after {max_retries} attempts.") - - time.sleep(retry_delay) - - # Create secrets for the pod - log.info(f"Creating secrets for {node_name}") - try: - subprocess.run( - ["kubectl", "cp", f"{node_name}:/root/.lnd/tls.cert", "./tls.cert"], - check=True - ) - subprocess.run( - ["kubectl", "cp", f"{node_name}:/root/.lnd/data/chain/bitcoin/regtest/admin.macaroon", "./admin.macaroon"], - check=True - ) - subprocess.run( - ["kubectl", "create", "secret", "generic", f"lnd-tls-cert-{node_name}", "--from-file=tls.cert=./tls.cert"], - check=True - ) - subprocess.run( - ["kubectl", "create", "secret", "generic", f"lnd-macaroon-{node_name}", "--from-file=admin.macaroon=./admin.macaroon"], - check=True - ) - except subprocess.CalledProcessError as e: - log.error(f"Failed to create secrets for {node_name}: {e}") - raise PluginError(f"Failed to create secrets for {node_name}.") - -def _launch_pod(ctx, - install_name: str = "circuitbreaker", - podName: str = "circuitbreaker-pod", - rpcserver: str = "localhost:10009", - httplisten: str = "0.0.0.0:9235"): - timestamp = int(time.time()) - # release_name = f"cb-{install_name}" - - command = ( - f"helm upgrade --install {install_name} {ctx.obj[PLUGIN_DIR_TAG]}/charts/circuitbreaker " - f"--set podName={podName} --set rpcserver={rpcserver} --set httplisten={httplisten}" - ) - - log.info(command) - log.info(run_command(command)) - -if __name__ == "__main__": - circuitbreaker() From 51e2f617c7ddc05b4061f12b1e30268ca5e7f778 Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Wed, 12 Mar 2025 15:43:00 +0100 Subject: [PATCH 11/44] template: add extracontainers for circuitbreaker --- resources/networks/hello/network.yaml | 32 +++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/resources/networks/hello/network.yaml b/resources/networks/hello/network.yaml index 313126147..31cf58944 100644 --- a/resources/networks/hello/network.yaml +++ b/resources/networks/hello/network.yaml @@ -32,6 +32,38 @@ nodes: target: tank-0004-ln capacity: 100000 push_amt: 50000 + extraContainers: + - name: circuitbreaker + securityContext: + privileged: true + capabilities: + add: + - NET_ADMIN + - NET_RAW + image: camillarhi/circuitbreaker:latest + imagePullPolicy: IfNotPresent + args: + - "--network=regtest" + - "--rpcserver=localhost:10009" + - "--tlscertpath=/root/.lnd/tls_copy.cert" + - "--macaroonpath=/root/.lnd/data/chain/bitcoin/regtest/admin.macaroon" + - "--httplisten=0.0.0.0:9235" + volumeMounts: + - name: lnd-data + mountPath: /root/.lnd + readinessProbe: + exec: + command: + - /bin/sh + - -c + - | + # Check if tls.cert exists and is readable + test -f /root/.lnd/tls.cert && echo "tls.cert found" || exit 1 + # Check if admin.macaroon exists and is readable + test -f /root/.lnd/data/chain/bitcoin/regtest/admin.macaroon && echo "macaroon found" || exit 1 + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 1 - name: tank-0004 addnode: From 00ce51c39a7507fe83c502e9d03375e0ffcceb23 Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Wed, 12 Mar 2025 16:06:02 +0100 Subject: [PATCH 12/44] pod: update pod for extra containers --- .../bitcoincore/charts/lnd/templates/pod.yaml | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml b/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml index d08850a44..61fe5a215 100644 --- a/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml +++ b/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml @@ -21,21 +21,23 @@ spec: {{- end }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 4 }} + {{- with .Values.extraContainers }} initContainers: - - name: prepare-files - image: busybox - command: - - sh - - -c - - | - cp /root/.lnd/tls.cert /root/.lnd/tls_copy.cert - chmod 644 /root/.lnd/tls_copy.cert - volumeMounts: - - name: lnd-data - mountPath: /root/.lnd - - name: config - mountPath: /root/.lnd/tls.cert - subPath: tls.cert + - name: prepare-files + image: busybox + command: + - sh + - -c + - | + cp /root/.lnd/tls.cert /root/.lnd/tls_copy.cert + chmod 644 /root/.lnd/tls_copy.cert + volumeMounts: + - name: lnd-data + mountPath: /root/.lnd + - name: config + mountPath: /root/.lnd/tls.cert + subPath: tls.cert + {{- end }} containers: - name: {{ .Chart.Name }} securityContext: From 8cecc148050fa99c1bbaab9ed6615870f1e33424 Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Tue, 18 Mar 2025 00:26:32 +0100 Subject: [PATCH 13/44] update volume mounts --- .../charts/lnd/templates/configmap.yaml | 13 +++++++- .../bitcoincore/charts/lnd/templates/pod.yaml | 28 +++++++++-------- resources/networks/hello/network.yaml | 30 +++++-------------- 3 files changed, 35 insertions(+), 36 deletions(-) diff --git a/resources/charts/bitcoincore/charts/lnd/templates/configmap.yaml b/resources/charts/bitcoincore/charts/lnd/templates/configmap.yaml index 096c764ff..a47be0d52 100644 --- a/resources/charts/bitcoincore/charts/lnd/templates/configmap.yaml +++ b/resources/charts/bitcoincore/charts/lnd/templates/configmap.yaml @@ -38,7 +38,18 @@ data: AwEHoUQDQgAEBVltIvaTlAQI/3FFatTqVflZuZdRJ0SmRMSJrFLPtp0fxE7hmteS t6gjQriy90fP8j9OJXBNAjt915kLY4zVvg== -----END EC PRIVATE KEY----- - MACAROON_HEX: 0201036c6e6402f801030a1062beabbf2a614b112128afa0c0b4fdd61201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e657261746512047265616400000620b17be53e367290871681055d0de15587f6d1cd47d1248fe2662ae27f62cfbdc6 + + MACAROON_HEX: | + 0201036c6e6402f801030a1062beabbf2a614b112128afa0c0b4fdd61201301a160a0 + 761646472657373120472656164120577726974651a130a04696e666f120472656164 + 120577726974651a170a08696e766f69636573120472656164120577726974651a210 + a086d616361726f6f6e120867656e6572617465120472656164120577726974651a16 + 0a076d657373616765120472656164120577726974651a170a086f6666636861696e1 + 20472656164120577726974651a160a076f6e636861696e1204726561641205777269 + 74651a140a057065657273120472656164120577726974651a180a067369676e65721 + 20867656e657261746512047265616400000620b17be53e367290871681055d0de155 + 87f6d1cd47d1248fe2662ae27f62cfbdc6 + --- apiVersion: v1 kind: ConfigMap diff --git a/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml b/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml index 61fe5a215..339b0c59e 100644 --- a/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml +++ b/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml @@ -21,23 +21,27 @@ spec: {{- end }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 4 }} - {{- with .Values.extraContainers }} initContainers: - name: prepare-files image: busybox command: - sh - -c - - | - cp /root/.lnd/tls.cert /root/.lnd/tls_copy.cert - chmod 644 /root/.lnd/tls_copy.cert + - | + # Read the hex-encoded macaroon from the ConfigMap + HEX_MACAROON=$(cat /config/MACAROON_HEX) + + # Convert the hex-encoded macaroon to binary format + echo "$HEX_MACAROON" | xxd -r -p > /shared-data/admin.macaroon + + # Ensure the file has the correct permissions + chmod 644 /shared-data/admin.macaroon volumeMounts: - - name: lnd-data - mountPath: /root/.lnd + - name: shared-data + mountPath: /shared-data - name: config - mountPath: /root/.lnd/tls.cert - subPath: tls.cert - {{- end }} + mountPath: /config + containers: - name: {{ .Chart.Name }} securityContext: @@ -63,8 +67,8 @@ spec: resources: {{- toYaml .Values.resources | nindent 8 }} volumeMounts: - - name: lnd-data - mountPath: /root/.lnd + - mountPath: /shared-data + name: shared-data {{- with .Values.volumeMounts }} {{- toYaml . | nindent 8 }} {{- end }} @@ -81,7 +85,7 @@ spec: {{- toYaml . | nindent 4 }} {{- end }} volumes: - - name: lnd-data + - name: shared-data emptyDir: {} {{- with .Values.volumes }} {{- toYaml . | nindent 4 }} diff --git a/resources/networks/hello/network.yaml b/resources/networks/hello/network.yaml index 31cf58944..bad79bd53 100644 --- a/resources/networks/hello/network.yaml +++ b/resources/networks/hello/network.yaml @@ -34,36 +34,20 @@ nodes: push_amt: 50000 extraContainers: - name: circuitbreaker - securityContext: - privileged: true - capabilities: - add: - - NET_ADMIN - - NET_RAW image: camillarhi/circuitbreaker:latest imagePullPolicy: IfNotPresent args: - "--network=regtest" - "--rpcserver=localhost:10009" - - "--tlscertpath=/root/.lnd/tls_copy.cert" - - "--macaroonpath=/root/.lnd/data/chain/bitcoin/regtest/admin.macaroon" + - "--tlscertpath=/tls.cert" + - "--macaroonpath=/shared-data/admin.macaroon" - "--httplisten=0.0.0.0:9235" volumeMounts: - - name: lnd-data - mountPath: /root/.lnd - readinessProbe: - exec: - command: - - /bin/sh - - -c - - | - # Check if tls.cert exists and is readable - test -f /root/.lnd/tls.cert && echo "tls.cert found" || exit 1 - # Check if admin.macaroon exists and is readable - test -f /root/.lnd/data/chain/bitcoin/regtest/admin.macaroon && echo "macaroon found" || exit 1 - initialDelaySeconds: 10 - periodSeconds: 5 - timeoutSeconds: 1 + - name: shared-data + mountPath: /shared-data + - name: config + mountPath: /tls.cert + subPath: tls.cert - name: tank-0004 addnode: From 067d98e08039460a6d5a56ea1211709c09588936 Mon Sep 17 00:00:00 2001 From: Rita Anene <92169163+Camillarhi@users.noreply.github.com> Date: Wed, 12 Mar 2025 16:10:02 +0100 Subject: [PATCH 14/44] Circuitbreaker Update (#4) * plugin: remove circuit-breaker from the plugin section * template: add extracontainers for circuitbreaker * pod: update pod for extra containers --- resources/charts/bitcoincore/charts/lnd/templates/pod.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml b/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml index 339b0c59e..1a666d3d6 100644 --- a/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml +++ b/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml @@ -21,6 +21,7 @@ spec: {{- end }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 4 }} + {{- with .Values.extraContainers }} initContainers: - name: prepare-files image: busybox @@ -84,6 +85,9 @@ spec: {{- with .Values.extraContainers }} {{- toYaml . | nindent 4 }} {{- end }} + {{- with .Values.extraContainers }} + {{- toYaml . | nindent 4 }} + {{- end }} volumes: - name: shared-data emptyDir: {} From f2f23d7cadfafd7c4ba776f7de4c00f92935288b Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Tue, 18 Mar 2025 01:57:59 +0100 Subject: [PATCH 15/44] update init containers --- resources/charts/bitcoincore/charts/lnd/templates/pod.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml b/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml index 1a666d3d6..ace991e61 100644 --- a/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml +++ b/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml @@ -21,14 +21,13 @@ spec: {{- end }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 4 }} - {{- with .Values.extraContainers }} initContainers: - name: prepare-files image: busybox command: - sh - -c - - | + - | # Read the hex-encoded macaroon from the ConfigMap HEX_MACAROON=$(cat /config/MACAROON_HEX) From a2f25aac353682fe2b0e75a7c481d451dc743163 Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Wed, 19 Mar 2025 02:14:04 +0100 Subject: [PATCH 16/44] update shared volume from `values.yaml` --- .../bitcoincore/charts/lnd/templates/pod.yaml | 13 +++++++------ resources/charts/bitcoincore/charts/lnd/values.yaml | 9 +++++++++ resources/networks/hello/network.yaml | 2 +- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml b/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml index ace991e61..843534730 100644 --- a/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml +++ b/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml @@ -21,6 +21,7 @@ spec: {{- end }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 4 }} + {{- if .Values.enableInitContainers }} initContainers: - name: prepare-files image: busybox @@ -37,11 +38,11 @@ spec: # Ensure the file has the correct permissions chmod 644 /shared-data/admin.macaroon volumeMounts: - - name: shared-data - mountPath: /shared-data + - name: {{ .Values.sharedVolume.name }} + mountPath: {{ .Values.sharedVolume.mountPath }} - name: config mountPath: /config - + {{- end }} containers: - name: {{ .Chart.Name }} securityContext: @@ -67,8 +68,8 @@ spec: resources: {{- toYaml .Values.resources | nindent 8 }} volumeMounts: - - mountPath: /shared-data - name: shared-data + - mountPath: {{ .Values.sharedVolume.mountPath }} + name: {{ .Values.sharedVolume.name }} {{- with .Values.volumeMounts }} {{- toYaml . | nindent 8 }} {{- end }} @@ -88,7 +89,7 @@ spec: {{- toYaml . | nindent 4 }} {{- end }} volumes: - - name: shared-data + - name: {{ .Values.sharedVolume.name }} emptyDir: {} {{- with .Values.volumes }} {{- toYaml . | nindent 4 }} diff --git a/resources/charts/bitcoincore/charts/lnd/values.yaml b/resources/charts/bitcoincore/charts/lnd/values.yaml index d56e65bf4..5b4ea51e3 100644 --- a/resources/charts/bitcoincore/charts/lnd/values.yaml +++ b/resources/charts/bitcoincore/charts/lnd/values.yaml @@ -132,3 +132,12 @@ config: "" defaultConfig: "" channels: [] + +# Controls whether the initContainers should be included +enableInitContainers: true + +# Shared volume configuration for storing shared data (e.g., macaroons or other files) +# This volume is mounted at the specified mountPath and can be used for inter-container communication. +sharedVolume: + name: shared-data + mountPath: /shared-data diff --git a/resources/networks/hello/network.yaml b/resources/networks/hello/network.yaml index bad79bd53..be4b7d316 100644 --- a/resources/networks/hello/network.yaml +++ b/resources/networks/hello/network.yaml @@ -48,7 +48,7 @@ nodes: - name: config mountPath: /tls.cert subPath: tls.cert - + - name: tank-0004 addnode: - tank-0000 From aef4f9c89901beb536a6bb89a4e6731985feaebb Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Thu, 27 Feb 2025 02:42:24 +0100 Subject: [PATCH 17/44] circuit breaker plugin readme --- resources/plugins/circuitbreaker/README.md | 151 +++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 resources/plugins/circuitbreaker/README.md diff --git a/resources/plugins/circuitbreaker/README.md b/resources/plugins/circuitbreaker/README.md new file mode 100644 index 000000000..5fb1e7f9c --- /dev/null +++ b/resources/plugins/circuitbreaker/README.md @@ -0,0 +1,151 @@ +# Circuit Breaker Plugin + +## Overview +The Circuit Breaker plugin integrates the [circuitbreaker](https://github.com/lightningequipment/circuitbreaker) tool with Warnet to protect Lightning Network nodes from being flooded with HTLCs. Circuit Breaker functions like a firewall for Lightning, allowing node operators to set limits on in-flight HTLCs and implement rate limiting on a per-peer basis. + +## What is Circuit Breaker? +Circuit Breaker is to Lightning what firewalls are to the internet. It provides protection against: +- HTLC flooding attacks +- Channel slot exhaustion (max 483 slots per channel) +- DoS/spam attacks using large numbers of fast-resolving HTLCs +- Channel balance probing attacks + +Circuit Breaker offers insights into HTLC traffic and provides configurable operating modes to handle excess traffic. + +## Usage +In your Python virtual environment with Warnet installed and set up, create a new Warnet user folder: + +``` +$ warnet new user_folder +$ cd user_folder +``` + +Deploy a network with Circuit Breaker enabled: + +``` +$ warnet deploy networks/circuitbreaker +``` + +## Configuration in `network.yaml` +You can incorporate the Circuit Breaker plugin into your `network.yaml` file as shown below: + +```yaml +nodes: + - name: tank-0000 + addnode: + - tank-0001 + ln: + lnd: true + + - name: tank-0001 + addnode: + - tank-0002 + ln: + lnd: true + + - name: tank-0002 + addnode: + - tank-0000 + ln: + lnd: true + + - name: tank-0003 + addnode: + - tank-0000 + ln: + lnd: true + lnd: + channels: + - id: + block: 300 + index: 1 + target: tank-0004-ln + capacity: 100000 + push_amt: 50000 + +plugins: + postDeploy: + circuitbreaker: + entrypoint: "../../plugins/circuitbreaker" + nodes: ["tank-0000-ln", "tank-0003-ln"] # Nodes to apply Circuit Breaker to + mode: "fail" # Operating mode: fail, queue, or queue_peer_initiated + maxPendingHtlcs: 10 # Default maximum pending HTLCs per peer + rateLimit: 1 # Minimum seconds between HTLCs (token bucket rate limit) +``` + +## Plugin Parameters + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `nodes` | List of LN node names to apply Circuit Breaker to | Required | +| `mode` | Operating mode (`fail`, `queue`, or `queue_peer_initiated`) | `fail` | +| `maxPendingHtlcs` | Default maximum number of pending HTLCs per peer | `30` | +| `rateLimit` | Minimum interval in seconds between HTLCs | `0` (disabled) | +| `port` | Port to expose the Circuit Breaker UI on | `9235` | +| `trusted_peers` | Map of node pubkeys to their individual HTLC limits | `{}` | + +## Operating Modes + +- **fail**: Fail HTLCs when limits are exceeded. Minimizes liquidity lock-up but affects routing reputation. +- **queue**: Queue HTLCs when limits are exceeded, forwarding them when space becomes available. Penalizes upstream nodes for bad traffic. +- **queue_peer_initiated**: Queue only HTLCs from channels that the remote node initiated. Uses fail mode for channels we initiated. + +**WARNING**: Queue modes require LND 0.16+ with auto-fail support to prevent force-closes. + +## Accessing the UI + +After deploying, you can port-forward to access the Circuit Breaker UI: + +``` +$ kubectl port-forward pod/circuitbreaker-tank-0000 9235:9235 +``` + +Then open http://127.0.0.1:9235 in a browser to view and configure Circuit Breaker settings. + +## Advanced Configuration Example + +```yaml +plugins: + postDeploy: + circuitbreaker: + entrypoint: "../../plugins/circuitbreaker" + nodes: ["tank-0000-ln", "tank-0003-ln"] + mode: "fail" + maxPendingHtlcs: 15 + rateLimit: 0.5 + trusted_peers: { + "03abcdef...": 50, + "02123456...": 100 + } +``` + + + +## Limitations + +- Circuit Breaker is alpha quality software. Use with caution, especially on mainnet. +- LND interfaces are not optimized for this purpose, which may lead to edge cases. +- Queue modes require LND 0.16+ to prevent channel force-closes. + +## Development + +To build your own version of the Circuit Breaker plugin: + +1. Clone the Circuit Breaker repository: `git clone https://github.com/lightningequipment/circuitbreaker.git` +2. Follow the build instructions in the repository +3. Update the plugin's `values.yaml` to point to your custom image \ No newline at end of file From 0418553e5fed32e6683fc5f4531e203b1a6b15d3 Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Thu, 27 Feb 2025 02:42:49 +0100 Subject: [PATCH 18/44] circuit breaker plugin skeleton setup --- .../charts/circuitbreaker/.helmignore | 23 ++++ .../charts/circuitbreaker/Chart.yaml | 5 + .../circuitbreaker/templates/_helpers.tpl | 7 ++ .../charts/circuitbreaker/templates/pod.yaml | 17 +++ .../charts/circuitbreaker/values.yaml | 5 + resources/plugins/circuitbreaker/plugin.py | 117 ++++++++++++++++++ 6 files changed, 174 insertions(+) create mode 100644 resources/plugins/circuitbreaker/charts/circuitbreaker/.helmignore create mode 100644 resources/plugins/circuitbreaker/charts/circuitbreaker/Chart.yaml create mode 100644 resources/plugins/circuitbreaker/charts/circuitbreaker/templates/_helpers.tpl create mode 100644 resources/plugins/circuitbreaker/charts/circuitbreaker/templates/pod.yaml create mode 100644 resources/plugins/circuitbreaker/charts/circuitbreaker/values.yaml create mode 100644 resources/plugins/circuitbreaker/plugin.py diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/.helmignore b/resources/plugins/circuitbreaker/charts/circuitbreaker/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/resources/plugins/circuitbreaker/charts/circuitbreaker/.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/plugins/circuitbreaker/charts/circuitbreaker/Chart.yaml b/resources/plugins/circuitbreaker/charts/circuitbreaker/Chart.yaml new file mode 100644 index 000000000..ab03ed1a3 --- /dev/null +++ b/resources/plugins/circuitbreaker/charts/circuitbreaker/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: circuitbreaker +description: A Helm chart to deploy Circuit Breaker +version: 0.1.0 +appVersion: "0.1.0" diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/_helpers.tpl b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/_helpers.tpl new file mode 100644 index 000000000..a699083e5 --- /dev/null +++ b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/_helpers.tpl @@ -0,0 +1,7 @@ +{{- define "mychart.name" -}} +{{- .Chart.Name | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "mychart.fullname" -}} +{{- printf "%s-%s" (include "mychart.name" .) .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/pod.yaml b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/pod.yaml new file mode 100644 index 000000000..a15a891f2 --- /dev/null +++ b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/pod.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{ include "mychart.fullname" . }} + labels: + app: {{ include "mychart.name" . }} + mission: {{ .Values.name }} +spec: + containers: + - name: {{ .Values.name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: ["sh", "-c"] + args: + - echo "Hello {{ .Values.mode }}"; + resources: {} + \ No newline at end of file diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/values.yaml b/resources/plugins/circuitbreaker/charts/circuitbreaker/values.yaml new file mode 100644 index 000000000..a439d37f7 --- /dev/null +++ b/resources/plugins/circuitbreaker/charts/circuitbreaker/values.yaml @@ -0,0 +1,5 @@ +name: "circuitbreaker" +image: + repository: "camillarhi/circuitbreaker" + tag: "0.2.3" + pullPolicy: IfNotPresent \ No newline at end of file diff --git a/resources/plugins/circuitbreaker/plugin.py b/resources/plugins/circuitbreaker/plugin.py new file mode 100644 index 000000000..07cb3bef1 --- /dev/null +++ b/resources/plugins/circuitbreaker/plugin.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +import json +import logging +from enum import Enum +from pathlib import Path +import time +from typing import Optional + +import click + +from warnet.constants import PLUGIN_ANNEX, AnnexMember, HookValue, WarnetContent +from warnet.process import run_command + +MISSION = "circuitbreaker" +PRIMARY_CONTAINER = MISSION + +PLUGIN_DIR_TAG = "plugin_dir" + + +class PluginError(Exception): + pass + + +log = logging.getLogger(MISSION) +if not log.hasHandlers(): + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.DEBUG) + formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + console_handler.setFormatter(formatter) + log.addHandler(console_handler) +log.setLevel(logging.DEBUG) +log.propagate = True + +class PluginContent(Enum): + MODE = "mode" + MAX_PENDING_HTLCS = "maxPendingHtlcs" + RATE_LIMIT = "rateLimit" + +@click.group() +@click.pass_context +def circuitbreaker(ctx): + """Commands for the Circuit Breaker plugin""" + ctx.ensure_object(dict) + plugin_dir = Path(__file__).resolve().parent + ctx.obj[PLUGIN_DIR_TAG] = Path(plugin_dir) + + +@circuitbreaker.command() +@click.argument("plugin_content", type=str) +@click.argument("warnet_content", type=str) +@click.pass_context +def entrypoint(ctx, plugin_content: str, warnet_content: str): + """Plugin entrypoint""" + plugin_content: dict = json.loads(plugin_content) + warnet_content: dict = json.loads(warnet_content) + + hook_value = warnet_content.get(WarnetContent.HOOK_VALUE.value) + + assert hook_value in { + item.value for item in HookValue + }, f"{hook_value} is not a valid HookValue" + + if warnet_content.get(PLUGIN_ANNEX): + for annex_member in [annex_item for annex_item in warnet_content.get(PLUGIN_ANNEX)]: + assert annex_member in { + item.value for item in AnnexMember + }, f"{annex_member} is not a valid AnnexMember" + + warnet_content[WarnetContent.HOOK_VALUE.value] = HookValue(hook_value) + + _entrypoint(ctx, plugin_content, warnet_content) + + +def _entrypoint(ctx, plugin_content: dict, warnet_content: dict): + """Called by entrypoint""" + hook_value = warnet_content[WarnetContent.HOOK_VALUE.value] + + match hook_value: + case ( + HookValue.PRE_NETWORK + | HookValue.POST_NETWORK + | HookValue.PRE_DEPLOY + | HookValue.POST_DEPLOY + ): + data = get_data(plugin_content) + if data: + _launch_circuit_breaker(ctx, node_name=hook_value.value.lower()) + else: + _launch_circuit_breaker(ctx, node_name=hook_value.value.lower()) + case HookValue.PRE_NODE: + name = warnet_content[PLUGIN_ANNEX][AnnexMember.NODE_NAME.value] + "-pre-pod" + _launch_circuit_breaker(ctx, node_name=hook_value.value.lower() + "-" + name) + case HookValue.POST_NODE: + name = warnet_content[PLUGIN_ANNEX][AnnexMember.NODE_NAME.value] + "-post-pod" + _launch_circuit_breaker(ctx, node_name=hook_value.value.lower() + "-" + name) + +def get_data(plugin_content: dict) -> Optional[dict]: + data = { + key: plugin_content.get(key) + for key in (PluginContent.MAX_PENDING_HTLCS.value, PluginContent.RATE_LIMIT.value) + if plugin_content.get(key) + } + return data or None + + +def _launch_circuit_breaker(ctx, node_name: str): + timestamp = int(time.time()) + release_name = f"cb-{node_name}-{timestamp}" + + command = f"helm upgrade --install {node_name} {ctx.obj[PLUGIN_DIR_TAG]}/charts/circuitbreaker --set node={node_name}" + + log.info(command) + log.info(run_command(command)) + + +if __name__ == "__main__": + circuitbreaker() From 91da2a1e9a8d0bc99335b08d6e7f2e56a4d2124c Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Wed, 19 Mar 2025 02:25:09 +0100 Subject: [PATCH 19/44] remove plugin implementation --- resources/plugins/circuitbreaker/README.md | 151 ------------------ .../charts/circuitbreaker/.helmignore | 23 --- .../charts/circuitbreaker/Chart.yaml | 5 - .../circuitbreaker/templates/_helpers.tpl | 7 - .../charts/circuitbreaker/templates/pod.yaml | 17 -- .../charts/circuitbreaker/values.yaml | 5 - resources/plugins/circuitbreaker/plugin.py | 117 -------------- 7 files changed, 325 deletions(-) delete mode 100644 resources/plugins/circuitbreaker/README.md delete mode 100644 resources/plugins/circuitbreaker/charts/circuitbreaker/.helmignore delete mode 100644 resources/plugins/circuitbreaker/charts/circuitbreaker/Chart.yaml delete mode 100644 resources/plugins/circuitbreaker/charts/circuitbreaker/templates/_helpers.tpl delete mode 100644 resources/plugins/circuitbreaker/charts/circuitbreaker/templates/pod.yaml delete mode 100644 resources/plugins/circuitbreaker/charts/circuitbreaker/values.yaml delete mode 100644 resources/plugins/circuitbreaker/plugin.py diff --git a/resources/plugins/circuitbreaker/README.md b/resources/plugins/circuitbreaker/README.md deleted file mode 100644 index 5fb1e7f9c..000000000 --- a/resources/plugins/circuitbreaker/README.md +++ /dev/null @@ -1,151 +0,0 @@ -# Circuit Breaker Plugin - -## Overview -The Circuit Breaker plugin integrates the [circuitbreaker](https://github.com/lightningequipment/circuitbreaker) tool with Warnet to protect Lightning Network nodes from being flooded with HTLCs. Circuit Breaker functions like a firewall for Lightning, allowing node operators to set limits on in-flight HTLCs and implement rate limiting on a per-peer basis. - -## What is Circuit Breaker? -Circuit Breaker is to Lightning what firewalls are to the internet. It provides protection against: -- HTLC flooding attacks -- Channel slot exhaustion (max 483 slots per channel) -- DoS/spam attacks using large numbers of fast-resolving HTLCs -- Channel balance probing attacks - -Circuit Breaker offers insights into HTLC traffic and provides configurable operating modes to handle excess traffic. - -## Usage -In your Python virtual environment with Warnet installed and set up, create a new Warnet user folder: - -``` -$ warnet new user_folder -$ cd user_folder -``` - -Deploy a network with Circuit Breaker enabled: - -``` -$ warnet deploy networks/circuitbreaker -``` - -## Configuration in `network.yaml` -You can incorporate the Circuit Breaker plugin into your `network.yaml` file as shown below: - -```yaml -nodes: - - name: tank-0000 - addnode: - - tank-0001 - ln: - lnd: true - - - name: tank-0001 - addnode: - - tank-0002 - ln: - lnd: true - - - name: tank-0002 - addnode: - - tank-0000 - ln: - lnd: true - - - name: tank-0003 - addnode: - - tank-0000 - ln: - lnd: true - lnd: - channels: - - id: - block: 300 - index: 1 - target: tank-0004-ln - capacity: 100000 - push_amt: 50000 - -plugins: - postDeploy: - circuitbreaker: - entrypoint: "../../plugins/circuitbreaker" - nodes: ["tank-0000-ln", "tank-0003-ln"] # Nodes to apply Circuit Breaker to - mode: "fail" # Operating mode: fail, queue, or queue_peer_initiated - maxPendingHtlcs: 10 # Default maximum pending HTLCs per peer - rateLimit: 1 # Minimum seconds between HTLCs (token bucket rate limit) -``` - -## Plugin Parameters - -| Parameter | Description | Default | -|-----------|-------------|---------| -| `nodes` | List of LN node names to apply Circuit Breaker to | Required | -| `mode` | Operating mode (`fail`, `queue`, or `queue_peer_initiated`) | `fail` | -| `maxPendingHtlcs` | Default maximum number of pending HTLCs per peer | `30` | -| `rateLimit` | Minimum interval in seconds between HTLCs | `0` (disabled) | -| `port` | Port to expose the Circuit Breaker UI on | `9235` | -| `trusted_peers` | Map of node pubkeys to their individual HTLC limits | `{}` | - -## Operating Modes - -- **fail**: Fail HTLCs when limits are exceeded. Minimizes liquidity lock-up but affects routing reputation. -- **queue**: Queue HTLCs when limits are exceeded, forwarding them when space becomes available. Penalizes upstream nodes for bad traffic. -- **queue_peer_initiated**: Queue only HTLCs from channels that the remote node initiated. Uses fail mode for channels we initiated. - -**WARNING**: Queue modes require LND 0.16+ with auto-fail support to prevent force-closes. - -## Accessing the UI - -After deploying, you can port-forward to access the Circuit Breaker UI: - -``` -$ kubectl port-forward pod/circuitbreaker-tank-0000 9235:9235 -``` - -Then open http://127.0.0.1:9235 in a browser to view and configure Circuit Breaker settings. - -## Advanced Configuration Example - -```yaml -plugins: - postDeploy: - circuitbreaker: - entrypoint: "../../plugins/circuitbreaker" - nodes: ["tank-0000-ln", "tank-0003-ln"] - mode: "fail" - maxPendingHtlcs: 15 - rateLimit: 0.5 - trusted_peers: { - "03abcdef...": 50, - "02123456...": 100 - } -``` - - - -## Limitations - -- Circuit Breaker is alpha quality software. Use with caution, especially on mainnet. -- LND interfaces are not optimized for this purpose, which may lead to edge cases. -- Queue modes require LND 0.16+ to prevent channel force-closes. - -## Development - -To build your own version of the Circuit Breaker plugin: - -1. Clone the Circuit Breaker repository: `git clone https://github.com/lightningequipment/circuitbreaker.git` -2. Follow the build instructions in the repository -3. Update the plugin's `values.yaml` to point to your custom image \ No newline at end of file diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/.helmignore b/resources/plugins/circuitbreaker/charts/circuitbreaker/.helmignore deleted file mode 100644 index 0e8a0eb36..000000000 --- a/resources/plugins/circuitbreaker/charts/circuitbreaker/.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/plugins/circuitbreaker/charts/circuitbreaker/Chart.yaml b/resources/plugins/circuitbreaker/charts/circuitbreaker/Chart.yaml deleted file mode 100644 index ab03ed1a3..000000000 --- a/resources/plugins/circuitbreaker/charts/circuitbreaker/Chart.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: v2 -name: circuitbreaker -description: A Helm chart to deploy Circuit Breaker -version: 0.1.0 -appVersion: "0.1.0" diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/_helpers.tpl b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/_helpers.tpl deleted file mode 100644 index a699083e5..000000000 --- a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/_helpers.tpl +++ /dev/null @@ -1,7 +0,0 @@ -{{- define "mychart.name" -}} -{{- .Chart.Name | trunc 63 | trimSuffix "-" -}} -{{- end -}} - -{{- define "mychart.fullname" -}} -{{- printf "%s-%s" (include "mychart.name" .) .Release.Name | trunc 63 | trimSuffix "-" -}} -{{- end -}} diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/pod.yaml b/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/pod.yaml deleted file mode 100644 index a15a891f2..000000000 --- a/resources/plugins/circuitbreaker/charts/circuitbreaker/templates/pod.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: {{ include "mychart.fullname" . }} - labels: - app: {{ include "mychart.name" . }} - mission: {{ .Values.name }} -spec: - containers: - - name: {{ .Values.name }} - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - command: ["sh", "-c"] - args: - - echo "Hello {{ .Values.mode }}"; - resources: {} - \ No newline at end of file diff --git a/resources/plugins/circuitbreaker/charts/circuitbreaker/values.yaml b/resources/plugins/circuitbreaker/charts/circuitbreaker/values.yaml deleted file mode 100644 index a439d37f7..000000000 --- a/resources/plugins/circuitbreaker/charts/circuitbreaker/values.yaml +++ /dev/null @@ -1,5 +0,0 @@ -name: "circuitbreaker" -image: - repository: "camillarhi/circuitbreaker" - tag: "0.2.3" - pullPolicy: IfNotPresent \ No newline at end of file diff --git a/resources/plugins/circuitbreaker/plugin.py b/resources/plugins/circuitbreaker/plugin.py deleted file mode 100644 index 07cb3bef1..000000000 --- a/resources/plugins/circuitbreaker/plugin.py +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/env python3 -import json -import logging -from enum import Enum -from pathlib import Path -import time -from typing import Optional - -import click - -from warnet.constants import PLUGIN_ANNEX, AnnexMember, HookValue, WarnetContent -from warnet.process import run_command - -MISSION = "circuitbreaker" -PRIMARY_CONTAINER = MISSION - -PLUGIN_DIR_TAG = "plugin_dir" - - -class PluginError(Exception): - pass - - -log = logging.getLogger(MISSION) -if not log.hasHandlers(): - console_handler = logging.StreamHandler() - console_handler.setLevel(logging.DEBUG) - formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") - console_handler.setFormatter(formatter) - log.addHandler(console_handler) -log.setLevel(logging.DEBUG) -log.propagate = True - -class PluginContent(Enum): - MODE = "mode" - MAX_PENDING_HTLCS = "maxPendingHtlcs" - RATE_LIMIT = "rateLimit" - -@click.group() -@click.pass_context -def circuitbreaker(ctx): - """Commands for the Circuit Breaker plugin""" - ctx.ensure_object(dict) - plugin_dir = Path(__file__).resolve().parent - ctx.obj[PLUGIN_DIR_TAG] = Path(plugin_dir) - - -@circuitbreaker.command() -@click.argument("plugin_content", type=str) -@click.argument("warnet_content", type=str) -@click.pass_context -def entrypoint(ctx, plugin_content: str, warnet_content: str): - """Plugin entrypoint""" - plugin_content: dict = json.loads(plugin_content) - warnet_content: dict = json.loads(warnet_content) - - hook_value = warnet_content.get(WarnetContent.HOOK_VALUE.value) - - assert hook_value in { - item.value for item in HookValue - }, f"{hook_value} is not a valid HookValue" - - if warnet_content.get(PLUGIN_ANNEX): - for annex_member in [annex_item for annex_item in warnet_content.get(PLUGIN_ANNEX)]: - assert annex_member in { - item.value for item in AnnexMember - }, f"{annex_member} is not a valid AnnexMember" - - warnet_content[WarnetContent.HOOK_VALUE.value] = HookValue(hook_value) - - _entrypoint(ctx, plugin_content, warnet_content) - - -def _entrypoint(ctx, plugin_content: dict, warnet_content: dict): - """Called by entrypoint""" - hook_value = warnet_content[WarnetContent.HOOK_VALUE.value] - - match hook_value: - case ( - HookValue.PRE_NETWORK - | HookValue.POST_NETWORK - | HookValue.PRE_DEPLOY - | HookValue.POST_DEPLOY - ): - data = get_data(plugin_content) - if data: - _launch_circuit_breaker(ctx, node_name=hook_value.value.lower()) - else: - _launch_circuit_breaker(ctx, node_name=hook_value.value.lower()) - case HookValue.PRE_NODE: - name = warnet_content[PLUGIN_ANNEX][AnnexMember.NODE_NAME.value] + "-pre-pod" - _launch_circuit_breaker(ctx, node_name=hook_value.value.lower() + "-" + name) - case HookValue.POST_NODE: - name = warnet_content[PLUGIN_ANNEX][AnnexMember.NODE_NAME.value] + "-post-pod" - _launch_circuit_breaker(ctx, node_name=hook_value.value.lower() + "-" + name) - -def get_data(plugin_content: dict) -> Optional[dict]: - data = { - key: plugin_content.get(key) - for key in (PluginContent.MAX_PENDING_HTLCS.value, PluginContent.RATE_LIMIT.value) - if plugin_content.get(key) - } - return data or None - - -def _launch_circuit_breaker(ctx, node_name: str): - timestamp = int(time.time()) - release_name = f"cb-{node_name}-{timestamp}" - - command = f"helm upgrade --install {node_name} {ctx.obj[PLUGIN_DIR_TAG]}/charts/circuitbreaker --set node={node_name}" - - log.info(command) - log.info(run_command(command)) - - -if __name__ == "__main__": - circuitbreaker() From 61da106464be614aee886f28b88e94eb56566bd0 Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Fri, 21 Mar 2025 02:45:27 +0100 Subject: [PATCH 20/44] Define new values in `network.yaml` file --- .../charts/bitcoincore/charts/lnd/templates/pod.yaml | 9 ++++++--- resources/charts/bitcoincore/charts/lnd/values.yaml | 9 --------- resources/networks/hello/network.yaml | 4 ++++ 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml b/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml index 843534730..a6cc6d645 100644 --- a/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml +++ b/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml @@ -38,8 +38,10 @@ spec: # Ensure the file has the correct permissions chmod 644 /shared-data/admin.macaroon volumeMounts: + {{- if .Values.sharedVolume }} - name: {{ .Values.sharedVolume.name }} mountPath: {{ .Values.sharedVolume.mountPath }} + {{- end }} - name: config mountPath: /config {{- end }} @@ -68,8 +70,10 @@ spec: resources: {{- toYaml .Values.resources | nindent 8 }} volumeMounts: + {{- if .Values.sharedVolume }} - mountPath: {{ .Values.sharedVolume.mountPath }} name: {{ .Values.sharedVolume.name }} + {{- end }} {{- with .Values.volumeMounts }} {{- toYaml . | nindent 8 }} {{- end }} @@ -85,12 +89,11 @@ spec: {{- with .Values.extraContainers }} {{- toYaml . | nindent 4 }} {{- end }} - {{- with .Values.extraContainers }} - {{- toYaml . | nindent 4 }} - {{- end }} volumes: + {{- if .Values.sharedVolume }} - name: {{ .Values.sharedVolume.name }} emptyDir: {} + {{- end }} {{- with .Values.volumes }} {{- toYaml . | nindent 4 }} {{- end }} diff --git a/resources/charts/bitcoincore/charts/lnd/values.yaml b/resources/charts/bitcoincore/charts/lnd/values.yaml index 5b4ea51e3..d56e65bf4 100644 --- a/resources/charts/bitcoincore/charts/lnd/values.yaml +++ b/resources/charts/bitcoincore/charts/lnd/values.yaml @@ -132,12 +132,3 @@ config: "" defaultConfig: "" channels: [] - -# Controls whether the initContainers should be included -enableInitContainers: true - -# Shared volume configuration for storing shared data (e.g., macaroons or other files) -# This volume is mounted at the specified mountPath and can be used for inter-container communication. -sharedVolume: - name: shared-data - mountPath: /shared-data diff --git a/resources/networks/hello/network.yaml b/resources/networks/hello/network.yaml index be4b7d316..866b73a49 100644 --- a/resources/networks/hello/network.yaml +++ b/resources/networks/hello/network.yaml @@ -32,6 +32,10 @@ nodes: target: tank-0004-ln capacity: 100000 push_amt: 50000 + sharedVolume: # Shared volume configuration for storing shared data (e.g., macaroons or other files). This volume is mounted at the specified mountPath and can be used for inter-container communication. + name: shared-data + mountPath: /shared-data + enableInitContainers: true # Controls whether the initContainers should be included in the pod spec extraContainers: - name: circuitbreaker image: camillarhi/circuitbreaker:latest From 857282400d89ff2942c47997b552e74fa5879de8 Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Sat, 22 Mar 2025 23:42:57 +0100 Subject: [PATCH 21/44] update line endings --- resources/networks/hello/network.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/networks/hello/network.yaml b/resources/networks/hello/network.yaml index 866b73a49..444bc0681 100644 --- a/resources/networks/hello/network.yaml +++ b/resources/networks/hello/network.yaml @@ -104,4 +104,5 @@ plugins: # Each plugin section has a number of hooks available (preDeploy, post hello: entrypoint: "../../plugins/hello" helloTo: "postNetwork!" - podName: "hello-post-network" \ No newline at end of file + podName: "hello-post-network" + \ No newline at end of file From 58257a9362c59deda7b944cba337246af68b5c8a Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Thu, 27 Mar 2025 00:45:58 +0100 Subject: [PATCH 22/44] update macaroon path to use hex value --- resources/networks/hello/network.yaml | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/resources/networks/hello/network.yaml b/resources/networks/hello/network.yaml index 444bc0681..66ab9e9a1 100644 --- a/resources/networks/hello/network.yaml +++ b/resources/networks/hello/network.yaml @@ -32,10 +32,6 @@ nodes: target: tank-0004-ln capacity: 100000 push_amt: 50000 - sharedVolume: # Shared volume configuration for storing shared data (e.g., macaroons or other files). This volume is mounted at the specified mountPath and can be used for inter-container communication. - name: shared-data - mountPath: /shared-data - enableInitContainers: true # Controls whether the initContainers should be included in the pod spec extraContainers: - name: circuitbreaker image: camillarhi/circuitbreaker:latest @@ -44,11 +40,12 @@ nodes: - "--network=regtest" - "--rpcserver=localhost:10009" - "--tlscertpath=/tls.cert" - - "--macaroonpath=/shared-data/admin.macaroon" + - "--macaroonpath=/macaroon.hex" - "--httplisten=0.0.0.0:9235" volumeMounts: - - name: shared-data - mountPath: /shared-data + - name: config + mountPath: /macaroon.hex + subPath: MACAROON_HEX - name: config mountPath: /tls.cert subPath: tls.cert From acf25a180691495f537dc7b2918a9f2b8fe23d2f Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Thu, 27 Mar 2025 00:46:32 +0100 Subject: [PATCH 23/44] removed init containers and shared volumes --- .../bitcoincore/charts/lnd/templates/pod.yaml | 35 ++----------------- 1 file changed, 3 insertions(+), 32 deletions(-) diff --git a/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml b/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml index a6cc6d645..3945af835 100644 --- a/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml +++ b/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml @@ -21,30 +21,6 @@ spec: {{- end }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 4 }} - {{- if .Values.enableInitContainers }} - initContainers: - - name: prepare-files - image: busybox - command: - - sh - - -c - - | - # Read the hex-encoded macaroon from the ConfigMap - HEX_MACAROON=$(cat /config/MACAROON_HEX) - - # Convert the hex-encoded macaroon to binary format - echo "$HEX_MACAROON" | xxd -r -p > /shared-data/admin.macaroon - - # Ensure the file has the correct permissions - chmod 644 /shared-data/admin.macaroon - volumeMounts: - {{- if .Values.sharedVolume }} - - name: {{ .Values.sharedVolume.name }} - mountPath: {{ .Values.sharedVolume.mountPath }} - {{- end }} - - name: config - mountPath: /config - {{- end }} containers: - name: {{ .Chart.Name }} securityContext: @@ -70,10 +46,6 @@ spec: resources: {{- toYaml .Values.resources | nindent 8 }} volumeMounts: - {{- if .Values.sharedVolume }} - - mountPath: {{ .Values.sharedVolume.mountPath }} - name: {{ .Values.sharedVolume.name }} - {{- end }} {{- with .Values.volumeMounts }} {{- toYaml . | nindent 8 }} {{- end }} @@ -86,14 +58,13 @@ spec: - mountPath: /root/.lnd/tls.cert name: config subPath: tls.cert + - mountPath: /root/.lnd/macaroon.hex + name: config + subPath: MACAROON_HEX {{- with .Values.extraContainers }} {{- toYaml . | nindent 4 }} {{- end }} volumes: - {{- if .Values.sharedVolume }} - - name: {{ .Values.sharedVolume.name }} - emptyDir: {} - {{- end }} {{- with .Values.volumes }} {{- toYaml . | nindent 4 }} {{- end }} From 8e7b1359cb9f831f744707c74668f1c69ab007d8 Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Thu, 27 Mar 2025 01:06:22 +0100 Subject: [PATCH 24/44] add circuit breaker to the extra containers --- test/data/ln/node-defaults.yaml | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/test/data/ln/node-defaults.yaml b/test/data/ln/node-defaults.yaml index 62e05199f..49ec0826a 100644 --- a/test/data/ln/node-defaults.yaml +++ b/test/data/ln/node-defaults.yaml @@ -37,4 +37,20 @@ lnd: ports: - name: prom-metrics containerPort: 9332 - protocol: TCP \ No newline at end of file + protocol: TCP + - name: circuitbreaker + image: camillarhi/circuitbreaker:latest + imagePullPolicy: IfNotPresent + args: + - "--network=regtest" + - "--rpcserver=localhost:10009" + - "--tlscertpath=/tls.cert" + - "--macaroonpath=/macaroon.hex" + - "--httplisten=0.0.0.0:9235" + volumeMounts: + - name: config + mountPath: /macaroon.hex + subPath: MACAROON_HEX + - name: config + mountPath: /tls.cert + subPath: tls.cert \ No newline at end of file From 8c541b1fea1fefdbfc070b74cfe305c8e3a81ecc Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Tue, 29 Apr 2025 00:31:39 +0100 Subject: [PATCH 25/44] update network and port to be pulled as global helm variables --- .../charts/lnd/templates/configmap.yaml | 12 +--------- .../bitcoincore/charts/lnd/templates/pod.yaml | 24 ++++++++++++++++--- .../charts/bitcoincore/charts/lnd/values.yaml | 5 ++++ resources/networks/hello/network.yaml | 22 ++++------------- 4 files changed, 31 insertions(+), 32 deletions(-) diff --git a/resources/charts/bitcoincore/charts/lnd/templates/configmap.yaml b/resources/charts/bitcoincore/charts/lnd/templates/configmap.yaml index a47be0d52..54c90ff37 100644 --- a/resources/charts/bitcoincore/charts/lnd/templates/configmap.yaml +++ b/resources/charts/bitcoincore/charts/lnd/templates/configmap.yaml @@ -38,18 +38,8 @@ data: AwEHoUQDQgAEBVltIvaTlAQI/3FFatTqVflZuZdRJ0SmRMSJrFLPtp0fxE7hmteS t6gjQriy90fP8j9OJXBNAjt915kLY4zVvg== -----END EC PRIVATE KEY----- - MACAROON_HEX: | - 0201036c6e6402f801030a1062beabbf2a614b112128afa0c0b4fdd61201301a160a0 - 761646472657373120472656164120577726974651a130a04696e666f120472656164 - 120577726974651a170a08696e766f69636573120472656164120577726974651a210 - a086d616361726f6f6e120867656e6572617465120472656164120577726974651a16 - 0a076d657373616765120472656164120577726974651a170a086f6666636861696e1 - 20472656164120577726974651a160a076f6e636861696e1204726561641205777269 - 74651a140a057065657273120472656164120577726974651a180a067369676e65721 - 20867656e657261746512047265616400000620b17be53e367290871681055d0de155 - 87f6d1cd47d1248fe2662ae27f62cfbdc6 - + 0201036c6e6402f801030a1062beabbf2a614b112128afa0c0b4fdd61201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e657261746512047265616400000620b17be53e367290871681055d0de15587f6d1cd47d1248fe2662ae27f62cfbdc6 --- apiVersion: v1 kind: ConfigMap diff --git a/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml b/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml index 3945af835..2199f556b 100644 --- a/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml +++ b/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml @@ -58,12 +58,28 @@ spec: - mountPath: /root/.lnd/tls.cert name: config subPath: tls.cert - - mountPath: /root/.lnd/macaroon.hex - name: config - subPath: MACAROON_HEX + - name: shared-volume + mountPath: /root/.lnd/ {{- with .Values.extraContainers }} {{- toYaml . | nindent 4 }} {{- end }} + {{- if .Values.circuitbreaker.enabled }} + - name: circuitbreaker + image: {{ .Values.circuitbreaker.image | quote }} + imagePullPolicy: IfNotPresent + args: + - "--network={{ .Values.global.chain }}" + - "--rpcserver=localhost:{{ .Values.RPCPort }}" + - "--tlscertpath=/tls.cert" + - "--macaroonpath=/root/.lnd/data/chain/bitcoin/{{ .Values.global.chain }}/admin.macaroon" + - "--httplisten=0.0.0.0:{{ .Values.circuitbreaker.httpPort }}" + volumeMounts: + - name: shared-volume + mountPath: /root/.lnd/ + - name: config + mountPath: /tls.cert + subPath: tls.cert + {{- end }} volumes: {{- with .Values.volumes }} {{- toYaml . | nindent 4 }} @@ -71,6 +87,8 @@ spec: - configMap: name: {{ include "lnd.fullname" . }} name: config + - name: shared-volume + emptyDir: {} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 4 }} diff --git a/resources/charts/bitcoincore/charts/lnd/values.yaml b/resources/charts/bitcoincore/charts/lnd/values.yaml index d56e65bf4..a33c40ee1 100644 --- a/resources/charts/bitcoincore/charts/lnd/values.yaml +++ b/resources/charts/bitcoincore/charts/lnd/values.yaml @@ -132,3 +132,8 @@ config: "" defaultConfig: "" channels: [] + +circuitbreaker: + enabled: false # Default to disabled + image: carlakirkcohen/circuitbreaker:attackathon-ln_51_attacker + httpPort: 9235 \ No newline at end of file diff --git a/resources/networks/hello/network.yaml b/resources/networks/hello/network.yaml index 66ab9e9a1..75db08343 100644 --- a/resources/networks/hello/network.yaml +++ b/resources/networks/hello/network.yaml @@ -32,24 +32,10 @@ nodes: target: tank-0004-ln capacity: 100000 push_amt: 50000 - extraContainers: - - name: circuitbreaker - image: camillarhi/circuitbreaker:latest - imagePullPolicy: IfNotPresent - args: - - "--network=regtest" - - "--rpcserver=localhost:10009" - - "--tlscertpath=/tls.cert" - - "--macaroonpath=/macaroon.hex" - - "--httplisten=0.0.0.0:9235" - volumeMounts: - - name: config - mountPath: /macaroon.hex - subPath: MACAROON_HEX - - name: config - mountPath: /tls.cert - subPath: tls.cert - + circuitbreaker: + enabled: true # This enables circuitbreaker for this node + httpPort: 9235 # Can override defaults per-node + - name: tank-0004 addnode: - tank-0000 From b775b6b895d60ce81c018e9201105c401e3c1865 Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Tue, 29 Apr 2025 00:43:22 +0100 Subject: [PATCH 26/44] remove extracontainers from test file --- test/data/ln/node-defaults.yaml | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/test/data/ln/node-defaults.yaml b/test/data/ln/node-defaults.yaml index 49ec0826a..62e05199f 100644 --- a/test/data/ln/node-defaults.yaml +++ b/test/data/ln/node-defaults.yaml @@ -37,20 +37,4 @@ lnd: ports: - name: prom-metrics containerPort: 9332 - protocol: TCP - - name: circuitbreaker - image: camillarhi/circuitbreaker:latest - imagePullPolicy: IfNotPresent - args: - - "--network=regtest" - - "--rpcserver=localhost:10009" - - "--tlscertpath=/tls.cert" - - "--macaroonpath=/macaroon.hex" - - "--httplisten=0.0.0.0:9235" - volumeMounts: - - name: config - mountPath: /macaroon.hex - subPath: MACAROON_HEX - - name: config - mountPath: /tls.cert - subPath: tls.cert \ No newline at end of file + protocol: TCP \ No newline at end of file From bb663f84b237f634f22c0b41dac109d0e6adee13 Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Tue, 29 Apr 2025 15:14:46 +0100 Subject: [PATCH 27/44] Include unit tests for circuitbreaker --- test/data/ln/network.yaml | 4 ++ test/ln_basic_test.py | 79 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/test/data/ln/network.yaml b/test/data/ln/network.yaml index 5d30686d4..8006b568f 100644 --- a/test/data/ln/network.yaml +++ b/test/data/ln/network.yaml @@ -21,6 +21,10 @@ nodes: target: tank-0004-ln capacity: 100000 push_amt: 50000 + circuitbreaker: + enabled: true + httpPort: 9235 + - name: tank-0004 addnode: - tank-0000 diff --git a/test/ln_basic_test.py b/test/ln_basic_test.py index fdb479dbd..805bd065b 100755 --- a/test/ln_basic_test.py +++ b/test/ln_basic_test.py @@ -2,6 +2,10 @@ import json import os +import random +import subprocess +import time +import requests from pathlib import Path from time import sleep @@ -23,11 +27,18 @@ def __init__(self): "tank-0004-ln", "tank-0005-ln", ] - + + self.cb_port = 9235 + self.cb_node = "tank-0003-ln" + self.port_forward = None + def run_test(self): try: # Wait for all nodes to wake up. ln_init will start automatically self.setup_network() + + # Test circuit breaker API + self.test_circuit_breaker_api() # Send a payment across channels opened automatically by ln_init self.pay_invoice(sender="tank-0005-ln", recipient="tank-0003-ln") @@ -119,6 +130,72 @@ def scenario_open_channels(self): scenario_file = self.scen_dir / "test_scenarios" / "ln_init.py" self.log.info(f"Running scenario from: {scenario_file}") self.warnet(f"run {scenario_file} --source_dir={self.scen_dir} --debug") + + def test_circuit_breaker_api(self): + self.log.info("Testing Circuit Breaker API") + + # Set up port forwarding to the circuit breaker + cb_url = self.setup_api_access(self.cb_node) + + self.log.info(f"Testing Circuit Breaker API at {cb_url}") + + # Test /info endpoint + info = self.cb_api_request(cb_url, "get", "/info") + assert "version" in info, "Circuit breaker info missing version" + + # Test /limits endpoint + limits = self.cb_api_request(cb_url, "get", "/limits") + assert isinstance(limits, dict), "Limits should be a dictionary" + + self.log.info("✅ Circuit Breaker API tests passed") + + def setup_api_access(self, pod_name): + """Set up local port forwarding to the circuit breaker""" + port = random.randint(10000, 20000) + self.log.info(f"Setting up port-forward {port}:9235 to {pod_name}") + + self.port_forward = subprocess.Popen( + ["kubectl", "port-forward", pod_name, f"{port}:{self.cb_port}"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + # prevent subprocess from holding up execution + start_new_session=True + ) + + cb_url = f"http://localhost:{port}/api" + if not self.wait_for_port_forward_ready(cb_url): + self.port_forward.terminate() + raise Exception("Port-forward failed to become ready") + + return cb_url + + def wait_for_port_forward_ready(self, cb_url, timeout=60): + """Wait until we can successfully connect to the API""" + start_time = time.time() + while time.time() - start_time < timeout: + try: + response = requests.get(f"{cb_url}/info", timeout=2) + if response.status_code == 200: + return True + except requests.exceptions.RequestException: + time.sleep(1) + return False + + def cb_api_request(self, base_url, method, endpoint, data=None): + url = f"{base_url}{endpoint}" + try: + if method == "get": + response = requests.get(url) + elif method == "post": + response = requests.post(url, json=data) + else: + raise ValueError(f"Unsupported method: {method}") + + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + self.log.error(f"Circuit Breaker API request failed: {e}") + raise if __name__ == "__main__": From 746b9f706bc04a71bb1b1362cf36b519ad7f421e Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Tue, 29 Apr 2025 15:24:15 +0100 Subject: [PATCH 28/44] update file linting and formating --- test/ln_basic_test.py | 44 +++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/test/ln_basic_test.py b/test/ln_basic_test.py index 805bd065b..90297afac 100755 --- a/test/ln_basic_test.py +++ b/test/ln_basic_test.py @@ -27,16 +27,16 @@ def __init__(self): "tank-0004-ln", "tank-0005-ln", ] - + self.cb_port = 9235 self.cb_node = "tank-0003-ln" self.port_forward = None - + def run_test(self): try: # Wait for all nodes to wake up. ln_init will start automatically self.setup_network() - + # Test circuit breaker API self.test_circuit_breaker_api() @@ -130,45 +130,45 @@ def scenario_open_channels(self): scenario_file = self.scen_dir / "test_scenarios" / "ln_init.py" self.log.info(f"Running scenario from: {scenario_file}") self.warnet(f"run {scenario_file} --source_dir={self.scen_dir} --debug") - + def test_circuit_breaker_api(self): self.log.info("Testing Circuit Breaker API") - - # Set up port forwarding to the circuit breaker + + # Set up port forwarding to the circuit breaker cb_url = self.setup_api_access(self.cb_node) - + self.log.info(f"Testing Circuit Breaker API at {cb_url}") - + # Test /info endpoint info = self.cb_api_request(cb_url, "get", "/info") assert "version" in info, "Circuit breaker info missing version" - + # Test /limits endpoint limits = self.cb_api_request(cb_url, "get", "/limits") assert isinstance(limits, dict), "Limits should be a dictionary" - + self.log.info("✅ Circuit Breaker API tests passed") - + def setup_api_access(self, pod_name): """Set up local port forwarding to the circuit breaker""" port = random.randint(10000, 20000) self.log.info(f"Setting up port-forward {port}:9235 to {pod_name}") - + self.port_forward = subprocess.Popen( - ["kubectl", "port-forward", pod_name, f"{port}:{self.cb_port}"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - # prevent subprocess from holding up execution - start_new_session=True + ["kubectl", "port-forward", pod_name, f"{port}:{self.cb_port}"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + # prevent subprocess from holding up execution + start_new_session=True, ) - + cb_url = f"http://localhost:{port}/api" if not self.wait_for_port_forward_ready(cb_url): self.port_forward.terminate() raise Exception("Port-forward failed to become ready") - + return cb_url - + def wait_for_port_forward_ready(self, cb_url, timeout=60): """Wait until we can successfully connect to the API""" start_time = time.time() @@ -180,7 +180,7 @@ def wait_for_port_forward_ready(self, cb_url, timeout=60): except requests.exceptions.RequestException: time.sleep(1) return False - + def cb_api_request(self, base_url, method, endpoint, data=None): url = f"{base_url}{endpoint}" try: @@ -190,7 +190,7 @@ def cb_api_request(self, base_url, method, endpoint, data=None): response = requests.post(url, json=data) else: raise ValueError(f"Unsupported method: {method}") - + response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: From 914ba77c34193ff8815d6df77bde4b3ade3d20c9 Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Tue, 29 Apr 2025 16:13:57 +0100 Subject: [PATCH 29/44] fix failing tests --- test/ln_basic_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ln_basic_test.py b/test/ln_basic_test.py index 90297afac..8b41c24f0 100755 --- a/test/ln_basic_test.py +++ b/test/ln_basic_test.py @@ -3,9 +3,9 @@ import json import os import random +import requests import subprocess import time -import requests from pathlib import Path from time import sleep From f0b0b9b1019623dec42caa402af3df070dd65b9b Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Tue, 29 Apr 2025 16:22:22 +0100 Subject: [PATCH 30/44] increase timeout on port-forwarding --- test/ln_basic_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ln_basic_test.py b/test/ln_basic_test.py index 8b41c24f0..6043898b6 100755 --- a/test/ln_basic_test.py +++ b/test/ln_basic_test.py @@ -169,7 +169,7 @@ def setup_api_access(self, pod_name): return cb_url - def wait_for_port_forward_ready(self, cb_url, timeout=60): + def wait_for_port_forward_ready(self, cb_url, timeout=120): """Wait until we can successfully connect to the API""" start_time = time.time() while time.time() - start_time < timeout: From 074192e948c70751a4d54c3aa913a8bdf6171e60 Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Tue, 29 Apr 2025 16:29:32 +0100 Subject: [PATCH 31/44] increase timeout on port-forwarding --- test/ln_basic_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ln_basic_test.py b/test/ln_basic_test.py index 6043898b6..679400003 100755 --- a/test/ln_basic_test.py +++ b/test/ln_basic_test.py @@ -169,7 +169,7 @@ def setup_api_access(self, pod_name): return cb_url - def wait_for_port_forward_ready(self, cb_url, timeout=120): + def wait_for_port_forward_ready(self, cb_url, timeout=300): """Wait until we can successfully connect to the API""" start_time = time.time() while time.time() - start_time < timeout: From dc56158ffd7a17c52f6ba8c17e4e90a010a729b3 Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Tue, 29 Apr 2025 17:23:41 +0100 Subject: [PATCH 32/44] Set up Kubernetes Service access to the Circuit Breaker API --- test/ln_basic_test.py | 82 ++++++++++++++++++++++++++++++++----------- 1 file changed, 61 insertions(+), 21 deletions(-) diff --git a/test/ln_basic_test.py b/test/ln_basic_test.py index 679400003..e009476cc 100755 --- a/test/ln_basic_test.py +++ b/test/ln_basic_test.py @@ -50,6 +50,7 @@ def run_test(self): self.pay_invoice(sender="tank-0000-ln", recipient="tank-0002-ln") finally: + self.cleanup_kubectl_creted_services() self.cleanup() def setup_network(self): @@ -150,37 +151,67 @@ def test_circuit_breaker_api(self): self.log.info("✅ Circuit Breaker API tests passed") def setup_api_access(self, pod_name): - """Set up local port forwarding to the circuit breaker""" - port = random.randint(10000, 20000) - self.log.info(f"Setting up port-forward {port}:9235 to {pod_name}") - - self.port_forward = subprocess.Popen( - ["kubectl", "port-forward", pod_name, f"{port}:{self.cb_port}"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - # prevent subprocess from holding up execution - start_new_session=True, - ) - - cb_url = f"http://localhost:{port}/api" - if not self.wait_for_port_forward_ready(cb_url): - self.port_forward.terminate() - raise Exception("Port-forward failed to become ready") + """Set up Kubernetes Service access to the Circuit Breaker API""" + # Create a properly labeled service using kubectl expose + service_name = f"{pod_name}-svc" + + self.log.info(f"Creating service {service_name} for pod {pod_name}") + try: + subprocess.run([ + "kubectl", "expose", "pod", pod_name, + "--name", service_name, + "--port", str(self.cb_port), + "--target-port", str(self.cb_port) + ], check=True, capture_output=True, text=True) + except subprocess.CalledProcessError as e: + self.log.error(f"Failed to create service: {e.stderr}") + raise Exception(f"Service creation failed: {e.stderr}") + + # Verify service endpoints exist + def verify_endpoints(): + try: + result = subprocess.run( + ["kubectl", "get", "endpoints", service_name, "-o", "jsonpath='{.subsets[*].addresses[*].ip}'"], + capture_output=True, text=True, check=True + ) + return bool(result.stdout.strip()) + except subprocess.CalledProcessError: + return False + + if not self.wait_for_predicate(verify_endpoints, timeout=30): + self.cleanup() + raise Exception("Service endpoints never became available") + + # Get service URL + service_url = f"http://{service_name}:{self.cb_port}/api" + self.service_to_cleanup = service_name + self.log.info(f"Service URL: {service_url}") + + # Verify the API is responsive + if not self.wait_for_port_forward_ready(service_url): + self.cleanup() + raise Exception("Service API never became responsive") - return cb_url + self.log.info(f"Successfully created service at {service_url}") + return service_url - def wait_for_port_forward_ready(self, cb_url, timeout=300): + def wait_for_port_forward_ready(self, cb_url, timeout=300, retry_interval=5): """Wait until we can successfully connect to the API""" start_time = time.time() + attempts = 0 while time.time() - start_time < timeout: + attempts += 1 try: response = requests.get(f"{cb_url}/info", timeout=2) if response.status_code == 200: + self.log.info(f"Port forward ready after {attempts} attempts") return True - except requests.exceptions.RequestException: - time.sleep(1) + except requests.exceptions.RequestException as e: + self.log.debug(f"Attempt {attempts} failed: {str(e)}") + time.sleep(retry_interval) + self.log.error(f"Port forward not ready after {timeout} seconds") return False - + def cb_api_request(self, base_url, method, endpoint, data=None): url = f"{base_url}{endpoint}" try: @@ -197,6 +228,15 @@ def cb_api_request(self, base_url, method, endpoint, data=None): self.log.error(f"Circuit Breaker API request failed: {e}") raise + def cleanup_kubectl_creted_services(self): + """Clean up any created resources""" + if hasattr(self, 'service_to_cleanup') and self.service_to_cleanup: + self.log.info(f"Deleting service {self.service_to_cleanup}") + subprocess.run( + ["kubectl", "delete", "svc", self.service_to_cleanup], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) if __name__ == "__main__": test = LNBasicTest() From 03954caf542c3c2a18e471432a50a4718e49d4a1 Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Tue, 29 Apr 2025 17:27:02 +0100 Subject: [PATCH 33/44] update file linting and formating --- test/ln_basic_test.py | 51 ++++++++++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/test/ln_basic_test.py b/test/ln_basic_test.py index e009476cc..57f652d45 100755 --- a/test/ln_basic_test.py +++ b/test/ln_basic_test.py @@ -154,25 +154,45 @@ def setup_api_access(self, pod_name): """Set up Kubernetes Service access to the Circuit Breaker API""" # Create a properly labeled service using kubectl expose service_name = f"{pod_name}-svc" - + self.log.info(f"Creating service {service_name} for pod {pod_name}") try: - subprocess.run([ - "kubectl", "expose", "pod", pod_name, - "--name", service_name, - "--port", str(self.cb_port), - "--target-port", str(self.cb_port) - ], check=True, capture_output=True, text=True) + subprocess.run( + [ + "kubectl", + "expose", + "pod", + pod_name, + "--name", + service_name, + "--port", + str(self.cb_port), + "--target-port", + str(self.cb_port), + ], + check=True, + capture_output=True, + text=True, + ) except subprocess.CalledProcessError as e: self.log.error(f"Failed to create service: {e.stderr}") raise Exception(f"Service creation failed: {e.stderr}") - + # Verify service endpoints exist def verify_endpoints(): try: result = subprocess.run( - ["kubectl", "get", "endpoints", service_name, "-o", "jsonpath='{.subsets[*].addresses[*].ip}'"], - capture_output=True, text=True, check=True + [ + "kubectl", + "get", + "endpoints", + service_name, + "-o", + "jsonpath='{.subsets[*].addresses[*].ip}'", + ], + capture_output=True, + text=True, + check=True, ) return bool(result.stdout.strip()) except subprocess.CalledProcessError: @@ -181,12 +201,12 @@ def verify_endpoints(): if not self.wait_for_predicate(verify_endpoints, timeout=30): self.cleanup() raise Exception("Service endpoints never became available") - + # Get service URL service_url = f"http://{service_name}:{self.cb_port}/api" self.service_to_cleanup = service_name self.log.info(f"Service URL: {service_url}") - + # Verify the API is responsive if not self.wait_for_port_forward_ready(service_url): self.cleanup() @@ -211,7 +231,7 @@ def wait_for_port_forward_ready(self, cb_url, timeout=300, retry_interval=5): time.sleep(retry_interval) self.log.error(f"Port forward not ready after {timeout} seconds") return False - + def cb_api_request(self, base_url, method, endpoint, data=None): url = f"{base_url}{endpoint}" try: @@ -230,14 +250,15 @@ def cb_api_request(self, base_url, method, endpoint, data=None): def cleanup_kubectl_creted_services(self): """Clean up any created resources""" - if hasattr(self, 'service_to_cleanup') and self.service_to_cleanup: + if hasattr(self, "service_to_cleanup") and self.service_to_cleanup: self.log.info(f"Deleting service {self.service_to_cleanup}") subprocess.run( ["kubectl", "delete", "svc", self.service_to_cleanup], stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL + stderr=subprocess.DEVNULL, ) + if __name__ == "__main__": test = LNBasicTest() test.run_test() From b634e1e62533d734377d7dd932e2cefba377b8ec Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Tue, 29 Apr 2025 17:32:29 +0100 Subject: [PATCH 34/44] update file linting and formating --- test/ln_basic_test.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/ln_basic_test.py b/test/ln_basic_test.py index 57f652d45..744ade927 100755 --- a/test/ln_basic_test.py +++ b/test/ln_basic_test.py @@ -2,13 +2,12 @@ import json import os -import random -import requests import subprocess import time from pathlib import Path from time import sleep +import requests from test_base import TestBase from warnet.process import stream_command @@ -176,7 +175,7 @@ def setup_api_access(self, pod_name): ) except subprocess.CalledProcessError as e: self.log.error(f"Failed to create service: {e.stderr}") - raise Exception(f"Service creation failed: {e.stderr}") + raise # Verify service endpoints exist def verify_endpoints(): From 64c1f1fec4d04078ecc89b138ce723b013f6f8af Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Tue, 29 Apr 2025 19:43:40 +0100 Subject: [PATCH 35/44] Set up Kubernetes Service access to the Circuit Breaker API --- test/ln_basic_test.py | 100 ++++++++++++++++-------------------------- 1 file changed, 37 insertions(+), 63 deletions(-) diff --git a/test/ln_basic_test.py b/test/ln_basic_test.py index 744ade927..b0509dbf9 100755 --- a/test/ln_basic_test.py +++ b/test/ln_basic_test.py @@ -7,7 +7,6 @@ from pathlib import Path from time import sleep -import requests from test_base import TestBase from warnet.process import stream_command @@ -49,7 +48,7 @@ def run_test(self): self.pay_invoice(sender="tank-0000-ln", recipient="tank-0002-ln") finally: - self.cleanup_kubectl_creted_services() + self.cleanup_kubectl_created_services() self.cleanup() def setup_network(self): @@ -170,84 +169,59 @@ def setup_api_access(self, pod_name): str(self.cb_port), ], check=True, - capture_output=True, - text=True, ) except subprocess.CalledProcessError as e: self.log.error(f"Failed to create service: {e.stderr}") raise - # Verify service endpoints exist - def verify_endpoints(): - try: - result = subprocess.run( - [ - "kubectl", - "get", - "endpoints", - service_name, - "-o", - "jsonpath='{.subsets[*].addresses[*].ip}'", - ], - capture_output=True, - text=True, - check=True, - ) - return bool(result.stdout.strip()) - except subprocess.CalledProcessError: - return False - - if not self.wait_for_predicate(verify_endpoints, timeout=30): - self.cleanup() - raise Exception("Service endpoints never became available") + time.sleep(0.01) - # Get service URL service_url = f"http://{service_name}:{self.cb_port}/api" self.service_to_cleanup = service_name self.log.info(f"Service URL: {service_url}") - # Verify the API is responsive - if not self.wait_for_port_forward_ready(service_url): - self.cleanup() - raise Exception("Service API never became responsive") - self.log.info(f"Successfully created service at {service_url}") return service_url - def wait_for_port_forward_ready(self, cb_url, timeout=300, retry_interval=5): - """Wait until we can successfully connect to the API""" - start_time = time.time() - attempts = 0 - while time.time() - start_time < timeout: - attempts += 1 - try: - response = requests.get(f"{cb_url}/info", timeout=2) - if response.status_code == 200: - self.log.info(f"Port forward ready after {attempts} attempts") - return True - except requests.exceptions.RequestException as e: - self.log.debug(f"Attempt {attempts} failed: {str(e)}") - time.sleep(retry_interval) - self.log.error(f"Port forward not ready after {timeout} seconds") - return False - def cb_api_request(self, base_url, method, endpoint, data=None): - url = f"{base_url}{endpoint}" + """Make API requests using kubectl run curl-test approach""" try: - if method == "get": - response = requests.get(url) - elif method == "post": - response = requests.post(url, json=data) - else: - raise ValueError(f"Unsupported method: {method}") - - response.raise_for_status() - return response.json() - except requests.exceptions.RequestException as e: - self.log.error(f"Circuit Breaker API request failed: {e}") + # Build the curl command + curl_cmd = [ + "kubectl", + "run", + "curl-test", + "--image=curlimages/curl", + "-it", + "--rm", + "--restart=Never", + "--", + "curl", + "-s", + "-X", + method.upper(), + f"{base_url}{endpoint}", + ] + + if data: + curl_cmd.extend(["-d", json.dumps(data), "-H", "Content-Type: application/json"]) + + # Run the command and capture output + result = subprocess.run(curl_cmd, check=True, capture_output=True, text=True) + + output = result.stdout.strip() + if 'pod "curl-test" deleted' in output: + output = output.replace('pod "curl-test" deleted', "").strip() + + return json.loads(output) + except subprocess.CalledProcessError as e: + self.log.error(f"API request failed: {e.stderr}") + raise + except json.JSONDecodeError: + self.log.error(f"Invalid JSON response: {result.stdout}") raise - def cleanup_kubectl_creted_services(self): + def cleanup_kubectl_created_services(self): """Clean up any created resources""" if hasattr(self, "service_to_cleanup") and self.service_to_cleanup: self.log.info(f"Deleting service {self.service_to_cleanup}") From dc82fa4cbdef1fb4da19962215692ac4354c9027 Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Tue, 29 Apr 2025 19:53:04 +0100 Subject: [PATCH 36/44] remove interactive flags --- test/ln_basic_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/ln_basic_test.py b/test/ln_basic_test.py index b0509dbf9..03fa3a219 100755 --- a/test/ln_basic_test.py +++ b/test/ln_basic_test.py @@ -192,12 +192,12 @@ def cb_api_request(self, base_url, method, endpoint, data=None): "run", "curl-test", "--image=curlimages/curl", - "-it", "--rm", "--restart=Never", + "--quiet", "--", "curl", - "-s", + "-sS", "-X", method.upper(), f"{base_url}{endpoint}", From c265bb19fadfe02b7aa99ca1a764fe8948979a98 Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Tue, 29 Apr 2025 20:04:12 +0100 Subject: [PATCH 37/44] remove interactive flags --- test/ln_basic_test.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/test/ln_basic_test.py b/test/ln_basic_test.py index 03fa3a219..960738064 100755 --- a/test/ln_basic_test.py +++ b/test/ln_basic_test.py @@ -189,17 +189,14 @@ def cb_api_request(self, base_url, method, endpoint, data=None): # Build the curl command curl_cmd = [ "kubectl", - "run", - "curl-test", - "--image=curlimages/curl", - "--rm", - "--restart=Never", - "--quiet", + "exec", + self.cb_node, "--", "curl", "-sS", "-X", method.upper(), + method.upper(), f"{base_url}{endpoint}", ] From 9035bd618da9e97979b942915b03180a6c9067eb Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Tue, 29 Apr 2025 20:04:37 +0100 Subject: [PATCH 38/44] remove interactive flags --- test/ln_basic_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/ln_basic_test.py b/test/ln_basic_test.py index 960738064..285de12b9 100755 --- a/test/ln_basic_test.py +++ b/test/ln_basic_test.py @@ -196,7 +196,6 @@ def cb_api_request(self, base_url, method, endpoint, data=None): "-sS", "-X", method.upper(), - method.upper(), f"{base_url}{endpoint}", ] From 46bb5b63de148ff146350377b8eff65de1e157bd Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Tue, 29 Apr 2025 20:52:43 +0100 Subject: [PATCH 39/44] update circuitbreaker request method --- test/ln_basic_test.py | 90 ++++++++++++++++++++++++------------------- 1 file changed, 51 insertions(+), 39 deletions(-) diff --git a/test/ln_basic_test.py b/test/ln_basic_test.py index 285de12b9..5382d5576 100755 --- a/test/ln_basic_test.py +++ b/test/ln_basic_test.py @@ -2,11 +2,13 @@ import json import os +import random import subprocess import time from pathlib import Path from time import sleep +import requests from test_base import TestBase from warnet.process import stream_command @@ -174,7 +176,7 @@ def setup_api_access(self, pod_name): self.log.error(f"Failed to create service: {e.stderr}") raise - time.sleep(0.01) + time.sleep(50) # Wait for the service to be created service_url = f"http://{service_name}:{self.cb_port}/api" self.service_to_cleanup = service_name @@ -184,48 +186,58 @@ def setup_api_access(self, pod_name): return service_url def cb_api_request(self, base_url, method, endpoint, data=None): - """Make API requests using kubectl run curl-test approach""" try: - # Build the curl command - curl_cmd = [ - "kubectl", - "exec", - self.cb_node, - "--", - "curl", - "-sS", - "-X", - method.upper(), - f"{base_url}{endpoint}", - ] - - if data: - curl_cmd.extend(["-d", json.dumps(data), "-H", "Content-Type: application/json"]) - - # Run the command and capture output - result = subprocess.run(curl_cmd, check=True, capture_output=True, text=True) - - output = result.stdout.strip() - if 'pod "curl-test" deleted' in output: - output = output.replace('pod "curl-test" deleted', "").strip() - - return json.loads(output) - except subprocess.CalledProcessError as e: - self.log.error(f"API request failed: {e.stderr}") - raise - except json.JSONDecodeError: - self.log.error(f"Invalid JSON response: {result.stdout}") + _, netloc_path = base_url.split("://", 1) + netloc, *path_parts = netloc_path.split("/") + service_name, _, port = netloc.partition(":") + port = port or "80" # Default port if not specified + base_path = "/" + "/".join(path_parts) if path_parts else "/" + + # Set up port forwarding with context manager + local_port = random.randint(10000, 20000) + with self._port_forward(service_name, port, local_port): + # Construct URL using urllib.parse for robustness + full_url = f"http://localhost:{local_port}{base_path.rstrip('/')}/{endpoint.lstrip('/')}" + self.log.debug(f"API request to: {full_url}") + + response = self._make_request(method, full_url, data) + response.raise_for_status() + return response.json() + + except Exception as e: + self.log.error(f"API request failed: {str(e)}") raise + def _port_forward(self, service_name, port, local_port): + """Context manager for port forwarding""" + pf = subprocess.Popen( + ["kubectl", "port-forward", f"svc/{service_name}", f"{local_port}:{port}"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + time.sleep(2) # Allow port-forward to establish + try: + yield pf + finally: + pf.terminate() + pf.wait() + + def _make_request(self, method, url, data=None): + """Helper method for making HTTP requests""" + kwargs = {"timeout": 30} + if data: + kwargs["json"] = data + return requests.request(method.lower(), url, **kwargs) + def cleanup_kubectl_created_services(self): - """Clean up any created resources""" - if hasattr(self, "service_to_cleanup") and self.service_to_cleanup: - self.log.info(f"Deleting service {self.service_to_cleanup}") - subprocess.run( - ["kubectl", "delete", "svc", self.service_to_cleanup], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) + """Clean up any created resources""" + if hasattr(self, "service_to_cleanup") and self.service_to_cleanup: + self.log.info(f"Deleting service {self.service_to_cleanup}") + subprocess.run( + ["kubectl", "delete", "svc", self.service_to_cleanup], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) if __name__ == "__main__": From c4289d12051430c1077267585873346b9153288d Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Tue, 29 Apr 2025 20:53:57 +0100 Subject: [PATCH 40/44] update docker image tag --- resources/charts/bitcoincore/charts/lnd/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/charts/bitcoincore/charts/lnd/values.yaml b/resources/charts/bitcoincore/charts/lnd/values.yaml index a33c40ee1..971a8e72b 100644 --- a/resources/charts/bitcoincore/charts/lnd/values.yaml +++ b/resources/charts/bitcoincore/charts/lnd/values.yaml @@ -135,5 +135,5 @@ channels: [] circuitbreaker: enabled: false # Default to disabled - image: carlakirkcohen/circuitbreaker:attackathon-ln_51_attacker + image: carlakirkcohen/circuitbreaker:attackathon-test httpPort: 9235 \ No newline at end of file From 0e440f580e1b711ed046b8ba7a9dc2563229b807 Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Tue, 29 Apr 2025 20:55:28 +0100 Subject: [PATCH 41/44] remove change --- .../charts/bitcoincore/charts/lnd/templates/configmap.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/charts/bitcoincore/charts/lnd/templates/configmap.yaml b/resources/charts/bitcoincore/charts/lnd/templates/configmap.yaml index 54c90ff37..861f406c4 100644 --- a/resources/charts/bitcoincore/charts/lnd/templates/configmap.yaml +++ b/resources/charts/bitcoincore/charts/lnd/templates/configmap.yaml @@ -38,7 +38,7 @@ data: AwEHoUQDQgAEBVltIvaTlAQI/3FFatTqVflZuZdRJ0SmRMSJrFLPtp0fxE7hmteS t6gjQriy90fP8j9OJXBNAjt915kLY4zVvg== -----END EC PRIVATE KEY----- - MACAROON_HEX: | + MACAROON_HEX: 0201036c6e6402f801030a1062beabbf2a614b112128afa0c0b4fdd61201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e657261746512047265616400000620b17be53e367290871681055d0de15587f6d1cd47d1248fe2662ae27f62cfbdc6 --- apiVersion: v1 From b4d66e7a25d2409833e463d76b5597306d15c338 Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Tue, 29 Apr 2025 20:59:25 +0100 Subject: [PATCH 42/44] fix lint --- test/ln_basic_test.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/test/ln_basic_test.py b/test/ln_basic_test.py index 5382d5576..0c7d90915 100755 --- a/test/ln_basic_test.py +++ b/test/ln_basic_test.py @@ -197,7 +197,9 @@ def cb_api_request(self, base_url, method, endpoint, data=None): local_port = random.randint(10000, 20000) with self._port_forward(service_name, port, local_port): # Construct URL using urllib.parse for robustness - full_url = f"http://localhost:{local_port}{base_path.rstrip('/')}/{endpoint.lstrip('/')}" + full_url = ( + f"http://localhost:{local_port}{base_path.rstrip('/')}/{endpoint.lstrip('/')}" + ) self.log.debug(f"API request to: {full_url}") response = self._make_request(method, full_url, data) @@ -228,16 +230,16 @@ def _make_request(self, method, url, data=None): if data: kwargs["json"] = data return requests.request(method.lower(), url, **kwargs) - + def cleanup_kubectl_created_services(self): - """Clean up any created resources""" - if hasattr(self, "service_to_cleanup") and self.service_to_cleanup: - self.log.info(f"Deleting service {self.service_to_cleanup}") - subprocess.run( - ["kubectl", "delete", "svc", self.service_to_cleanup], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) + """Clean up any created resources""" + if hasattr(self, "service_to_cleanup") and self.service_to_cleanup: + self.log.info(f"Deleting service {self.service_to_cleanup}") + subprocess.run( + ["kubectl", "delete", "svc", self.service_to_cleanup], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) if __name__ == "__main__": From 08903ecada040f13f285ec1543487c78dd7e1319 Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Tue, 29 Apr 2025 21:01:19 +0100 Subject: [PATCH 43/44] remove change --- .../charts/bitcoincore/charts/lnd/templates/configmap.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/resources/charts/bitcoincore/charts/lnd/templates/configmap.yaml b/resources/charts/bitcoincore/charts/lnd/templates/configmap.yaml index 861f406c4..096c764ff 100644 --- a/resources/charts/bitcoincore/charts/lnd/templates/configmap.yaml +++ b/resources/charts/bitcoincore/charts/lnd/templates/configmap.yaml @@ -38,8 +38,7 @@ data: AwEHoUQDQgAEBVltIvaTlAQI/3FFatTqVflZuZdRJ0SmRMSJrFLPtp0fxE7hmteS t6gjQriy90fP8j9OJXBNAjt915kLY4zVvg== -----END EC PRIVATE KEY----- - MACAROON_HEX: - 0201036c6e6402f801030a1062beabbf2a614b112128afa0c0b4fdd61201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e657261746512047265616400000620b17be53e367290871681055d0de15587f6d1cd47d1248fe2662ae27f62cfbdc6 + MACAROON_HEX: 0201036c6e6402f801030a1062beabbf2a614b112128afa0c0b4fdd61201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e657261746512047265616400000620b17be53e367290871681055d0de15587f6d1cd47d1248fe2662ae27f62cfbdc6 --- apiVersion: v1 kind: ConfigMap From c044f17eac19fb5a6c1afbb68770f2d43f46da85 Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Tue, 29 Apr 2025 21:07:07 +0100 Subject: [PATCH 44/44] Set up Kubernetes Service access to the Circuit Breaker API --- test/ln_basic_test.py | 72 +++++++++++++++++++++---------------------- 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/test/ln_basic_test.py b/test/ln_basic_test.py index 0c7d90915..ea271abf4 100755 --- a/test/ln_basic_test.py +++ b/test/ln_basic_test.py @@ -176,7 +176,7 @@ def setup_api_access(self, pod_name): self.log.error(f"Failed to create service: {e.stderr}") raise - time.sleep(50) # Wait for the service to be created + time.sleep(51) # Wait for the service to be created service_url = f"http://{service_name}:{self.cb_port}/api" self.service_to_cleanup = service_name @@ -186,51 +186,49 @@ def setup_api_access(self, pod_name): return service_url def cb_api_request(self, base_url, method, endpoint, data=None): + """Universal API request handler with proper path handling""" try: - _, netloc_path = base_url.split("://", 1) - netloc, *path_parts = netloc_path.split("/") - service_name, _, port = netloc.partition(":") - port = port or "80" # Default port if not specified - base_path = "/" + "/".join(path_parts) if path_parts else "/" + # Parse the base URL components + url_parts = base_url.split("://")[1].split("/") + service_name = url_parts[0].split(":")[0] + port = url_parts[0].split(":")[1] if ":" in url_parts[0] else "80" + base_path = "/" + "/".join(url_parts[1:]) if len(url_parts) > 1 else "/" - # Set up port forwarding with context manager + # Set up port forwarding local_port = random.randint(10000, 20000) - with self._port_forward(service_name, port, local_port): - # Construct URL using urllib.parse for robustness - full_url = ( - f"http://localhost:{local_port}{base_path.rstrip('/')}/{endpoint.lstrip('/')}" - ) - self.log.debug(f"API request to: {full_url}") - - response = self._make_request(method, full_url, data) + pf = subprocess.Popen( + ["kubectl", "port-forward", f"svc/{service_name}", f"{local_port}:{port}"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + try: + # Wait for port-forward to establish + time.sleep(2) + + # Construct the full local URL with proper path handling + full_path = base_path.rstrip("/") + "/" + endpoint.lstrip("/") + local_url = f"http://localhost:{local_port}{full_path}" + + self.log.debug(f"Attempting API request to: {local_url}") + + # Make the request + if method.lower() == "get": + response = requests.get(local_url, timeout=30) + else: + response = requests.post(local_url, json=data, timeout=30) + response.raise_for_status() return response.json() + finally: + pf.terminate() + pf.wait() + except Exception as e: - self.log.error(f"API request failed: {str(e)}") + self.log.error(f"API request to {local_url} failed: {str(e)}") raise - def _port_forward(self, service_name, port, local_port): - """Context manager for port forwarding""" - pf = subprocess.Popen( - ["kubectl", "port-forward", f"svc/{service_name}", f"{local_port}:{port}"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - time.sleep(2) # Allow port-forward to establish - try: - yield pf - finally: - pf.terminate() - pf.wait() - - def _make_request(self, method, url, data=None): - """Helper method for making HTTP requests""" - kwargs = {"timeout": 30} - if data: - kwargs["json"] = data - return requests.request(method.lower(), url, **kwargs) - def cleanup_kubectl_created_services(self): """Clean up any created resources""" if hasattr(self, "service_to_cleanup") and self.service_to_cleanup: